From 539a7c71b60115144959f712ef5966f48c63f9f0 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 28 Jul 2026 18:15:29 +0200 Subject: naming: localise liturgical day names in any language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add internal/naming, which renders temporal day names and celebration display names from the calendar engine's slugs in any language. English is the built-in baseline; a language is data -- an embedded lang/.ini (pl shipped) and/or a user file at /names/.ini that overrides it key by key. Names compose from a small vocabulary plus per-language format templates, so word order and grammatical case can differ (e.g. Polish genitive "3. Niedziela Okresu Zwykłego"); any string a language omits falls back to English. - Move day-name generation out of calendar (calendar.HumanizeSlug removed, calendar stays a pure engine) into naming.DayName; the English golden cases carry over verbatim. - naming.CelebrationName unifies the three duplicated resolvers (cli/readings/calfeed): name. -> English -> Latin -> humanized slug. Saint names stay in the calendar data (name.), overridable via calendar layers. - config: ui_language now accepts any code (lower-cased, not clamped to en/pl) so names/.ini applies; UI chrome still resolves en/pl and falls back to English. Add NamesDir(); wire naming.SetUserDir (and the previously unwired bible.SetUserCorporaDir) in the TUI and web mains too, so external corpora and name files work across all three binaries. --- internal/calendar/names.go | 151 ---------------------------------------- internal/calendar/names_test.go | 27 ------- 2 files changed, 178 deletions(-) delete mode 100644 internal/calendar/names.go delete mode 100644 internal/calendar/names_test.go (limited to 'internal/calendar') diff --git a/internal/calendar/names.go b/internal/calendar/names.go deleted file mode 100644 index 935b60c..0000000 --- a/internal/calendar/names.go +++ /dev/null @@ -1,151 +0,0 @@ -package calendar - -import ( - "regexp" - "strconv" - "strings" -) - -var weekdayName = map[string]string{ - "mon": "Monday", "tue": "Tuesday", "wed": "Wednesday", "thu": "Thursday", "fri": "Friday", "sat": "Saturday", "sun": "Sunday", - "monday": "Monday", "tuesday": "Tuesday", "wednesday": "Wednesday", "thursday": "Thursday", "friday": "Friday", "saturday": "Saturday", "sunday": "Sunday", -} - -// namedDay maps a computed temporal slug (with any "ef-" prefix removed) to its -// proper English name, for days that have one. -var namedDay = map[string]string{ - "triduum-thu": "Holy Thursday", "triduum-fri": "Good Friday", "triduum-sat": "Holy Saturday", - "maundy-thursday": "Holy Thursday", "good-friday": "Good Friday", "holy-saturday": "Holy Saturday", - "palm-sunday": "Palm Sunday", "passion-sunday": "Passion Sunday", "low-sunday": "Second Sunday of Easter", - "trinity-sunday": "The Most Holy Trinity", "trinity": "Trinity Sunday", - "corpus-christi": "The Body and Blood of Christ", "sacred-heart": "The Most Sacred Heart of Jesus", - "christ-the-king": "Our Lord Jesus Christ, King of the Universe", - "easter-sunday": "Easter Sunday", "pentecost": "Pentecost Sunday", "ascension": "The Ascension of the Lord", - "holy-family": "The Holy Family of Jesus, Mary and Joseph", "baptism-of-the-lord": "The Baptism of the Lord", - "epiphany": "The Epiphany of the Lord", "christmas": "The Nativity of the Lord", "nativity": "The Nativity of the Lord", - "circumcision": "The Circumcision of the Lord", "ash-wednesday": "Ash Wednesday", - "ascension-vigil": "Vigil of the Ascension", "pentecost-vigil": "Vigil of Pentecost", - "christmas-sunday-sun": "Second Sunday after the Nativity", - "mary-mother-of-god-octave-of-christmas": "Mary, the Holy Mother of God", -} - -var ( - reSundayNum = regexp.MustCompile(`^(ordinary|advent|lent|easter)-sunday-(\d+)$`) - reWeekday = regexp.MustCompile(`^(.+)-(\d+)-([a-z]+)$`) // -- - reOctave = regexp.MustCompile(`^easter-octave-([a-z]+)$`) - reAfterAsh = regexp.MustCompile(`^lent-after-ashes-([a-z]+)$`) - reHolyWeek = regexp.MustCompile(`^holy-week-([a-z]+)$`) - reAdventDec = regexp.MustCompile(`^advent-dec-(\d+)$`) - reXmasDate = regexp.MustCompile(`^christmas-(dec|jan)-(\d+)$`) - reAftEpiph = regexp.MustCompile(`^christmas-after-epiphany-([a-z]+)$`) - reEmber = regexp.MustCompile(`^(september|advent)-ember-([a-z]+)$`) - reSeasonSun = regexp.MustCompile(`^(.+)-sunday-(\d+)$`) -) - -func ordinal(n int) string { - s := strconv.Itoa(n) - if n%100 >= 11 && n%100 <= 13 { - return s + "th" - } - switch n % 10 { - case 1: - return s + "st" - case 2: - return s + "nd" - case 3: - return s + "rd" - } - return s + "th" -} - -func titleCase(s string) string { - small := map[string]bool{"of": true, "the": true, "in": true, "after": true, "before": true} - words := strings.Fields(s) - for i, w := range words { - if i > 0 && small[w] { - continue - } - words[i] = strings.ToUpper(w[:1]) + w[1:] - } - return strings.Join(words, " ") -} - -// seasonPhrase renders a season slug ("ordinary", "time-after-pentecost") and the -// preposition that introduces its numbered weeks. -func seasonPhrase(season string) (name, prep string) { - switch season { - case "ordinary": - return "Ordinary Time", "in" - case "advent", "lent", "easter", "septuagesima", "passiontide": - return titleCase(season), "of" - } - if rest := strings.TrimPrefix(season, "time-after-"); rest != season { - return titleCase(rest), "after" // e.g. "Week after Pentecost" - } - return titleCase(strings.ReplaceAll(season, "-", " ")), "of" -} - -// HumanizeSlug turns a computed temporal-day slug into a proper English name (used -// when a temporal celebration carries no explicit name). -func HumanizeSlug(slug string) string { - s := strings.TrimPrefix(slug, "ef-") - if n, ok := namedDay[s]; ok { - return n - } - if m := reSundayNum.FindStringSubmatch(s); m != nil { - n, _ := strconv.Atoi(m[2]) - name, _ := seasonPhrase(m[1]) - if m[1] == "ordinary" { - return ordinal(n) + " Sunday in " + name - } - return ordinal(n) + " Sunday of " + name - } - if m := reOctave.FindStringSubmatch(s); m != nil { - return weekdayName[m[1]] + " in the Octave of Easter" - } - if m := reAfterAsh.FindStringSubmatch(s); m != nil { - if m[1] == "wed" { - return "Ash Wednesday" - } - return weekdayName[m[1]] + " after Ash Wednesday" - } - if m := reHolyWeek.FindStringSubmatch(s); m != nil { - return weekdayName[m[1]] + " of Holy Week" - } - if m := reAdventDec.FindStringSubmatch(s); m != nil { - n, _ := strconv.Atoi(m[1]) - return "December " + strconv.Itoa(n) - } - if m := reXmasDate.FindStringSubmatch(s); m != nil { - n, _ := strconv.Atoi(m[2]) - if m[1] == "dec" { - return "December " + strconv.Itoa(n) - } - return "January " + strconv.Itoa(n) - } - if m := reAftEpiph.FindStringSubmatch(s); m != nil { - return weekdayName[m[1]] + " after the Epiphany" - } - if m := reEmber.FindStringSubmatch(s); m != nil { - return "Ember " + weekdayName[m[2]] + " of " + titleCase(m[1]) - } - if m := reSeasonSun.FindStringSubmatch(s); m != nil { // -sunday-N (EF) - n, _ := strconv.Atoi(m[2]) - name, prep := seasonPhrase(m[1]) - return ordinal(n) + " Sunday " + prep + " " + name - } - if m := reWeekday.FindStringSubmatch(s); m != nil { - if wd, ok := weekdayName[m[3]]; ok { - n, _ := strconv.Atoi(m[2]) - name, prep := seasonPhrase(m[1]) - if n == 0 { // the pre-Palm-Sunday week of Passiontide has no week number - if m[1] == "passiontide" { - return wd + " of Passion Week" - } - return wd + " " + prep + " " + name - } - return wd + " of the " + ordinal(n) + " Week " + prep + " " + name - } - } - return titleCase(strings.ReplaceAll(s, "-", " ")) -} diff --git a/internal/calendar/names_test.go b/internal/calendar/names_test.go deleted file mode 100644 index 217f481..0000000 --- a/internal/calendar/names_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package calendar - -import "testing" - -func TestHumanizeSlug(t *testing.T) { - cases := map[string]string{ - "ordinary-sunday-11": "11th Sunday in Ordinary Time", - "ordinary-11-tue": "Tuesday of the 11th Week in Ordinary Time", - "advent-2-mon": "Monday of the 2nd Week of Advent", - "advent-dec-17": "December 17", - "lent-after-ashes-wed": "Ash Wednesday", - "lent-after-ashes-thu": "Thursday after Ash Wednesday", - "triduum-fri": "Good Friday", - "easter-octave-mon": "Monday in the Octave of Easter", - "christmas-jan-2": "January 2", - "christmas-after-epiphany-mon": "Monday after the Epiphany", - "trinity-sunday": "The Most Holy Trinity", - "ef-time-after-pentecost-4-tue": "Tuesday of the 4th Week after Pentecost", - "ef-passiontide-0-thursday": "Thursday of Passion Week", - "ef-september-ember-wed": "Ember Wednesday of September", - } - for slug, want := range cases { - if got := HumanizeSlug(slug); got != want { - t.Errorf("HumanizeSlug(%q) = %q, want %q", slug, got, want) - } - } -} -- cgit v1.3