diff --git a/package.json b/package.json index 818fe306..bb5588e8 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "leaflet": "^1.9.4", "mongoose": "^9.4.1", "node-cron": "^4.2.1", - "romcal": "3.0.0-dev.125", + "romcal": "github:AlexBocken/romcal1962#e4731a8", "sharp": "^0.34.5", "web-haptics": "^0.0.6" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72ca79fe..cb21c4f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 2.1.8-no-fsevents.3 '@romcal/calendar.general-roman': specifier: 3.0.0-dev.125 - version: 3.0.0-dev.125(romcal@3.0.0-dev.125(typescript@6.0.2)) + version: 3.0.0-dev.125(romcal@https://codeload.github.com/AlexBocken/romcal1962/tar.gz/e4731a8(typescript@6.0.2)) '@sveltejs/adapter-node': specifier: ^5.5.4 version: 5.5.4(@sveltejs/kit@2.56.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@types/node@22.18.0)(esbuild@0.27.3)(terser@5.46.0)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@types/node@22.18.0)(esbuild@0.27.3)(terser@5.46.0))) @@ -54,8 +54,8 @@ importers: specifier: ^4.2.1 version: 4.2.1 romcal: - specifier: 3.0.0-dev.125 - version: 3.0.0-dev.125(typescript@6.0.2) + specifier: github:AlexBocken/romcal1962#e4731a8 + version: https://codeload.github.com/AlexBocken/romcal1962/tar.gz/e4731a8(typescript@6.0.2) sharp: specifier: ^0.34.5 version: 0.34.5 @@ -897,6 +897,7 @@ packages: '@romcal/calendar.general-roman@3.0.0-dev.125': resolution: {integrity: sha512-6E4D9zfGqkz7NlJTv38v8++tZv234F1Z2jXlyzOithy92ZzZJhOMgUcmw0MOLUbn24lqz3477/VlFdLvRtzQBA==} + version: 3.0.0-dev.125 engines: {node: '>=18.0.0'} peerDependencies: romcal: 3.0.0-dev.125 @@ -1420,8 +1421,8 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - i18next@25.10.10: - resolution: {integrity: sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==} + i18next@26.0.4: + resolution: {integrity: sha512-gXF7U9bfioXPLv7mw8Qt2nfO7vij5MyINvPgVv99pX3fL1Y01pw2mKBFrlYpRxRCl2wz3ISenj6VsMJT2isfuA==} peerDependencies: typescript: ^5 || ^6 peerDependenciesMeta: @@ -1781,8 +1782,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - romcal@3.0.0-dev.125: - resolution: {integrity: sha512-mMNojZW+6asQ3XCxsFZaiGIsnXoX+MEmnCmf7OLMT3CZ6/ltMZUQOCNB79qZYcNVLAjbNA+YFW9zU9OjumD6vw==} + romcal@https://codeload.github.com/AlexBocken/romcal1962/tar.gz/e4731a8: + resolution: {tarball: https://codeload.github.com/AlexBocken/romcal1962/tar.gz/e4731a8} + version: 3.0.0-dev.125 engines: {node: '>=18.0.0'} sade@1.8.1: @@ -2681,9 +2683,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.1': optional: true - '@romcal/calendar.general-roman@3.0.0-dev.125(romcal@3.0.0-dev.125(typescript@6.0.2))': + '@romcal/calendar.general-roman@3.0.0-dev.125(romcal@https://codeload.github.com/AlexBocken/romcal1962/tar.gz/e4731a8(typescript@6.0.2))': dependencies: - romcal: 3.0.0-dev.125(typescript@6.0.2) + romcal: https://codeload.github.com/AlexBocken/romcal1962/tar.gz/e4731a8(typescript@6.0.2) '@sec-ant/readable-stream@0.4.1': {} @@ -3179,7 +3181,7 @@ snapshots: transitivePeerDependencies: - supports-color - i18next@25.10.10(typescript@6.0.2): + i18next@26.0.4(typescript@6.0.2): dependencies: '@babel/runtime': 7.29.2 optionalDependencies: @@ -3543,9 +3545,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 - romcal@3.0.0-dev.125(typescript@6.0.2): + romcal@https://codeload.github.com/AlexBocken/romcal1962/tar.gz/e4731a8(typescript@6.0.2): dependencies: - i18next: 25.10.10(typescript@6.0.2) + i18next: 26.0.4(typescript@6.0.2) transitivePeerDependencies: - typescript diff --git a/src/params/calendarRite.ts b/src/params/calendarRite.ts new file mode 100644 index 00000000..f0772970 --- /dev/null +++ b/src/params/calendarRite.ts @@ -0,0 +1,5 @@ +import type { ParamMatcher } from '@sveltejs/kit'; + +export const match: ParamMatcher = (param) => { + return param === '1962' || param === '1969'; +}; diff --git a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[[year=calendarRite]]/+page.server.ts b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[[year=calendarRite]]/+page.server.ts index 5cff3164..1d0f70cd 100644 --- a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[[year=calendarRite]]/+page.server.ts +++ b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[[year=calendarRite]]/+page.server.ts @@ -6,7 +6,16 @@ import { GeneralRoman_En, GeneralRoman_La } from '@romcal/calendar.general-roman'; -import { expectedSlug, isValidRite, type CalendarLang, type Rite } from './calendarI18n'; +import { Romcal1962 } from 'romcal/1962'; +import type { Celebration1962, ResolvedDay1962 } from 'romcal/1962'; +import { + colorLabel1962, + expectedSlug, + rank1962Label, + season1962Label, + type CalendarLang, + type Rite +} from '../calendarI18n'; export interface CalendarDay { iso: string; @@ -19,6 +28,41 @@ export interface CalendarDay { colorKeys: string[]; psalterWeek: string | null; sundayCycle: string | null; + rite1962?: Rite1962Detail; +} + +export interface Rite1962Commem { + key: string; + name: string; + rankName: string; + kind: 'tempora' | 'sancti'; + colorNames: string[]; + colorKeys: string[]; +} + +export interface Rite1962Detail { + class: 1 | 2 | 3 | 4; + kind: 'tempora' | 'sancti'; + commemorations: Rite1962Commem[]; + rubrics: { + gloria: boolean; + credo: boolean; + preface?: string; + lastGospel?: string; + ite?: string; + }; + octave?: { + id: string; + parentFeastId: string; + day: number; + rank: string; + }; + vigilOf?: string; + transferredFrom?: string; + properSource: string; + communeSlug?: string; + propers: ProperSection[]; + extraSections: ProperSection[]; } const localeBundles = { @@ -68,6 +112,178 @@ async function getYear(lang: CalendarLang, year: number): Promise(); +function getRomcal1962(lang: CalendarLang): Romcal1962 { + let r = romcal1962ByLang.get(lang); + if (r) return r; + const locales = lang === 'la' ? ['la'] : ['la', lang]; + r = new Romcal1962({ includePropers: true, propersLocales: locales }); + romcal1962ByLang.set(lang, r); + return r; +} + +const PROPER_ORDER = [ + 'introit', + 'collect', + 'epistle', + 'gradual', + 'alleluia', + 'tract', + 'sequence', + 'gospel', + 'offertory', + 'secret', + 'preface', + 'communion', + 'postcommunion' +] as const; + +type ProperKey = (typeof PROPER_ORDER)[number]; + +export interface ProperSection { + key: string; + la: string; + local?: string; +} + +const COLOR_KEY_1962: Record = { + White: 'WHITE', + Red: 'RED', + Green: 'GREEN', + Violet: 'PURPLE', + Black: 'BLACK', + Rose: 'ROSE' +}; + +const RANK_FROM_CLASS_1962: Record<1 | 2 | 3 | 4, string> = { + 1: 'SOLEMNITY', + 2: 'FEAST', + 3: 'MEMORIAL', + 4: 'WEEKDAY' +}; + +function colorKeysFrom(c: Celebration1962): string[] { + return c.colors.map((col) => COLOR_KEY_1962[col] ?? col.toUpperCase()); +} + +function localizedName(c: Celebration1962, lang: CalendarLang): string { + if (lang === 'la') return c.name; + return c.names?.[lang] ?? c.name; +} + +function adaptCommem(c: Celebration1962, lang: CalendarLang): Rite1962Commem { + const colorKeys = colorKeysFrom(c); + return { + key: c.key, + name: localizedName(c, lang), + rankName: rank1962Label(c.rank1962, lang), + kind: c.kind, + colorKeys, + colorNames: colorKeys.map((k) => colorLabel1962(k, lang)) + }; +} + +function textOf(dict: Record | undefined, locale: string): string { + const v = dict?.[locale]; + return v && v.trim() ? v : ''; +} + +function propersOf(p: Celebration1962, lang: CalendarLang): ProperSection[] { + const out: ProperSection[] = []; + const m = p.propers; + if (!m) return out; + for (const key of PROPER_ORDER) { + const la = textOf(m[key as ProperKey], 'la'); + const local = lang === 'la' ? '' : textOf(m[key as ProperKey], lang); + if (!la && !local) continue; + out.push({ key, la, ...(local ? { local } : {}) }); + } + return out; +} + +function extraSectionsOf(p: Celebration1962, lang: CalendarLang): ProperSection[] { + const extras = p.extraSections; + if (!extras) return []; + const out: ProperSection[] = []; + for (const [key, block] of Object.entries(extras)) { + const buckets: Record = {}; + for (const item of block) { + if (item.type !== 'text') continue; + (buckets[item.lang] ??= []).push(item.value); + } + const la = (buckets['la'] ?? []).join('\n\n').trim(); + const local = lang === 'la' ? '' : (buckets[lang] ?? []).join('\n\n').trim(); + if (!la && !local) continue; + out.push({ key, la, ...(local ? { local } : {}) }); + } + return out; +} + +function adaptDay1962(day: ResolvedDay1962, lang: CalendarLang): CalendarDay { + const p: Celebration1962 = day.primary; + const colorKeys = colorKeysFrom(p); + const colorNames = colorKeys.map((k) => colorLabel1962(k, lang)); + const detail: Rite1962Detail = { + class: p.classOf1962, + kind: p.kind, + commemorations: day.commemorations.map((c) => adaptCommem(c, lang)), + rubrics: { + gloria: p.rubrics.gloria, + credo: p.rubrics.credo, + preface: p.rubrics.preface, + lastGospel: p.rubrics.lastGospel, + ite: p.rubrics.ite + }, + ...(p.octave + ? { + octave: { + id: p.octave.id, + parentFeastId: p.octave.parentFeastId, + day: p.octave.day, + rank: p.octave.rank + } + } + : {}), + ...(p.vigil ? { vigilOf: p.vigil.of } : {}), + ...(day.transferredFrom ? { transferredFrom: day.transferredFrom } : {}), + properSource: p.properRef.source, + ...(p.properRef.communeSlug ? { communeSlug: p.properRef.communeSlug } : {}), + propers: propersOf(p, lang), + extraSections: extraSectionsOf(p, lang) + }; + return { + iso: day.date, + id: p.key, + name: localizedName(p, lang), + rankName: rank1962Label(p.rank1962, lang), + rank: RANK_FROM_CLASS_1962[p.classOf1962], + seasonNames: day.season ? [season1962Label(day.season, lang)] : [], + colorNames, + colorKeys, + psalterWeek: null, + sundayCycle: null, + rite1962: detail + }; +} + +const yearCache1962 = new Map>(); + +async function getYear1962( + lang: CalendarLang, + year: number +): Promise> { + const cacheKey = `${lang}|${year}`; + const cached = yearCache1962.get(cacheKey); + if (cached) return cached; + const resolved = await getRomcal1962(lang).generateCalendar(year); + const map = new Map(); + for (const [iso, day] of resolved) map.set(iso, adaptDay1962(day, lang)); + yearCache1962.set(cacheKey, map); + return map; +} + function isoFor(year: number, month: number, day: number): string { const mm = String(month + 1).padStart(2, '0'); const dd = String(day).padStart(2, '0'); @@ -85,36 +301,23 @@ export const load: PageServerLoad = async ({ params, url, locals }) => { params.faithLang === 'faith' ? 'en' : params.faithLang === 'fides' ? 'la' : 'de'; const today = new Date(); - const riteParam = url.searchParams.get('rite'); - const rite: Rite = isValidRite(riteParam) ? riteParam : '1969'; - - // 1962 rite is WIP — skip data generation - if (rite === '1962') { - return { - rite, - wip: true, - year: today.getFullYear(), - month: today.getMonth(), - monthDays: [], - today: null, - todayIso: today.toISOString().slice(0, 10), - selected: null, - selectedIso: '', - session: locals.session ?? (await locals.auth()) - }; - } + // Rite lives in the optional [[year]] route segment (1962 | 1969). When + // absent we default to 1962, the new tridentine calendar. + const rite: Rite = params.year === '1969' ? '1969' : '1962'; const yParam = url.searchParams.get('y'); const mParam = url.searchParams.get('m'); const selectedDateParam = url.searchParams.get('d'); + const minYear = rite === '1962' ? 1900 : 1969; const y = yParam !== null ? Number(yParam) : NaN; const m = mParam !== null ? Number(mParam) : NaN; - const year = Number.isFinite(y) && y >= 1969 && y <= 2100 ? y : today.getFullYear(); + const year = Number.isFinite(y) && y >= minYear && y <= 2100 ? y : today.getFullYear(); const month = Number.isFinite(m) && m >= 0 && m <= 11 ? m : today.getMonth(); - const yearMap = await getYear(lang, year); + const yearMap = + rite === '1962' ? await getYear1962(lang, year) : await getYear(lang, year); const daysInMonth = new Date(year, month + 1, 0).getDate(); const monthDays: CalendarDay[] = []; for (let d = 1; d <= daysInMonth; d++) { @@ -138,7 +341,10 @@ export const load: PageServerLoad = async ({ params, url, locals }) => { } const todayIso = today.toISOString().slice(0, 10); - const todayYearMap = await getYear(lang, today.getFullYear()); + const todayYearMap = + rite === '1962' + ? await getYear1962(lang, today.getFullYear()) + : await getYear(lang, today.getFullYear()); const todayEntry = todayYearMap.get(todayIso) ?? null; let selectedIso: string; @@ -155,7 +361,9 @@ export const load: PageServerLoad = async ({ params, url, locals }) => { ? yearMap : selectedYear === today.getFullYear() ? todayYearMap - : await getYear(lang, selectedYear); + : rite === '1962' + ? await getYear1962(lang, selectedYear) + : await getYear(lang, selectedYear); const selectedEntry = selectedYearMap.get(selectedIso) ?? monthDays[0]; return { diff --git a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[[year=calendarRite]]/+page.svelte b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[[year=calendarRite]]/+page.svelte index c1cf82d7..a698ba43 100644 --- a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[[year=calendarRite]]/+page.svelte +++ b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[[year=calendarRite]]/+page.svelte @@ -1,5 +1,6 @@ @@ -104,6 +116,15 @@

{t('wipBody', lang)}

{:else} + {#if rite === '1962'} + + {/if} {#if today} {@const todayHex = hexFor(today.colorKeys)}
@@ -228,6 +249,96 @@ {t('cycle', lang)}: {humanizeSundayCycle(selected.sundayCycle)} {/if} + {#if selected.rite1962} + {@const d = selected.rite1962} +
+
+
{t1962('source', lang)}
+
{d.kind}{d.properSource ? ` · ${d.properSource}` : ''}{d.communeSlug ? ` (${d.communeSlug})` : ''}
+
+ {#if d.vigilOf} +
+
{t1962('vigilOf', lang)}
+
{d.vigilOf}
+
+ {/if} + {#if d.octave} +
+
{t1962('octave', lang)}
+
{d.octave.id} · {t1962('octaveDay', lang)} {d.octave.day} · {d.octave.rank}
+
+ {/if} + {#if d.transferredFrom} +
+
{t1962('transferredFrom', lang)}
+
{d.transferredFrom}
+
+ {/if} +
+
+

{t1962('rubrics', lang)}

+
+ {t1962('gloria', lang)}: {d.rubrics.gloria ? t1962('yes', lang) : t1962('no', lang)} + {t1962('credo', lang)}: {d.rubrics.credo ? t1962('yes', lang) : t1962('no', lang)} + {#if d.rubrics.preface} + {t1962('preface', lang)}: {d.rubrics.preface} + {/if} + {#if d.rubrics.lastGospel} + {t1962('lastGospel', lang)}: {d.rubrics.lastGospel} + {/if} + {#if d.rubrics.ite} + {t1962('ite', lang)}: {d.rubrics.ite} + {/if} +
+
+ {#if d.commemorations.length} +
+

{t1962('commemorations', lang)}

+
    + {#each d.commemorations as c (c.key)} + {@const cHex = hexFor(c.colorKeys)} +
  • + + {c.name} + {c.rankName} +
  • + {/each} +
+
+ {/if} + {#if d.propers.length} +
+

{t1962('propers', lang)}

+ {#each d.propers as section (section.key)} +
+
{properLabel(section.key, lang)}
+
+
{section.la}
+ {#if lang !== 'la' && section.local} +
{section.local}
+ {/if} +
+
+ {/each} +
+ {/if} + {#if d.extraSections.length} +
+

{t1962('extraSections', lang)}

+ {#each d.extraSections as section (section.key)} +
+
{properLabel(section.key, lang)}
+
+
{section.la}
+ {#if lang !== 'la' && section.local} +
{section.local}
+ {/if} +
+
+ {/each} +
+ {/if} + {/if}
{/if} {/if} @@ -330,6 +441,36 @@ line-height: 1.5; } + /* --- 1962 accuracy disclaimer --- */ + .disclaimer { + display: flex; + align-items: flex-start; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin-bottom: 1.25rem; + background: var(--color-surface); + border-left: 3px solid var(--color-primary); + border-radius: var(--radius-card); + box-shadow: var(--shadow-sm, var(--shadow-md)); + color: var(--color-text-secondary); + font-size: 0.9rem; + line-height: 1.45; + } + .disclaimer svg { + flex-shrink: 0; + margin-top: 0.15rem; + color: var(--color-primary); + } + .disclaimer strong { + display: block; + color: var(--color-text-primary); + font-weight: 600; + margin-bottom: 0.2rem; + } + .disclaimer p { + margin: 0; + } + /* --- Today hero --- */ .today-hero { position: relative; @@ -607,6 +748,139 @@ gap: 0.5rem; } + .detail-extras { + margin: 1rem 0 0.5rem; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.5rem 1rem; + font-size: var(--text-sm); + } + .detail-extras div { + display: flex; + flex-direction: column; + } + .detail-extras dt { + font-weight: 600; + color: var(--color-text-secondary); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.05em; + } + .detail-extras dd { + margin: 0; + color: var(--color-text-primary); + } + .rubrics-grid { + margin-top: 0.75rem; + } + .rubrics-grid h4, + .commems h4 { + margin: 0.5rem 0 0.4rem; + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-secondary); + font-weight: 600; + } + .rubric-row { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + } + .rubric-chip { + padding: 0.25rem 0.6rem; + border-radius: var(--radius-pill); + background: var(--color-bg-tertiary); + color: var(--color-text-secondary); + font-size: 0.78rem; + border: 1px solid var(--color-border); + } + .rubric-chip.on { + background: color-mix(in srgb, var(--accent) 15%, var(--color-bg-tertiary)); + color: var(--color-text-primary); + border-color: color-mix(in srgb, var(--accent) 30%, var(--color-border)); + } + .commems { + margin-top: 0.75rem; + } + .commems ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.35rem; + } + .commems li { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.6rem; + background: var(--color-bg-tertiary); + border-radius: var(--radius-sm, 6px); + font-size: 0.85rem; + } + .commem-name { + flex: 1 1 auto; + color: var(--color-text-primary); + } + .commem-rank { + font-size: 0.72rem; + color: var(--color-text-secondary); + white-space: nowrap; + } + + .propers { + margin-top: 1rem; + border-top: 1px solid var(--color-border); + padding-top: 0.75rem; + } + .propers h4 { + margin: 0 0 0.6rem; + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-secondary); + font-weight: 600; + } + .proper-block { + margin-bottom: 0.75rem; + } + .proper-label { + font-weight: 600; + font-size: 0.8rem; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.03em; + margin-bottom: 0.2rem; + } + .proper-cols { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; + } + .proper-cols.single { + grid-template-columns: 1fr; + } + .proper-col { + white-space: pre-wrap; + font-size: 0.92rem; + line-height: 1.5; + color: var(--color-text-primary); + } + .proper-col-la { + font-style: italic; + color: var(--color-text-primary); + } + .proper-col-local { + color: var(--color-text-primary); + } + @media (max-width: 640px) { + .proper-cols { + grid-template-columns: 1fr; + } + } + /* --- Responsive --- */ @media (max-width: 560px) { .cal-wrap { diff --git a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/calendarI18n.ts b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/calendarI18n.ts index 12905e73..852a823d 100644 --- a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/calendarI18n.ts +++ b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/calendarI18n.ts @@ -111,6 +111,16 @@ export const ui = { en: 'The calendar for the older rite (1962 Missal) is not yet available. Stay tuned.', de: 'Der Kalender für den alten Ritus (Messbuch 1962) ist noch nicht verfügbar. Bleib dran.', la: 'Calendarium ritus antiqui (Missale 1962) nondum paratum est. Exspecta paulisper.' + }, + rite1962DisclaimerTitle: { + en: 'Accuracy still being verified', + de: 'Genauigkeit wird noch geprüft', + la: 'Accuratio adhuc probanda' + }, + rite1962DisclaimerBody: { + en: 'The 1962 calendar is derived from divinum-officium data and is still being checked day-by-day against authoritative sources. Local proper calendars (diocese, religious order, national feasts) are not yet applied — only the general Roman calendar is rendered.', + de: 'Der Kalender für den Ritus von 1962 stammt aus divinum-officium-Daten und wird noch Tag für Tag mit maßgeblichen Quellen abgeglichen. Eigenkalender (Diözese, Ordensgemeinschaft, Landesfeste) sind noch nicht berücksichtigt — dargestellt wird nur der allgemeine römische Kalender.', + la: 'Calendarium anni 1962 ex datis divinum-officium ductum est et adhuc diebus singulis contra fontes fideles examinatur. Calendaria propria localia (dioecesis, ordinis religiosi, festa nationalia) nondum adhibentur — tantum calendarium Romanum generale ostenditur.' } }; @@ -122,3 +132,100 @@ export type Rite = '1969' | '1962'; export function isValidRite(v: string | null): v is Rite { return v === '1969' || v === '1962'; } + +// --- 1962 localization helpers --- +// 1962 calendar data is Latin-only at source (feast names stay Latin — +// they are canonical). UI chrome (rank, season, color) is localized here. + +const CLASS_LABEL: Record> = { + ClassI: { en: 'Class I', de: 'I. Klasse', la: 'I classis' }, + ClassII: { en: 'Class II', de: 'II. Klasse', la: 'II classis' }, + ClassIII: { en: 'Class III', de: 'III. Klasse', la: 'III classis' }, + ClassIV: { en: 'Class IV', de: 'IV. Klasse', la: 'IV classis' }, + Ferial: { en: 'Ferial', de: 'Ferialtag', la: 'Feria' } +}; + +export function rank1962Label(rank: string, lang: CalendarLang): string { + return CLASS_LABEL[rank]?.[lang] ?? rank; +} + +const SEASON_LABEL: Record> = { + Advent: { en: 'Advent', de: 'Advent', la: 'Adventus' }, + ChristmasTide: { en: 'Christmastide', de: 'Weihnachtszeit', la: 'Tempus Nativitatis' }, + EpiphanyTide: { en: 'Epiphanytide', de: 'Epiphaniaszeit', la: 'Tempus Epiphaniæ' }, + Septuagesima: { en: 'Septuagesima', de: 'Vorfastenzeit', la: 'Septuagesima' }, + Lent: { en: 'Lent', de: 'Fastenzeit', la: 'Quadragesima' }, + Passiontide: { en: 'Passiontide', de: 'Passionszeit', la: 'Tempus Passionis' }, + HolyWeek: { en: 'Holy Week', de: 'Karwoche', la: 'Hebdomada Sancta' }, + EasterWeek: { en: 'Easter Week', de: 'Osteroktav', la: 'Hebdomada Paschae' }, + Paschaltide: { en: 'Eastertide', de: 'Osterzeit', la: 'Tempus Paschale' }, + TimeAfterPentecost: { en: 'after Pentecost', de: 'nach Pfingsten', la: 'post Pentecosten' } +}; + +export function season1962Label(season: string, lang: CalendarLang): string { + return SEASON_LABEL[season]?.[lang] ?? season; +} + +const COLOR_LABEL_1962: Record> = { + WHITE: { en: 'White', de: 'Weiß', la: 'Albus' }, + RED: { en: 'Red', de: 'Rot', la: 'Ruber' }, + GREEN: { en: 'Green', de: 'Grün', la: 'Viridis' }, + PURPLE: { en: 'Violet', de: 'Violett', la: 'Violaceus' }, + ROSE: { en: 'Rose', de: 'Rosa', la: 'Rosaceus' }, + BLACK: { en: 'Black', de: 'Schwarz', la: 'Niger' }, + GOLD: { en: 'Gold', de: 'Gold', la: 'Aureus' } +}; + +export function colorLabel1962(colorKey: string, lang: CalendarLang): string { + return COLOR_LABEL_1962[colorKey]?.[lang] ?? colorKey; +} + +export const ui1962 = { + commemorations: { en: 'Commemorations', de: 'Kommemorationen', la: 'Commemorationes' }, + rubrics: { en: 'Rubrics', de: 'Rubriken', la: 'Rubricæ' }, + gloria: { en: 'Gloria', de: 'Gloria', la: 'Gloria' }, + credo: { en: 'Credo', de: 'Credo', la: 'Credo' }, + preface: { en: 'Preface', de: 'Präfation', la: 'Præfatio' }, + lastGospel: { en: 'Last Gospel', de: 'Letztes Evangelium', la: 'Ultimum Evangelium' }, + ite: { en: 'Dismissal', de: 'Entlassung', la: 'Dimissio' }, + octave: { en: 'Octave', de: 'Oktav', la: 'Octava' }, + octaveDay: { en: 'day', de: 'Tag', la: 'dies' }, + vigilOf: { en: 'Vigil of', de: 'Vigil von', la: 'Vigilia' }, + transferredFrom: { en: 'Transferred from', de: 'Übertragen von', la: 'Translatum ex' }, + source: { en: 'Source', de: 'Quelle', la: 'Fons' }, + yes: { en: 'yes', de: 'ja', la: 'sic' }, + no: { en: 'no', de: 'nein', la: 'non' }, + properRef: { en: 'Proper', de: 'Proprium', la: 'Proprium' }, + propers: { en: 'Mass propers', de: 'Messproprium', la: 'Propria Missæ' }, + extraSections: { en: 'Additional readings', de: 'Zusätzliche Lesungen', la: 'Lectiones additae' } +} as const; + +export function t1962(key: keyof typeof ui1962, lang: CalendarLang): string { + return ui1962[key][lang] ?? ui1962[key].en; +} + +const PROPER_LABEL: Record> = { + introit: { en: 'Introit', de: 'Introitus', la: 'Introitus' }, + collect: { en: 'Collect', de: 'Kollekte', la: 'Collecta' }, + epistle: { en: 'Epistle', de: 'Epistel', la: 'Epistola' }, + gradual: { en: 'Gradual', de: 'Graduale', la: 'Graduale' }, + alleluia: { en: 'Alleluia', de: 'Alleluja', la: 'Alleluia' }, + tract: { en: 'Tract', de: 'Tractus', la: 'Tractus' }, + sequence: { en: 'Sequence', de: 'Sequenz', la: 'Sequentia' }, + gospel: { en: 'Gospel', de: 'Evangelium', la: 'Evangelium' }, + offertory: { en: 'Offertory', de: 'Offertorium', la: 'Offertorium' }, + secret: { en: 'Secret', de: 'Stillgebet', la: 'Secreta' }, + preface: { en: 'Preface', de: 'Präfation', la: 'Præfatio' }, + communion: { en: 'Communion', de: 'Kommunion', la: 'Communio' }, + postcommunion: { en: 'Postcommunion', de: 'Schlussgebet', la: 'Postcommunio' } +}; + +export function properLabel(key: string, lang: CalendarLang): string { + // Ember-Saturday extra readings like LectioL1..LectioL5 + const m = /^LectioL(\d+)$/.exec(key); + if (m) { + const n = m[1]; + return lang === 'de' ? `Lesung ${n}` : lang === 'la' ? `Lectio ${n}` : `Reading ${n}`; + } + return PROPER_LABEL[key]?.[lang] ?? key; +}