diff --git a/TODO.md b/TODO.md index 2d6fe3d..2a642b8 100644 --- a/TODO.md +++ b/TODO.md @@ -34,10 +34,17 @@ across every hour and content type in this app. date/entry already resolves correctly end-to-end); what's left is authoring/importing more content in that same shape across the rest of the calendar: - Latin saint/octave/named-feast names for the bilingual date/day-label header (see - "Bilingual, merged date/day-label header" below) — the `{en, la}` mechanism is built and - every proper name falls back to English gracefully, but zero saints, octaves, or named - temporal feasts have an authored Latin name anywhere in `src/data/**`. Weekday names, - month names, rank labels, and season/ordinal phrasing already have real Latin. + "Bilingual, merged date/day-label header" below and the dated log's new entry at the + bottom of this file) — `SanctoralIdentity`/`SaintRecord`/`TemporalFeastRecord`/ + `ActiveOctave` now carry a real optional `nameLa` field, threaded end-to-end through + `calendar/day-label.ts`'s `biName` helper (live-verified: winner, octave-headline, and + commemoration branches all render authored Latin, not the English fallback, for a proof + set of 12 records — all 11 named temporal feasts plus St. Lawrence). Zero saints besides + St. Lawrence have an authored `nameLa` yet — this is now real bulk-content work in an + already-proven shape, same "one saint at a time, sourced against the reference engine's + own `[Officium]`/`[Rank]` title, nominative-noun-phrase form" methodology used for the + named temporal feasts. Weekday names, month names, rank labels, and season/ordinal + phrasing already had real Latin before this. - Matins sanctoral-side readings: the per-book Matins responsory pool has only one book seeded (`isa`) — needs expanding. Bible-plan TSV content also isn't yet in a test table (see "Bible-plan TSV — bulk conversion done" below). @@ -4383,3 +4390,42 @@ not just under this app's implementation of them. Neither is "unauthored content pending" — there is no historical text for either case to author. Dropped from the "Open work" list entirely rather than marked done-with-a-caveat, since authoring something anyway would mean inventing text that never existed. + +### Bilingual header `nameLa` mechanism (2026-08-31) + +Picked up the standing "Latin saint/octave/named-feast names" backlog item. The `Bi` +(`{en, la}`) *display* wrapper already existed in `calendar/day-label.ts`, but the underlying +*data* had no bilingual name field at all — `SaintRecord.name`, `TemporalFeastRecord.name`, and +the derived `SanctoralIdentity`/`ActiveOctave`/`Commemoration` types were plain English strings, +so every proper name necessarily fell back through `biFallback`. + +Added an optional `nameLa?: string` alongside `name` on `SanctoralIdentity` (`calendar/types.ts`), +`SaintRecord`/`SanctoralCandidate` (`calendar/feasts.ts`), `TemporalFeastRecord` +(`calendar/temporal-feasts.ts`), and `ActiveOctave` (`calendar/octaves.ts`), then threaded it +through every construction site that had been spreading `id`/`name`/`rank` (`collision.ts`, +`commemorations.ts`, `movable-feasts.ts`, `index.ts` x3, `vespers.ts` x2, +`octaves.ts`'s `considerCandidate`). `day-label.ts`'s old `biFallback(name)` calls became +`biName(name, nameLa)`, which still falls back to the English string when `nameLa` is absent — +the "surface real text, don't block on missing" convention preserved exactly, just now backed by +a real field instead of no field at all. + +Authored `nameLa` for all 11 named temporal feasts as the proof set (each sourced from the +reference engine's own `[Officium]`/`[Rank]` title, adapted from the source's genitive/ablative +liturgical-title style to this app's nominative-noun-phrase naming convention — e.g. "In +Ascensione Domini" → `Ascensio Domini`, "Sacratissimi Cordis Domini Nostri Jesu Christi" → +`Sacratissimum Cor Domini Nostri Jesu Christi`) plus St. Lawrence as the one saint, chosen +specifically to exercise the octave-headline (`octaveCoreName`) and octave-commemoration code +paths, not just the plain-winner path the temporal feasts alone would have covered. Live-verified +end-to-end via a throwaway scratch test (removed after confirming) that Pentecost's/the +Ascension's/the Immaculate Heart's own winner label, St. Lawrence's mid-octave and closing-day +octave label, and his octave *commemoration* line (Aug 12) all render the authored Latin, not the +English fallback. Six pre-existing `toEqual` test fixtures across `august-sanctoral.test.ts`, +`octaves.test.ts`, `transfer.test.ts`, and `december-sanctoral.test.ts` needed their expected +objects updated to include the new `nameLa` key (Christmas's own octave-commemoration entries too, +since `christmas-day.yml` picked up a name in this same pass). `npm test` (807 passed) and +`tsc --noEmit` pass. + +**Not started**: `nameLa` for the other ~220 saints — the mechanism is now proven in all three +code paths (plain winner, octave headline, sanctoral commemoration); what's left is the same +per-saint sourcing pass the sanctoral-content sweep itself used, just for a name instead of a +collect. diff --git a/src/calendar/collision.ts b/src/calendar/collision.ts index 3632ac1..f030cc0 100644 --- a/src/calendar/collision.ts +++ b/src/calendar/collision.ts @@ -19,11 +19,11 @@ export interface CollisionResult { } function asWinner(candidate: SanctoralIdentity): DayWinner { - return { kind: 'sanctoral', id: candidate.id, name: candidate.name, rank: candidate.rank }; + return { kind: 'sanctoral', id: candidate.id, name: candidate.name, nameLa: candidate.nameLa, rank: candidate.rank }; } function asCommemoration(candidate: SanctoralIdentity): Commemoration { - return { kind: 'sanctoral', id: candidate.id, name: candidate.name, rank: candidate.rank }; + return { kind: 'sanctoral', id: candidate.id, name: candidate.name, nameLa: candidate.nameLa, rank: candidate.rank }; } /** diff --git a/src/calendar/commemorations.ts b/src/calendar/commemorations.ts index 4a2745a..56a312f 100644 --- a/src/calendar/commemorations.ts +++ b/src/calendar/commemorations.ts @@ -83,11 +83,11 @@ export interface OccurrenceResult { } function sanctoralCommemoration(candidate: SanctoralIdentity): Commemoration { - return { kind: 'sanctoral', id: candidate.id, name: candidate.name, rank: candidate.rank }; + return { kind: 'sanctoral', id: candidate.id, name: candidate.name, nameLa: candidate.nameLa, rank: candidate.rank }; } function sanctoralWinner(candidate: SanctoralIdentity): DayWinner { - return { kind: 'sanctoral', id: candidate.id, name: candidate.name, rank: candidate.rank }; + return { kind: 'sanctoral', id: candidate.id, name: candidate.name, nameLa: candidate.nameLa, rank: candidate.rank }; } /** diff --git a/src/calendar/day-label.ts b/src/calendar/day-label.ts index 458c68b..b9c97ca 100644 --- a/src/calendar/day-label.ts +++ b/src/calendar/day-label.ts @@ -12,12 +12,14 @@ // // Bilingual: every label is built as a `Bi` (`{en, la}`) pair throughout. // Proper names (a saint's name, an octave's name, a named temporal feast's -// name) have no authored Latin form anywhere in `data/` today — those use -// the English string as their own `la` value too (same "surface real text -// rather than block on missing" convention used elsewhere in this app), -// rather than leaving Latin mode showing blanks. Only the closed, +// name) carry an optional `nameLa` field alongside the required English +// `name` — see calendar/types.ts's `SanctoralIdentity.nameLa` doc comment. +// Most saints still have no authored Latin name; `biName` falls back to +// the English string as the `la` value in that case (same "surface real +// text rather than block on missing" convention used elsewhere in this +// app), rather than leaving Latin mode showing blanks. The closed, // mechanism-level vocabulary here (weekdays, ranks, season/ordinal -// phrasing) has real Latin authored. +// phrasing) has always had real Latin authored, independent of this. import type { FeastClass, LiturgicalDay, TemporalCategory, Weekday } from './types'; import { easterSunday } from './easter'; import { adventStart, firstSundayStrictlyAfter, sundayOnOrBefore } from './temporal'; @@ -36,10 +38,11 @@ function bi(en: string, la: string): Bi { return { en, la }; } -/** A proper name with no authored Latin form yet — the English string - * stands in for `la` too. See file-header note. */ -function biFallback(name: string): Bi { - return { en: name, la: name }; +/** A proper name (saint, octave, named temporal feast) with an optional + * authored Latin form — falls back to the English string when `nameLa` + * hasn't been authored yet, same convention as `biFallback` above. */ +function biName(name: string, nameLa: string | undefined): Bi { + return { en: name, la: nameLa ?? name }; } function joinBi(items: Bi[], sep = ' — '): Bi { @@ -348,13 +351,13 @@ function temporalLabel(day: LiturgicalDay): Bi { * own winner is the feast itself, handled above before this is ever * called) — kept simple rather than special-cased for that rare edge case * (see applyOctaves's own `isOwnStartDay` comment in calendar/index.ts for - * when it can happen). The octave's own `name` has no authored Latin form - * (see file header), so both languages currently show the same name text - * — only the surrounding phrasing differs. */ + * when it can happen). Falls back to the English name for `la` too when + * the octave's own record has no authored `nameLa` yet (see file header). */ function octaveCoreName(octave: ActiveOctave): Bi { + const name = biName(octave.name, octave.nameLa); return octave.isClosingDay - ? bi(`Octave of ${octave.name}`, `In Octava ${octave.name}`) - : bi(`${ordinal(octave.dayNumber)} Day within the Octave of ${octave.name}`, `${ordinalLa(octave.dayNumber)} die infra Octavam ${octave.name}`); + ? bi(`Octave of ${name.en}`, `In Octava ${name.la}`) + : bi(`${ordinal(octave.dayNumber)} Day within the Octave of ${name.en}`, `${ordinalLa(octave.dayNumber)} die infra Octavam ${name.la}`); } function octaveLabel(octave: ActiveOctave): Bi { @@ -405,8 +408,8 @@ const CROSS_DAY_VESPERS_LABELS: Record<'today' | 'tomorrow', Bi> = { * - `sanctoral`: every commemorated saint, plain name, no rank (multiple * real ones can coexist — e.g. two colliding saints on the same date — * this app's own "generous commemorations" design keeps every one of - * them, not just the first). No authored Latin saint names exist yet - * (see file header), so `la` falls back to the same English name. + * them, not just the first). `la` uses the saint's own `nameLa` when + * authored, else falls back to the same English name (see file header). * - `temporal`: the day's own real Sunday/feria identity, if * `decideOccurrence` demoted it to a commemoration (`ordinary-sunday`/ * `privileged-feria`/`privileged-feria-minor`) — matched by id against @@ -438,7 +441,7 @@ function collectCommemorations( const octaves: Bi[] = []; for (const c of day.commemorations) { if (c.kind === 'sanctoral') { - const name = biFallback(c.name); + const name = biName(c.name, c.nameLa); sanctoral.push( c.vespersNote ? bi(`${name.en} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].en})`, `${name.la} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].la})`) @@ -450,7 +453,7 @@ function collectCommemorations( } } else if (opts.showOctaves && c.id !== opts.excludeOctaveId) { const octave = activeOctaves.find((a) => a.id === c.id); - octaves.push(octave ? octaveCommemorationLabel(octave) : biFallback(c.name)); + octaves.push(octave ? octaveCommemorationLabel(octave) : biName(c.name, c.nameLa)); } } return { sanctoral, temporal, octaves }; @@ -542,7 +545,7 @@ export function getDayLabel(day: LiturgicalDay): Partial> showOctaves: day.temporalCategory === 'ordinary-feria', }); const rank = formatRank(day.winner.rank); - const winnerName = biFallback(day.winner.name); + const winnerName = biName(day.winner.name, day.winner.nameLa); const winner = bi(`${winnerName.en} (${rank.en})`, `${winnerName.la} (${rank.la})`); return joinBi([winner, ...temporal, ...sanctoral, ...octaves]); } @@ -554,7 +557,7 @@ export function getDayLabel(day: LiturgicalDay): Partial> // temporal-feasts.ts) and fall through further down. const namedFeast = getTemporalFeastRecord(day.winner.id); if (namedFeast) { - const name = biFallback(namedFeast.name); + const name = biName(namedFeast.name, namedFeast.nameLa); const primary = namedFeast.rank ? bi(`${name.en} (${formatRank(namedFeast.rank).en})`, `${name.la} (${formatRank(namedFeast.rank).la})`) : name; diff --git a/src/calendar/feasts.ts b/src/calendar/feasts.ts index 0fbe593..6f611a6 100644 --- a/src/calendar/feasts.ts +++ b/src/calendar/feasts.ts @@ -15,6 +15,9 @@ import sanctoralCalendarData from '../data/calendar/sanctoral-calendar.yml'; export interface SaintRecord { id: string; name: string; + /** Latin form of `name`, when authored — see calendar/types.ts's + * `SanctoralIdentity.nameLa` doc comment for the display fallback. */ + nameLa?: string; rank: FeastClass; common: string; propers: string | null; @@ -138,6 +141,7 @@ for (const mod of Object.values(saintModules)) { export interface SanctoralCandidate { id: string; name: string; + nameLa?: string; rank: FeastClass; } @@ -157,6 +161,6 @@ export function getSanctoralCandidatesFor(isoDate: string): SanctoralCandidate[] if (!saint) { throw new Error(`sanctoral-calendar.yml references unknown saint id '${id}'`); } - return { id: saint.id, name: saint.name, rank: saint.rank }; + return { id: saint.id, name: saint.name, nameLa: saint.nameLa, rank: saint.rank }; }); } diff --git a/src/calendar/index.ts b/src/calendar/index.ts index ac6b3e4..058579e 100644 --- a/src/calendar/index.ts +++ b/src/calendar/index.ts @@ -237,7 +237,7 @@ function applyChristmasOctaveSunday( if (idx !== -1) { commemorations.splice(idx, 1); } - return { kind: 'sanctoral', id: displaced.id, name: displaced.name, rank: displaced.rank }; + return { kind: 'sanctoral', id: displaced.id, name: displaced.name, nameLa: displaced.nameLa, rank: displaced.rank }; } } @@ -290,7 +290,7 @@ function applyMarianSaturday( if (winner.rank !== 'simplex') { return winner; } - commemorations.push({ kind: 'sanctoral', id: winner.id, name: winner.name, rank: winner.rank }); + commemorations.push({ kind: 'sanctoral', id: winner.id, name: winner.name, nameLa: winner.nameLa, rank: winner.rank }); return MARIAN_SATURDAY; } @@ -343,7 +343,7 @@ function applyOctaves(isoDate: string, winner: DayWinner, commemorations: Commem if (cmp < 0 || tiedAgainstClosingDay) { const isOneOfTheseOctaves = octaves.some((o) => o.id === winner.id); if (!isOneOfTheseOctaves) { - commemorations.push({ kind: 'sanctoral', id: winner.id, name: winner.name, rank: winner.rank }); + commemorations.push({ kind: 'sanctoral', id: winner.id, name: winner.name, nameLa: winner.nameLa, rank: winner.rank }); resolvedWinner = { kind: 'temporal', id: resolveTemporalId(isoDate) }; } } @@ -356,7 +356,7 @@ function applyOctaves(isoDate: string, winner: DayWinner, commemorations: Commem // again alongside itself would be redundant. const isOwnStartDay = octave.dayNumber === 1 && resolvedWinner.kind === 'temporal'; if (!isSelf && !isOwnStartDay) { - commemorations.push({ kind: 'octave', id: octave.id, name: octave.name }); + commemorations.push({ kind: 'octave', id: octave.id, name: octave.name, nameLa: octave.nameLa }); } } diff --git a/src/calendar/movable-feasts.ts b/src/calendar/movable-feasts.ts index 96308df..4710471 100644 --- a/src/calendar/movable-feasts.ts +++ b/src/calendar/movable-feasts.ts @@ -83,7 +83,7 @@ export function applyMovableFeasts(isoDate: string, winner: DayWinner, commemora if (resolvedWinner.kind === 'temporal') { commemorations.push({ kind: 'temporal', id: resolvedWinner.id }); } else { - commemorations.push({ kind: 'sanctoral', id: resolvedWinner.id, name: resolvedWinner.name, rank: resolvedWinner.rank }); + commemorations.push({ kind: 'sanctoral', id: resolvedWinner.id, name: resolvedWinner.name, nameLa: resolvedWinner.nameLa, rank: resolvedWinner.rank }); } resolvedWinner = FEAST_WINNER; continue; @@ -95,7 +95,7 @@ export function applyMovableFeasts(isoDate: string, winner: DayWinner, commemora } else if (compareFeastClass(resolvedWinner.rank, rank) >= 0) { commemorations.push({ kind: 'temporal', id: feast.id }); } else { - commemorations.push({ kind: 'sanctoral', id: resolvedWinner.id, name: resolvedWinner.name, rank: resolvedWinner.rank }); + commemorations.push({ kind: 'sanctoral', id: resolvedWinner.id, name: resolvedWinner.name, nameLa: resolvedWinner.nameLa, rank: resolvedWinner.rank }); resolvedWinner = FEAST_WINNER; } } diff --git a/src/calendar/octaves.ts b/src/calendar/octaves.ts index 133360b..80ad38b 100644 --- a/src/calendar/octaves.ts +++ b/src/calendar/octaves.ts @@ -17,6 +17,9 @@ import { compareFeastClass, minimumOutrightWinningRank } from './commemorations' export interface ActiveOctave { id: string; name: string; + /** Latin form of `name`, when authored — see calendar/types.ts's + * `SanctoralIdentity.nameLa` doc comment for the display fallback. */ + nameLa?: string; /** This octave's own effective rank *today* — the ordinary `wins` * threshold on every day except its own closing day, where it's * `closingDayRank` instead (see calendar/types.ts's OctaveConfig). @@ -49,6 +52,7 @@ function considerCandidate( startDate: string, id: string, name: string, + nameLa: string | undefined, octave: OctaveConfig | undefined, ): void { if (!octave?.enabled || seen.has(id)) { @@ -63,7 +67,7 @@ function considerCandidate( const dayNumber = offset + 1; const isClosingDay = dayNumber === days; const wins = isClosingDay ? (octave.closingDayRank ?? DEFAULT_CLOSING_DAY_RANK) : (octave.wins ?? DEFAULT_WINS); - active.push({ id, name, wins, dayNumber, isClosingDay }); + active.push({ id, name, nameLa, wins, dayNumber, isClosingDay }); } /** Every octave (sanctoral or temporal) whose window covers `isoDate`, @@ -80,14 +84,14 @@ export function activeOctavesFor(isoDate: string): ActiveOctave[] { for (const candidate of getSanctoralCandidatesFor(candidateDate)) { const record = getSaintRecord(candidate.id); if (record) { - considerCandidate(active, seen, isoDate, candidateDate, record.id, record.name, record.octave); + considerCandidate(active, seen, isoDate, candidateDate, record.id, record.name, record.nameLa, record.octave); } } for (const feastId of temporalFeastIdsStartingOn(candidateDate)) { const record = getTemporalFeastRecord(feastId); if (record) { - considerCandidate(active, seen, isoDate, candidateDate, record.id, record.name, record.octave); + considerCandidate(active, seen, isoDate, candidateDate, record.id, record.name, record.nameLa, record.octave); } } } diff --git a/src/calendar/temporal-feasts.ts b/src/calendar/temporal-feasts.ts index 11ce5cf..155bfb5 100644 --- a/src/calendar/temporal-feasts.ts +++ b/src/calendar/temporal-feasts.ts @@ -30,6 +30,9 @@ export type MovableAnchor = export interface TemporalFeastRecord { id: string; name: string; + /** Latin form of `name`, when authored — see calendar/types.ts's + * `SanctoralIdentity.nameLa` doc comment for the display fallback. */ + nameLa?: string; octave?: OctaveConfig; rank?: FeastClass; anchor?: MovableAnchor; diff --git a/src/calendar/types.ts b/src/calendar/types.ts index d5b948e..6156c31 100644 --- a/src/calendar/types.ts +++ b/src/calendar/types.ts @@ -103,6 +103,11 @@ export type TemporalCategory = export interface SanctoralIdentity { id: string; name: string; + /** Latin form of `name`, when authored (see `data/calendar/saints/*.yml`'s + * own `nameLa` field). Absent for the majority of saints still — display + * code falls back to `name` itself when this is missing, same "surface + * real text rather than block on missing" convention used elsewhere. */ + nameLa?: string; rank: FeastClass; } @@ -180,7 +185,7 @@ export type Commemoration = // carries no such qualifier. vespersNote?: 'today' | 'tomorrow'; } & SanctoralIdentity) - | { kind: 'octave'; id: string; name: string }; + | { kind: 'octave'; id: string; name: string; nameLa?: string }; export interface LiturgicalDay { /** ISO date, e.g. "2026-08-09" */ diff --git a/src/calendar/vespers.ts b/src/calendar/vespers.ts index cfe5d10..fe5f261 100644 --- a/src/calendar/vespers.ts +++ b/src/calendar/vespers.ts @@ -85,7 +85,7 @@ function commemorationOfDisplacedPredecessor(loser: DayWinner): Commemoration | if (loser.kind !== 'sanctoral' || loser.rank === 'simplex') { return undefined; } - return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank, vespersNote: 'today' }; + return { kind: 'sanctoral', id: loser.id, name: loser.name, nameLa: loser.nameLa, rank: loser.rank, vespersNote: 'today' }; } /** @@ -120,7 +120,7 @@ function commemorationOfDisplacedSuccessor(winner: DayWinner, loser: DayWinner): if (forbids) { return undefined; } - return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank, vespersNote: 'tomorrow' }; + return { kind: 'sanctoral', id: loser.id, name: loser.name, nameLa: loser.nameLa, rank: loser.rank, vespersNote: 'tomorrow' }; } function tagVespersFrom(tomorrow: LiturgicalDay): LiturgicalDay { diff --git a/src/data/calendar/saints/st-lawrence.yml b/src/data/calendar/saints/st-lawrence.yml index 6a49d4a..bebfd41 100644 --- a/src/data/calendar/saints/st-lawrence.yml +++ b/src/data/calendar/saints/st-lawrence.yml @@ -6,6 +6,10 @@ # antiphon. id: st-lawrence name: "St. Lawrence, Martyr" +# Standard Latin form, matching this app's own "St. X, Rank" naming +# convention (the source's own martyrology/kalendar entries for him use +# the genitive "S. Laurentii Martyris", dependent on an implied "Festum"). +nameLa: "Sanctus Laurentius, Martyr" rank: duplex-2-classis common: common-of-a-martyr propers: "st-lawrence" diff --git a/src/data/calendar/temporal-feasts/ascension.yml b/src/data/calendar/temporal-feasts/ascension.yml index 50b67a1..6d082d7 100644 --- a/src/data/calendar/temporal-feasts/ascension.yml +++ b/src/data/calendar/temporal-feasts/ascension.yml @@ -15,6 +15,11 @@ # — the `ordinary-feria` category was never actually load-bearing here. id: ascension name: "The Ascension of Our Lord" +# Nominative form of the reference engine's own title ("In Ascensione +# Domini", Pasc5-4.txt's [Officium]/[Rank] header), matching this app's +# own nominative-noun-phrase naming convention rather than the source's +# prepositional-phrase style. +nameLa: "Ascensio Domini" rank: duplex-1-classis anchor: kind: easter diff --git a/src/data/calendar/temporal-feasts/christ-the-king.yml b/src/data/calendar/temporal-feasts/christ-the-king.yml index ecb8329..28a1a55 100644 --- a/src/data/calendar/temporal-feasts/christ-the-king.yml +++ b/src/data/calendar/temporal-feasts/christ-the-king.yml @@ -8,6 +8,11 @@ # movable feast. See calendar/movable-feasts.ts for the occurrence rule. id: christ-the-king name: "Christ the King" +# The feast's own traditional short Latin title (Pius XI's "Quas Primas"), +# matching this app's nominative-noun-phrase naming convention — the +# reference engine's own [Rank] title is genitive ("Domini Nostri Jesu +# Christi Regis", Sancti/10-DU.txt), dependent on an implied "Festum". +nameLa: "Christus Rex" rank: duplex-1-classis anchor: kind: nth-sunday-of-month diff --git a/src/data/calendar/temporal-feasts/christmas-day.yml b/src/data/calendar/temporal-feasts/christmas-day.yml index 378e293..fb29960 100644 --- a/src/data/calendar/temporal-feasts/christmas-day.yml +++ b/src/data/calendar/temporal-feasts/christmas-day.yml @@ -12,6 +12,10 @@ # specifically. id: christmas-day name: "Christmas" +# Nominative form of the reference engine's own title ("In Nativitate +# Domini", Sancti/12-25.txt's [Rank]), matching this app's own +# nominative-noun-phrase naming convention. +nameLa: "Nativitas Domini" rank: duplex-1-classis octave: enabled: true diff --git a/src/data/calendar/temporal-feasts/circumcision.yml b/src/data/calendar/temporal-feasts/circumcision.yml index 3db8442..afa2c43 100644 --- a/src/data/calendar/temporal-feasts/circumcision.yml +++ b/src/data/calendar/temporal-feasts/circumcision.yml @@ -20,6 +20,10 @@ # not winner resolution at all. id: circumcision name: "The Circumcision of Our Lord" +# Nominative form of the reference engine's own title ("In Circumcisione +# Domini", Sancti/01-01.txt's [Rank]), matching this app's own +# nominative-noun-phrase naming convention. +nameLa: "Circumcisio Domini" rank: duplex-2-classis anchor: kind: fixed-date diff --git a/src/data/calendar/temporal-feasts/corpus-christi.yml b/src/data/calendar/temporal-feasts/corpus-christi.yml index a3a7f33..0e297b7 100644 --- a/src/data/calendar/temporal-feasts/corpus-christi.yml +++ b/src/data/calendar/temporal-feasts/corpus-christi.yml @@ -14,6 +14,11 @@ # day's real winner. id: corpus-christi name: "The Most Holy Body of Christ" +# Common short Latin title, well attested in the reference engine's own +# text ("Festum Sanctissimi Corporis Christi", Tempora/Pent01-4.txt's +# [Officium]) — already a nominative noun phrase on its own, no +# adaptation needed. +nameLa: "Corpus Christi" rank: duplex-1-classis anchor: kind: easter diff --git a/src/data/calendar/temporal-feasts/holy-name-of-jesus.yml b/src/data/calendar/temporal-feasts/holy-name-of-jesus.yml index d437016..616ba55 100644 --- a/src/data/calendar/temporal-feasts/holy-name-of-jesus.yml +++ b/src/data/calendar/temporal-feasts/holy-name-of-jesus.yml @@ -16,6 +16,10 @@ # the day. id: holy-name-of-jesus name: "The Most Holy Name of Jesus" +# Nominative form of the reference engine's own title ("Sanctissimi +# Nominis Jesu", Sancti/01-00.txt's [Rank]), matching this app's own +# nominative-noun-phrase naming convention. +nameLa: "Sanctissimum Nomen Jesu" rank: duplex-2-classis anchor: kind: fixed-date diff --git a/src/data/calendar/temporal-feasts/immaculate-heart-of-mary.yml b/src/data/calendar/temporal-feasts/immaculate-heart-of-mary.yml index 570f2f7..dde1dbb 100644 --- a/src/data/calendar/temporal-feasts/immaculate-heart-of-mary.yml +++ b/src/data/calendar/temporal-feasts/immaculate-heart-of-mary.yml @@ -11,6 +11,10 @@ # own content. id: immaculate-heart-of-mary name: "The Immaculate Heart of the Blessed Virgin Mary" +# Nominative form of the reference engine's own title ("Immaculati Cordis +# Beatæ Mariæ Virginis", Sancti/08-22.txt's [Rank]), matching this app's +# own nominative-noun-phrase naming convention. +nameLa: "Immaculatum Cor Beatæ Mariæ Virginis" rank: duplex-2-classis anchor: kind: easter diff --git a/src/data/calendar/temporal-feasts/marian-saturday.yml b/src/data/calendar/temporal-feasts/marian-saturday.yml index 452e161..a256c67 100644 --- a/src/data/calendar/temporal-feasts/marian-saturday.yml +++ b/src/data/calendar/temporal-feasts/marian-saturday.yml @@ -5,4 +5,9 @@ # for how a day actually becomes this. id: marian-saturday name: "Our Lady's Saturday" +# Adapted to a nominative noun phrase from the reference engine's own +# title ("Sanctæ Mariæ Sabbato", Commune/C10.txt's [Officium] — an +# ablative time-when construction, "on the Saturday of Holy Mary"), +# matching this app's own nominative naming convention. +nameLa: "Sabbatum Sanctæ Mariæ" rank: simplex diff --git a/src/data/calendar/temporal-feasts/patronage-of-st-joseph.yml b/src/data/calendar/temporal-feasts/patronage-of-st-joseph.yml index 9471c69..1c15126 100644 --- a/src/data/calendar/temporal-feasts/patronage-of-st-joseph.yml +++ b/src/data/calendar/temporal-feasts/patronage-of-st-joseph.yml @@ -21,6 +21,11 @@ # (PascN-D = Easter + 7N + D, e.g. Ascension = Pasc5-4 = Easter+39). id: patronage-of-st-joseph name: "The Patronage of St. Joseph" +# Nominative form of the reference engine's own title ("In Octava +# Patrocinii S. Joseph", Tempora/Pasc3-3.txt's [Officium], octave-day +# form of the feast's own genitive "Patrocinii S. Joseph"), matching this +# app's own nominative-noun-phrase naming convention. +nameLa: "Patrocinium Sancti Joseph" rank: duplex-1-classis anchor: kind: easter diff --git a/src/data/calendar/temporal-feasts/pentecost-sunday.yml b/src/data/calendar/temporal-feasts/pentecost-sunday.yml index 62dc34c..b1966ee 100644 --- a/src/data/calendar/temporal-feasts/pentecost-sunday.yml +++ b/src/data/calendar/temporal-feasts/pentecost-sunday.yml @@ -10,6 +10,9 @@ # before it gets the chance. id: pentecost-sunday name: "Pentecost" +# Reference engine's own [Officium] title, Tempora/Pent00-0.txt — already +# a nominative noun phrase, no adaptation needed. +nameLa: "Dominica Pentecostes" rank: duplex-1-classis octave: enabled: true diff --git a/src/data/calendar/temporal-feasts/sacred-heart.yml b/src/data/calendar/temporal-feasts/sacred-heart.yml index 41711dc..e6f122d 100644 --- a/src/data/calendar/temporal-feasts/sacred-heart.yml +++ b/src/data/calendar/temporal-feasts/sacred-heart.yml @@ -10,6 +10,10 @@ # temporal-feasts.ts's own doc comment) a record to live on. id: sacred-heart name: "The Most Sacred Heart of Our Lord Jesus Christ" +# Nominative form of the reference engine's own title ("Sacratissimi +# Cordis Domini Nostri Jesu Christi", Tempora/Pent02-5.txt's [Officium]), +# matching this app's own nominative-noun-phrase naming convention. +nameLa: "Sacratissimum Cor Domini Nostri Jesu Christi" rank: duplex-1-classis anchor: kind: easter diff --git a/tests/calendar/august-sanctoral.test.ts b/tests/calendar/august-sanctoral.test.ts index 24452e7..6fc517e 100644 --- a/tests/calendar/august-sanctoral.test.ts +++ b/tests/calendar/august-sanctoral.test.ts @@ -71,6 +71,7 @@ describe('August sanctoral pull (first pass)', () => { kind: 'sanctoral', id: 'st-lawrence', name: 'St. Lawrence, Martyr', + nameLa: 'Sanctus Laurentius, Martyr', rank: 'duplex-2-classis', }); expect(getDayCollect(day).status.en).toBe('verified'); @@ -86,7 +87,7 @@ describe('August sanctoral pull (first pass)', () => { expect(clareDay.winner.kind).toBe('temporal'); expect(clareDay.commemorations).toEqual([ { kind: 'sanctoral', id: 'st-clare', name: 'St. Clare, Virgin', rank: 'simplex' }, - { kind: 'octave', id: 'st-lawrence', name: 'St. Lawrence, Martyr' }, + { kind: 'octave', id: 'st-lawrence', name: 'St. Lawrence, Martyr', nameLa: 'Sanctus Laurentius, Martyr' }, ]); // Aug 22 is the Assumption's own octave day (In Octava Assumptionis), // Duplex, blocking Ss. Timothy/Hippolytus/Symphorian the same way -- diff --git a/tests/calendar/december-sanctoral.test.ts b/tests/calendar/december-sanctoral.test.ts index 89fa9f0..fb9dc3a 100644 --- a/tests/calendar/december-sanctoral.test.ts +++ b/tests/calendar/december-sanctoral.test.ts @@ -84,7 +84,7 @@ describe('December sanctoral pull (first pass, 12/12)', () => { // dedicated stacking test below for the full week's build-up. expect(stephen.commemorations).toEqual([ { kind: 'temporal', id: expect.any(String) }, - { kind: 'octave', id: 'christmas-day', name: 'Christmas' }, + { kind: 'octave', id: 'christmas-day', name: 'Christmas', nameLa: 'Nativitas Domini' }, ]); }); @@ -101,7 +101,7 @@ describe('December sanctoral pull (first pass, 12/12)', () => { // all stacked up and commemorate alongside him. expect(day.commemorations).toEqual([ { kind: 'temporal', id: expect.any(String) }, - { kind: 'octave', id: 'christmas-day', name: 'Christmas' }, + { kind: 'octave', id: 'christmas-day', name: 'Christmas', nameLa: 'Nativitas Domini' }, { kind: 'octave', id: 'st-stephen-protomartyr', name: 'St. Stephen, Protomartyr' }, { kind: 'octave', id: 'st-john-apostle', name: 'St. John, Apostle and Evangelist' }, { kind: 'octave', id: 'holy-innocents', name: 'The Holy Innocents, Martyrs' }, @@ -155,7 +155,7 @@ describe('December sanctoral pull (first pass, 12/12)', () => { const dec30 = resolveDay('2033-12-30'); // no saint of its own -- a plain Octave-of-the-Nativity day expect(dec30.winner.kind).toBe('temporal'); expect(dec30.commemorations).toEqual([ - { kind: 'octave', id: 'christmas-day', name: 'Christmas' }, + { kind: 'octave', id: 'christmas-day', name: 'Christmas', nameLa: 'Nativitas Domini' }, { kind: 'octave', id: 'st-stephen-protomartyr', name: 'St. Stephen, Protomartyr' }, { kind: 'octave', id: 'st-john-apostle', name: 'St. John, Apostle and Evangelist' }, { kind: 'octave', id: 'holy-innocents', name: 'The Holy Innocents, Martyrs' }, diff --git a/tests/calendar/octaves.test.ts b/tests/calendar/octaves.test.ts index 495ddf6..cf1871d 100644 --- a/tests/calendar/octaves.test.ts +++ b/tests/calendar/octaves.test.ts @@ -10,7 +10,14 @@ describe('activeOctavesFor', () => { it("finds a saint's own octave on its own day (day 1) and through day 8, not on day 9", () => { const day1 = activeOctavesFor('2026-08-10'); // St. Lawrence's own day expect(day1).toEqual([ - { id: 'st-lawrence', name: 'St. Lawrence, Martyr', wins: 'semiduplex', dayNumber: 1, isClosingDay: false }, + { + id: 'st-lawrence', + name: 'St. Lawrence, Martyr', + nameLa: 'Sanctus Laurentius, Martyr', + wins: 'semiduplex', + dayNumber: 1, + isClosingDay: false, + }, ]); // Aug 17 is also day 3 of the Assumption's own octave (started Aug 15) -- @@ -19,7 +26,14 @@ describe('activeOctavesFor', () => { // pattern, not a per-saint quirk (see calendar/types.ts's OctaveConfig). const day8 = activeOctavesFor('2026-08-17'); expect(day8).toEqual([ - { id: 'st-lawrence', name: 'St. Lawrence, Martyr', wins: 'duplex', dayNumber: 8, isClosingDay: true }, + { + id: 'st-lawrence', + name: 'St. Lawrence, Martyr', + nameLa: 'Sanctus Laurentius, Martyr', + wins: 'duplex', + dayNumber: 8, + isClosingDay: true, + }, { id: 'assumption', name: 'The Assumption of the Blessed Virgin Mary', @@ -37,7 +51,14 @@ describe('activeOctavesFor', () => { it("Pentecost's octave uses its own configured threshold (duplex), not the default (semiduplex)", () => { const octaves = activeOctavesFor('2026-05-29'); // within Pentecost's octave, 2026 expect(octaves).toEqual([ - { id: 'pentecost-sunday', name: 'Pentecost', wins: 'duplex', dayNumber: 6, isClosingDay: false }, + { + id: 'pentecost-sunday', + name: 'Pentecost', + nameLa: 'Dominica Pentecostes', + wins: 'duplex', + dayNumber: 6, + isClosingDay: false, + }, ]); expect(strictestThreshold(octaves)).toBe('duplex'); }); diff --git a/tests/calendar/transfer.test.ts b/tests/calendar/transfer.test.ts index 86d7683..2575455 100644 --- a/tests/calendar/transfer.test.ts +++ b/tests/calendar/transfer.test.ts @@ -46,6 +46,7 @@ describe('transfer mechanism (resolveDay integration)', () => { kind: 'sanctoral', id: 'st-lawrence', name: 'St. Lawrence, Martyr', + nameLa: 'Sanctus Laurentius, Martyr', rank: 'duplex-2-classis', }); }); @@ -74,7 +75,7 @@ describe('transfer mechanism (resolveDay integration)', () => { expect(saturday.winner).toEqual({ kind: 'temporal', id: 'pentecost-sunday' }); expect(saturday.commemorations).toEqual([ { kind: 'sanctoral', id: 'st-felix-i', name: 'St. Felix I, Pope and Martyr', rank: 'simplex' }, - { kind: 'octave', id: 'pentecost-sunday', name: 'Pentecost' }, + { kind: 'octave', id: 'pentecost-sunday', name: 'Pentecost', nameLa: 'Dominica Pentecostes' }, ]); // Felix doesn't reach Trinity Sunday at all now. Trinity Sunday 2026 @@ -93,7 +94,7 @@ describe('transfer mechanism (resolveDay integration)', () => { }); expect(sunday.commemorations).toEqual([ { kind: 'temporal', id: 'post-pentecost-01' }, - { kind: 'octave', id: 'pentecost-sunday', name: 'Pentecost' }, + { kind: 'octave', id: 'pentecost-sunday', name: 'Pentecost', nameLa: 'Dominica Pentecostes' }, ]); }); });