summaryrefslogtreecommitdiff
path: root/internal/readings/offline.go
blob: 59a03fa22ea6ead9b7756d1b72a71933f6ad076b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package readings

import (
	"fmt"
	"time"

	"github.com/lukaszkasprzak/lectio/internal/bible"
	"github.com/lukaszkasprzak/lectio/internal/caldata"
	"github.com/lukaszkasprzak/lectio/internal/calendar"
	"github.com/lukaszkasprzak/lectio/internal/config"
	"github.com/lukaszkasprzak/lectio/internal/liturgy"
	"github.com/lukaszkasprzak/lectio/internal/naming"
)

// offlineLoad resolves a day's readings entirely from the embedded calendar
// engine and lectionary data -- no network. It returns the same source-agnostic
// liturgy.Section / liturgy.DayInfo the CLI/TUI/web already render, so the daily
// view is unchanged apart from where its data comes from. Citations are
// lectio's English-canonical authored form; the render localises each one to
// the chosen corpus's Psalter and the user's sigla dialect (see
// render.GatherVersion, bible.OFRef).
func offlineLoad(cfg config.Config, date string) ([]liturgy.Section, liturgy.DayInfo, error) {
	d, err := time.Parse("2006-01-02", date)
	if err != nil {
		return nil, liturgy.DayInfo{}, fmt.Errorf("bad date %q (want YYYY-MM-DD)", date)
	}
	sel := cfg.Selection()
	dir, _ := config.CalendarsDir()
	layers, _ := caldata.Stack(sel.Form, dir, cfg.Use) // Stack falls back to embedded data on error
	day := calendar.Compute(d.UTC(), sel, layers)
	rs := caldata.Readings(sel, layers, d.UTC(), day)
	tbl, _ := bible.LoadBookTable(config.UserBooksINI()) // nil on error -> citations shown as authored
	return sectionsFor(rs, sel.Form, cfg.UILanguage, cfg.SiglaLang(), tbl), dayInfo(cfg, day), nil
}

// citationForms renders a reading's authored (English) citation into its
// display form (the reader's sigla dialect) and its English-canonical lookup
// reference. When the book table is missing or cannot parse the citation, the
// authored form is used verbatim for both.
func citationForms(raw, siglaLang string, tbl *bible.BookTable) (display, ref string) {
	if tbl == nil {
		return raw, raw
	}
	canonical, ok := tbl.ParseRef("en", raw)
	if !ok {
		return raw, raw
	}
	return tbl.FormatRef(siglaLang, canonical), canonical
}

// ofPart maps a computed reading Part to the modern-lectionary section id and
// its Polish heading label. The heading is always the Polish label: the render
// (render.LocalizeHeading) rewrites it to English for an English UI, mirroring
// how the niedziela sections were shaped.
var ofPart = map[string]struct{ id, heading string }{
	"first":       {"pierwsze_czytanie", "1. czytanie"},
	"psalm":       {"psalm", "Psalm"},
	"second":      {"drugie_czytanie", "2. czytanie"},
	"acclamation": {"aklamacja", "Aklamacja"},
	"gospel":      {"ewangelia", "Ewangelia"},
}

// efPartHeading gives the traditional (1962) section's heading per UI language;
// the EF has only an epistle/lesson and a gospel. Unlike the OF headings these
// are not translated downstream, so they are set in the target language here.
func efPartHeading(part, lang string) (id, heading string) {
	pl := lang == "pl"
	switch part {
	case "first":
		if pl {
			return "epistola", "Lekcja"
		}
		return "epistola", "Lesson"
	case "gospel":
		if pl {
			return "evangelium", "Ewangelia"
		}
		return "evangelium", "Gospel"
	}
	return "", ""
}

// sectionsFor turns computed readings into render-ready sections, tagging each
// with the section id and heading its form expects and rendering its citation
// into display (sigla dialect) and lookup (English-canonical) forms.
func sectionsFor(rs []calendar.Reading, form, lang, siglaLang string, tbl *bible.BookTable) []liturgy.Section {
	var out []liturgy.Section
	for _, r := range rs {
		if r.Citation == "" {
			continue
		}
		var id, heading string
		if form == "old" {
			id, heading = efPartHeading(r.Part, lang)
		} else if p, ok := ofPart[r.Part]; ok {
			id, heading = p.id, p.heading
		}
		if id == "" {
			continue // an unknown part carries no section
		}
		display, ref := citationForms(r.Citation, siglaLang, tbl)
		out = append(out, liturgy.Section{
			PartID:   id,
			Heading:  heading,
			Citation: display,
			Ref:      ref,
		})
	}
	return out
}

// dayInfo builds the header (celebration name, liturgical colour, rank) for
// the computed day. Season is left empty: the celebration name already
// carries the temporal identity for temporal days, and the header is a
// nice-to-have.
func dayInfo(cfg config.Config, day calendar.LiturgicalDay) liturgy.DayInfo {
	return liturgy.DayInfo{
		Name:   celebrationName(cfg, day.Observed),
		Colour: string(day.Colour),
		Rank:   string(day.Observed.Rank),
	}
}

// celebrationName is the observed celebration's name in the UI language,
// resolved by naming.CelebrationName (name.<lang> -> English -> Latin ->
// humanized slug). An empty result (unnamed feria) omits the header line.
func celebrationName(cfg config.Config, c calendar.Celebration) string {
	return naming.CelebrationName(cfg.UILanguage, c)
}