summaryrefslogtreecommitdiff
path: root/internal/readings/offline.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-14 13:22:16 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-14 13:22:16 +0200
commit913974b10a993af251b25125d1a417c452ad785c (patch)
tree5dd82811c100de6daa995bebd115a12434296508 /internal/readings/offline.go
parentd7da4b09f7775276231d0241cfe2700d247728ee (diff)
parent3b32c002d3eddda5ece9422442717657b9fee63b (diff)
downloadlectio-913974b10a993af251b25125d1a417c452ad785c.tar.gz
lectio-913974b10a993af251b25125d1a417c452ad785c.zip
Merge branch 'polish-ui-and-calendar': the gomobile facade and the EF calendar fixes
Two bodies of work that shared a branch. The gomobile facade (2026-08-03..05): mobile.PartLabels, Days, and the observed rank on DayInfo, so dlectio stops hardcoding part IDs and rank strings; the 1962 part labels become i18n data; the documented gomobile bind command is corrected so it reproduces the shipped .aar. The EF calendar fixes (2026-08-12): seven defects found by differencing this engine against colitur, a second 1962 implementation built from the Missal's General Rubrics rather than from this codebase. RG 96 transfers were not skipping II-class days; a II-class privileged feria was not yielding to a feast; Sunday ranks, the two Rose Sundays and Holy Thursday's colour were wrong; and scripts/gen-sanctoral-ef inferred ranks, deduped and tagged classes wrongly, which put 15 III-class feasts into the shipped tridentine-calendar.ini as bare commemorations and dropped four entries outright. Holy Thursday was violet in both engines, which is how a shared lineage hides a defect: this project's ini is generated from missalemeum and colitur's data was bootstrapped from here, so an error inherited by both is invisible to a differential. It took the Missal itself to see it. The EF oracle test now asserts rank and colour, not season alone. One known gap is recorded in the source rather than fixed, as out of scope: RG 95 chained transfers (calendar.go).
Diffstat (limited to 'internal/readings/offline.go')
-rw-r--r--internal/readings/offline.go125
1 files changed, 109 insertions, 16 deletions
diff --git a/internal/readings/offline.go b/internal/readings/offline.go
index d0b8942..b2c16df 100644
--- a/internal/readings/offline.go
+++ b/internal/readings/offline.go
@@ -8,6 +8,7 @@ import (
"github.com/lukaszkasprzak/lectio/internal/caldata"
"github.com/lukaszkasprzak/lectio/internal/calendar"
"github.com/lukaszkasprzak/lectio/internal/config"
+ "github.com/lukaszkasprzak/lectio/internal/i18n"
"github.com/lukaszkasprzak/lectio/internal/liturgy"
"github.com/lukaszkasprzak/lectio/internal/naming"
)
@@ -60,24 +61,68 @@ var ofPart = map[string]struct{ id, heading string }{
"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.
+// efPartHeading gives the traditional (1962) section's ID and heading per UI
+// language; the EF has only an epistle/lesson and a gospel. The label words are
+// i18n data, like the modern ones. Unlike the OF headings these are not
+// translated downstream (render.LocalizeHeading only handles modern IDs), so
+// they are resolved 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"
+ id = "epistola"
case "gospel":
- if pl {
- return "evangelium", "Ewangelia"
- }
- return "evangelium", "Gospel"
+ id = "evangelium"
+ default:
+ return "", ""
+ }
+ heading = i18n.Get(lang).PartLabel[id]
+ if heading == "" {
+ heading = id
+ }
+ return id, heading
+}
+
+// ofPartOrder and efPartOrder are the display orders of each lectionary's
+// sections. They are the single source of truth for which part IDs exist.
+//
+// ofPartOrder's five modern IDs must remain the same *set* as
+// internal/render/render.go's modernPartOrder, which lists them in a
+// different, deliberate order (prefix-match determinism for
+// render.LocalizeHeading, unrelated to display order). Nothing enforces
+// that agreement mechanically -- internal/readings and internal/render do
+// not import each other (adding a cross-package test would create a new
+// dependency edge that does not exist today) -- so if a sixth modern part
+// is ever added here, add it to modernPartOrder too, by hand.
+var (
+ ofPartOrder = []string{"pierwsze_czytanie", "psalm", "drugie_czytanie", "aklamacja", "ewangelia"}
+ efPartOrder = []string{"epistola", "evangelium"}
+)
+
+// ofEmittedPartOrder is the subset of ofPartOrder the offline engine can
+// actually produce, in display order. It excludes "aklamacja":
+// internal/caldata/caldata.go:42 parses only "first", "psalm", "second" and
+// "gospel" out of the lectionary data, so no computed OF reading ever carries
+// Part == "acclamation" and ofPart's "aklamacja" mapping above is never
+// reached. ofPartOrder stays the full five-ID set on purpose -- it also
+// drives render.LocalizeHeading's *label* matching, where a scraped heading
+// can still read "Aklamacja" even though this engine's own readings never
+// produce that section -- so PartIDs, which promises IDs an app can filter
+// on, needs this narrower list rather than reusing or shrinking ofPartOrder.
+var ofEmittedPartOrder = []string{"pierwsze_czytanie", "psalm", "drugie_czytanie", "ewangelia"}
+
+// PartIDs returns the part IDs the given lectionary can emit, in display order.
+// lect takes config.Config.Lectionary's values: "new" or "traditional". An
+// unknown lectionary returns nil. Callers that build per-part UI (the dlectio
+// app's reading filters) must derive their list from this rather than
+// hardcoding IDs.
+func PartIDs(lect string) []string {
+ switch lect {
+ case "new":
+ return append([]string(nil), ofEmittedPartOrder...)
+ case "traditional":
+ return append([]string(nil), efPartOrder...)
}
- return "", ""
+ return nil
}
// sectionsFor turns computed readings into render-ready sections, tagging each
@@ -109,14 +154,62 @@ func sectionsFor(rs []calendar.Reading, form, lang, siglaLang string, tbl *bible
return out
}
-// dayInfo builds the header (celebration name, liturgical colour) 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.
+// 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: displayRank(cfg, day),
+ }
+}
+
+// displayRank is the Ordinary Form's REPORTED rank for the observed
+// celebration -- it never changes calendar.Celebration.Rank (the engine's own
+// field, which drives precedence via ofRankOrder); it only relabels what is
+// handed to a caller here, after the engine has already finished computing.
+//
+// In the 1969 Universal Norms' Table of Liturgical Days, Sunday is its own
+// category, not a solemnity. The calendar engine's temporal.go builds every
+// "Nth Sunday of <season>" day (Ordinary Time, Advent, Lent, the Easter
+// season, Christmas time, and Palm Sunday) with sundayDay(), which sets
+// Rank=solemnity but leaves Class at its zero value (calendar.ClassNone) --
+// solemnity is a placeholder there, not a real classification. A genuinely
+// NAMED solemnity of the Lord that happens to fall on a Sunday (Easter Sunday
+// itself, Pentecost, Ascension/Corpus Christi when transferred, Trinity,
+// Christ the King, Christmas, Epiphany) is built by solemn(), which does set
+// Class=ClassLord, and must keep reporting "solemnity"; a feast of the Lord
+// (Holy Family, Baptism of the Lord) is Rank=feast already and is untouched.
+//
+// Investigated first: temporal.go's own Sunday bool (sundayDay's
+// `Sunday: !priv`) looked like the natural signal, but it does not reach
+// calendar.LiturgicalDay at all (LiturgicalDay carries no such field, only
+// the aggregate ObservedBand), and even if plumbed through it would be the
+// wrong signal here -- by its own doc comment it marks only the band-6
+// Christmas-time/Ordinary-time Sundays, not the band-2 Advent/Lent/Easter
+// Sundays this change must ALSO relabel (the 1st Sunday of Advent from the
+// original bug report is band 2, Privileged=true, Sunday=false). Using
+// Celebration.Class instead of that flag, or of temporalDay.Privileged,
+// covers exactly the sundayDay()-built set in both bands without adding any
+// new field: RankSolemnity + Layer=="temporal" + Class!=ClassLord occurs only
+// from sundayDay(), and (checked against every call site in temporal.go) only
+// ever on an actual Sunday, so the weekday check below is defensive, not
+// load-bearing.
+//
+// The Extraordinary Form (1962) has no such category and is untouched: this
+// only ever fires when the modern form is selected.
+func displayRank(cfg config.Config, day calendar.LiturgicalDay) string {
+ obs := day.Observed
+ if cfg.Selection().Form != "old" &&
+ day.Weekday == time.Sunday &&
+ obs.Layer == "temporal" &&
+ obs.Rank == calendar.RankSolemnity &&
+ obs.Class != calendar.ClassLord {
+ return string(calendar.RankSunday)
}
+ return string(obs.Rank)
}
// celebrationName is the observed celebration's name in the UI language,