diff --git a/src/hours/types.ts b/src/hours/types.ts index 1db05f2..d345d15 100644 --- a/src/hours/types.ts +++ b/src/hours/types.ts @@ -232,7 +232,13 @@ export type ResolvedPart = // groups a reading under its 'nocturn-psalmody' sibling for the UI (1-3, // omitted on a ferial 1-nocturn day); `source` is a human-readable // attribution ("St. John Chrysostom, Homily 3 on Matthew"; the user's - // own bible-reading plan doesn't carry one). `isGospel` flags a reading + // own bible-reading plan doesn't carry one). `label` is usually that same + // plain string (nocturn-readings' own `source`, already written once in + // English and reused as-is in both language columns), but octave + // readings' own `source` is a genuine bilingual attribution (their + // English title is a real translation of a Latin incipit, not an + // editorial description written once) so `label` accepts either shape — + // see hour-view.ts's renderLessonLabel. `isGospel` flags a reading // as a Gospel pericope (from either the user's own continuous-reading // plan or the day's own proper Gospel+homily — never a Common-of-Saints // fallback Gospel, which this app deliberately excludes for Matins, see @@ -245,7 +251,7 @@ export type ResolvedPart = | { kind: 'lesson'; text: ResolvedText; - label?: string; + label?: string | Partial>; nocturn?: number; source?: string; isGospel?: boolean; diff --git a/src/propers/octave-readings.ts b/src/propers/octave-readings.ts index 08d0a5f..c5f358c 100644 --- a/src/propers/octave-readings.ts +++ b/src/propers/octave-readings.ts @@ -21,9 +21,11 @@ export interface ScriptureCitation { export interface OctaveReadingText { id: string; /** Attribution for the reading — author and work, e.g. "St. John - * Damascene, 2nd Sermon on the Dormition of the Mother of God". Not - * itself translated/localized. */ - source: string; + * Damascene, 2nd Sermon on the Dormition of the Mother of God". Bilingual + * like everything else here since it's rendered as the reading's own + * heading in both language columns (2026-08-21 fix: it used to be a bare + * Latin incipit shown verbatim in the English column too). */ + source: Partial>; text: Partial>; /** The first responsory following the reading in the source, where one * was found — which one to use when several lessons (and several @@ -51,7 +53,7 @@ export interface OctaveReadingText { */ interface OctaveReadingRecord { id: string; - source: string; + source: Partial>; text?: Partial>; passages?: ScriptureCitation[]; responsory?: Partial>; diff --git a/src/ui/hour-view.ts b/src/ui/hour-view.ts index 656e99b..bcc0ae1 100644 --- a/src/ui/hour-view.ts +++ b/src/ui/hour-view.ts @@ -34,6 +34,27 @@ function renderCitation(text: ResolvedText): string { return values.length ? `

${escapeHtml(values.join(' / '))}

` : ''; } +// A lesson's `label` is either a plain string (an editorial attribution +// written once and reused in both language columns) or a per-language +// object (a genuine translation, e.g. an octave reading's Latin incipit +// vs. its English title) — render the latter as its own lang-columns row +// matching the body text underneath it. +function renderLessonLabel( + label: string | Partial> | undefined, + languages: readonly string[], +): string { + if (!label) { + return '

Reading

'; + } + if (typeof label === 'string') { + return `

${escapeHtml(label)}

`; + } + const cols = languages + .map((lang) => `${escapeHtml(label[lang] ?? '')}`) + .join(''); + return `

${cols}

`; +} + function renderPart(part: ResolvedPart, languages: readonly string[]): string { switch (part.kind) { case 'hymn': @@ -70,7 +91,7 @@ function renderPart(part: ResolvedPart, languages: readonly string[]): string { case 'lesson': return `
-

${part.isGospel ? 'Gospel' : part.label ? escapeHtml(part.label) : 'Reading'}

+ ${part.isGospel ? '

Gospel

' : renderLessonLabel(part.label, languages)} ${renderCitation(part.text)} ${renderColumns(part.text, languages)} ${part.responsory ? `
${renderColumns(part.responsory, languages)}
` : ''} diff --git a/tests/hours/prime.test.ts b/tests/hours/prime.test.ts index 7430b83..f7e83b9 100644 --- a/tests/hours/prime.test.ts +++ b/tests/hours/prime.test.ts @@ -90,7 +90,9 @@ describe('resolveOrdo("prime", ...)', () => { it('resolves a Regula reading for the day, via the canonical-date table', () => { const ordo = resolveOrdo('prime', '2026-08-25'); // 08-25 maps to canonical 04-25 (Rule ch. 67) in data/regula/table.yml. - const rule = ordo.parts.find((p) => p.kind === 'lesson' && p.label?.includes('67')); + const rule = ordo.parts.find( + (p) => p.kind === 'lesson' && typeof p.label === 'string' && p.label.includes('67'), + ); expect(rule).toBeDefined(); }); diff --git a/tests/propers/octave-readings.test.ts b/tests/propers/octave-readings.test.ts index 85af280..c09356c 100644 --- a/tests/propers/octave-readings.test.ts +++ b/tests/propers/octave-readings.test.ts @@ -4,7 +4,8 @@ import { getOctaveReading } from '../../src/propers'; describe('getOctaveReading', () => { it('resolves a real reading for a day that has one, collapsed into one continuous text', () => { const reading = getOctaveReading('st-lawrence', 2); - expect(reading?.source).toBe('Sermo sancti Augustíni Epíscopi.'); + expect(reading?.source.la).toBe('Sermo sancti Augustíni Epíscopi.'); + expect(reading?.source.en).toBe('Sermon of St. Augustine, Bishop'); expect(reading?.text.la).toContain('Beatissimi Laurentii Martyris'); expect(reading?.text.en).toContain('most blessed Martyr, Lawrence'); expect(reading?.status.la).toBe('verified');