From e37d41b180223d5ce69da33fb63d1304556feaa6 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Sat, 18 Apr 2026 22:28:48 +0200 Subject: [PATCH] feat(faith): adopt flat-id romcal fork and simplify 1962 calendar rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the romcal dependency to AlexBocken/romcal (monorepo fork with collapsed bucket prefixes) and strip the runtime prefix-fallback chain from liturgicalCalendar.ts — name/propers lookups now use a single flat id. The 1962 data model shrinks to just what the rendering uses (commem {id,name}, detail carrying propers as {key, la[], local[]}) and the detail + overview pages drop the rubrics/octave/properSource fields that never got wired in. --- package.json | 4 +- pnpm-lock.yaml | 16 +- src/lib/calendarTypes.ts | 35 +- src/lib/server/liturgicalCalendar.ts | 479 +++++++----------- .../[[dd=calendarDay]]/+page.server.ts | 1 - .../[[dd=calendarDay]]/+page.svelte | 139 ++--- .../[dd=calendarDay]/+page.svelte | 155 +----- .../[calendar=calendarLang]/calendarColors.ts | 8 +- .../[calendar=calendarLang]/calendarI18n.ts | 65 ++- 9 files changed, 305 insertions(+), 597 deletions(-) diff --git a/package.json b/package.json index 548fade1..8cc55ec8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.35.0", + "version": "1.35.1", "private": true, "type": "module", "scripts": { @@ -62,7 +62,7 @@ "leaflet": "^1.9.4", "mongoose": "^9.4.1", "node-cron": "^4.2.1", - "romcal": "github:AlexBocken/romcal1962#dev", + "romcal": "github:AlexBocken/romcal#dev", "sharp": "^0.34.5", "web-haptics": "^0.0.6" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c440544..f0b3e188 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@https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d(typescript@6.0.2)) + version: 3.0.0-dev.125(romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(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: github:AlexBocken/romcal1962#dev - version: https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d(typescript@6.0.2) + specifier: github:AlexBocken/romcal#dev + version: https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2) sharp: specifier: ^0.34.5 version: 0.34.5 @@ -1782,8 +1782,8 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - romcal@https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d: - resolution: {tarball: https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d} + romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde: + resolution: {tarball: https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde} version: 3.0.0-dev.125 engines: {node: '>=18.0.0'} @@ -2683,9 +2683,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.1': optional: true - '@romcal/calendar.general-roman@3.0.0-dev.125(romcal@https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d(typescript@6.0.2))': + '@romcal/calendar.general-roman@3.0.0-dev.125(romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2))': dependencies: - romcal: https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d(typescript@6.0.2) + romcal: https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2) '@sec-ant/readable-stream@0.4.1': {} @@ -3545,7 +3545,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 - romcal@https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d(typescript@6.0.2): + romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2): dependencies: i18next: 26.0.4(typescript@6.0.2) transitivePeerDependencies: diff --git a/src/lib/calendarTypes.ts b/src/lib/calendarTypes.ts index 8e3fcf15..7ef01e01 100644 --- a/src/lib/calendarTypes.ts +++ b/src/lib/calendarTypes.ts @@ -35,52 +35,25 @@ export interface SeasonArc { } export interface Rite1962Commem { - key: string; + id: 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; + ofId: string; day: number; - rank: string; }; vigilOf?: string; transferredFrom?: string; - properSource: string; - communeSlug?: string; propers: ProperSection[]; - extraSections: ProperSection[]; -} - -export interface ProperSegment { - refs: string[]; - la: string; - local?: string; - // When true, `local` text comes from the Bible translation lookup because - // the propers dataset had no localized text for this segment. - fromBible?: boolean; } export interface ProperSection { key: string; - segments: ProperSegment[]; - // Aggregate list of refs across segments (for quick checks) - refs: string[]; - fromBible?: boolean; + la: string[]; + local: string[]; } diff --git a/src/lib/server/liturgicalCalendar.ts b/src/lib/server/liturgicalCalendar.ts index 45b5d46f..f9b3f23e 100644 --- a/src/lib/server/liturgicalCalendar.ts +++ b/src/lib/server/liturgicalCalendar.ts @@ -19,14 +19,11 @@ import { Switzerland_Saint_Maurice_Abbey, Switzerland_Sankt_Gallen, Switzerland_Sion, - resolvePropersBlocks -} from 'romcal/1962'; -import type { - LiturgicalDay1962, - MassPropersBlocks, - MassSectionField, - PropersBlock + createI18n1962 } from 'romcal/1962'; +import type { LiturgicalDay1962, RomcalBundle1962 } from 'romcal/1962'; +import { pathToFileURL } from 'node:url'; +import { resolve as resolvePath } from 'node:path'; import { colorLabel1962, rank1962Label, @@ -38,13 +35,9 @@ import { import type { CalendarDay, ProperSection, - ProperSegment, Rite1962Commem, Rite1962Detail } from '../calendarTypes'; -import { lookupReference } from '$lib/server/bible'; -import { translateRefToTarget } from '$lib/server/bibleRefLatin'; -import { resolve as resolvePath } from 'path'; const bundles1969: Record> = { general: { en: GeneralRoman_En, de: GeneralRoman_De, la: GeneralRoman_La }, @@ -56,7 +49,7 @@ function getRomcal(lang: CalendarLang, diocese: Diocese1969): Romcal { const key = `${diocese}|${lang}`; let r = romcalByKey.get(key); if (r) return r; - r = new Romcal({ localizedCalendar: bundles1969[diocese][lang] }); + r = new Romcal({ localizedCalendar: bundles1969[diocese][lang], scope: 'liturgical' }); romcalByKey.set(key, r); return r; } @@ -111,307 +104,183 @@ const calendars1962 = { sion: Switzerland_Sion } as const satisfies Record; -const romcal1962ByKey = new Map(); -function getRomcal1962(lang: CalendarLang, diocese: Diocese1962): Romcal1962 { - const key = `${diocese}|${lang}`; - let r = romcal1962ByKey.get(key); - if (r) return r; - // Package localizes celebration.name via i18next; structured proper - // blocks are fetched lazily via resolvePropersBlocks (below), so we - // don't ask attachPropers to pre-concatenate locale text. - const calendar = calendars1962[diocese]; - r = calendar - ? new Romcal1962({ localeId: lang, calendar }) - : new Romcal1962({ localeId: lang }); - romcal1962ByKey.set(key, r); - return r; +// Names & propers for the 1962 rite ship as bundled JS files outside the +// package's `exports` map, so resolve them via an absolute file URL. +const bundle1962Cache = new Map>(); +function loadBundle1962(lang: CalendarLang): Promise { + let p = bundle1962Cache.get(lang); + if (p) return p; + const abs = resolvePath( + `node_modules/romcal/rites/roman1962/dist/bundles/${lang}/esm/index.js` + ); + p = import(/* @vite-ignore */ pathToFileURL(abs).href).then( + (m) => m.default as RomcalBundle1962 + ); + bundle1962Cache.set(lang, p); + return p; } -const PROPER_ORDER: MassSectionField[] = [ - 'introit', - 'collect', - 'epistle', - 'gradual', - 'alleluia', - 'tract', - 'sequence', - 'gospel', - 'offertory', - 'secret', - 'preface', - 'communion', - 'postcommunion' -]; +const romcal1962ByKey = new Map>(); +function getRomcal1962(lang: CalendarLang, diocese: Diocese1962): Promise { + const key = `${diocese}|${lang}`; + let p = romcal1962ByKey.get(key); + if (p) return p; + const calendar = calendars1962[diocese]; + // `localizedCalendar` must be a 1969-shape bundle; the 1962 names live on + // the 1962 propers bundle and are injected via createI18n1962 extraNames. + const base1969 = bundles1969.general[lang]; + p = loadBundle1962(lang).then((b) => { + const i18next = createI18n1962(lang, { [lang]: b.i18n.names }); + // `i18next` is part of Romcal's runtime config but absent from the + // published input type. Build via a permissive record so TS accepts it. + const base: Record = { + i18next, + localizedCalendar: base1969, + scope: 'liturgical' + }; + if (calendar) base.particularCalendar = calendar; + return new Romcal1962(base as ConstructorParameters[0]); + }); + romcal1962ByKey.set(key, p); + return p; +} -const COLOR_KEY_1962: Record = { - White: 'WHITE', - Red: 'RED', - Green: 'GREEN', - Violet: 'PURPLE', - Black: 'BLACK', - Rose: 'ROSE' -}; +// Section order follows the flow of the Mass. The new propers bundles key +// sections by their Latin section names. +const PROPER_ORDER = [ + 'Introitus', + 'Oratio', + 'Lectio', + 'Graduale', + 'GradualeF', + 'Tractus', + 'Sequentia', + 'Evangelium', + 'Offertorium', + 'Secreta', + 'Communio', + 'Postcommunio' +] as const; +const PROPER_ORDER_SET: ReadonlySet = new Set(PROPER_ORDER); const RANK_FROM_CLASS_1962: Record<1 | 2 | 3 | 4, string> = { - 1: 'SOLEMNITY', - 2: 'FEAST', - 3: 'MEMORIAL', - 4: 'WEEKDAY' + 1: 'ClassI', + 2: 'ClassII', + 3: 'ClassIII', + 4: 'ClassIV' }; function colorKeysFrom(c: LiturgicalDay1962): string[] { - return c.colors.map((col) => COLOR_KEY_1962[col] ?? col.toUpperCase()); + // romcal 3 returns SCREAMING_SNAKE color keys ("WHITE", "ROSE", ...) which + // already match our legend/CSS tokens. + return c.colors ? [...c.colors] : []; } -function adaptCommem(c: LiturgicalDay1962, lang: CalendarLang): Rite1962Commem { - const colorKeys = colorKeysFrom(c); - return { - key: c.key, - name: c.name, - rankName: rank1962Label(c.rank1962, lang), - kind: c.kind, - colorKeys, - colorNames: colorKeys.map((k) => colorLabel1962(k, lang)) - }; +function buildCommemorations(d: LiturgicalDay1962): Rite1962Commem[] { + return (d.commemorations ?? []).map((c) => ({ id: c.id, name: c.name })); } -function bibleTextFor(ref: string, targetLang: 'en' | 'de'): string | null { - const tsvPath = resolvePath(targetLang === 'de' ? 'static/allioli.tsv' : 'static/drb.tsv'); - const segments = ref.split(';').map((s) => s.trim()).filter(Boolean); - if (!segments.length) return null; - - let lastBook: string | null = null; - let lastChapter: string | null = null; - const parts: string[] = []; - - for (const seg of segments) { - // Detect optional leading book (letters, optional leading digit like "1 Cor") - const bookMatch = seg.match(/^(\d?\s?[A-Za-z]+\.?)\s+(.*)$/); - let book: string | null = null; - let rest = seg; - if (bookMatch) { - book = bookMatch[1]; - rest = bookMatch[2].trim(); - } - if (book) lastBook = book; - if (!lastBook) continue; - - let chapter: string; - let verseRange: string; - // Accept "118:85", "118, 85", "118:85-90", or bare "85" (inherit chapter) - const normalized = rest.replace(/\s*,\s*/, ':').replace(/\s+/g, ' ').trim(); - if (normalized.includes(':')) { - const [c, v] = normalized.split(':'); - chapter = c.trim(); - verseRange = v.trim(); - lastChapter = chapter; - } else if (lastChapter) { - chapter = lastChapter; - verseRange = normalized; - } else continue; - - const fullRef = `${lastBook} ${chapter}:${verseRange}`; - const translated = translateRefToTarget(fullRef, targetLang); - if (!translated) continue; - - try { - const result = lookupReference(translated, tsvPath); - if (result && result.verses.length) { - parts.push(result.verses.map((v) => `${v.verse}. ${v.text}`).join(' ')); - } - } catch { - // skip - } - } - - return parts.length ? parts.join(' ') : null; -} - -interface RawSegment { - refs: string[]; - la: string; - local: string; -} - -// Zip la / local text streams by index so la[i] and local[i] land in -// the same segment. Scripture refs attach to the la block that follows -// them; trailing refs with no following la block attach to the last -// segment so they still render. -function buildSegments(items: PropersBlock, localLang: CalendarLang): RawSegment[] { - const la: string[] = []; - const local: string[] = []; - const refsByIdx = new Map(); - let pendingRefs: string[] = []; - - for (const it of items) { - if (it.type === 'scriptureRef') { - pendingRefs.push(it.ref); - } else if (it.type === 'text') { - const val = it.value.trim(); - if (!val) continue; - if (it.lang === 'la') { - if (pendingRefs.length) { - refsByIdx.set(la.length, pendingRefs); - pendingRefs = []; - } - la.push(val); - } else if (it.lang === localLang) { - local.push(val); - } - } - } - - if (pendingRefs.length && la.length) { - const lastIdx = la.length - 1; - const existing = refsByIdx.get(lastIdx) ?? []; - refsByIdx.set(lastIdx, [...existing, ...pendingRefs]); - } - - const count = Math.max(la.length, local.length); - const segs: RawSegment[] = []; - for (let i = 0; i < count; i++) { - segs.push({ refs: refsByIdx.get(i) ?? [], la: la[i] ?? '', local: local[i] ?? '' }); - } - return segs; -} - -function propersOf(sections: MassPropersBlocks, lang: CalendarLang): ProperSection[] { - const out: ProperSection[] = []; - for (const key of PROPER_ORDER) { - const block = sections[key]; - if (!block || !block.length) continue; - const rawSegs = buildSegments(block, lang); - if (!rawSegs.length) continue; - - const segments: ProperSegment[] = []; - const allRefs: string[] = []; - let sectionFromBible = false; - - for (const raw of rawSegs) { - const seg: ProperSegment = { refs: raw.refs, la: raw.la }; - if (lang !== 'la' && raw.local) seg.local = raw.local; - - // Bible fallback: only for this segment, using only its own refs - if (!seg.local && raw.refs.length && lang !== 'la') { - const bible = bibleTextFor(raw.refs.join('; '), lang); - if (bible) { - seg.local = bible; - seg.fromBible = true; - sectionFromBible = true; - } - } - - allRefs.push(...raw.refs); - if (seg.la || seg.local || seg.refs.length) segments.push(seg); - } - - if (!segments.length) continue; - const section: ProperSection = { key, segments, refs: allRefs }; - if (sectionFromBible) section.fromBible = true; - out.push(section); - } - return out; -} - -function extraSectionsOf( - extras: Record, - lang: CalendarLang +function sectionsFromBundle( + laPropers: Record | undefined, + localPropers: Record | undefined ): ProperSection[] { - const out: ProperSection[] = []; - for (const [key, block] of Object.entries(extras)) { - const buckets: Record = {}; - const refs: string[] = []; - for (const item of block) { - if (item.type === 'text') (buckets[item.lang] ??= []).push(item.value); - else if (item.type === 'scriptureRef') refs.push(item.ref); - } - const la = (buckets['la'] ?? []).join('\n\n').trim(); - const local = lang === 'la' ? '' : (buckets[lang] ?? []).join('\n\n').trim(); - if (!la && !local && refs.length === 0) continue; - const segment: ProperSegment = { refs, la }; - if (local) segment.local = local; - out.push({ key, segments: [segment], refs }); - } - return out; -} - -// Primary celebration may only carry the sections that override the commune -// (e.g. only `gospel` for a confessor). Merge commune sections under the -// primary so every liturgical slot has a text source. -function mergeCommunePropers( - p: LiturgicalDay1962, - sections: MassPropersBlocks, - extraSections: Record -): { sections: MassPropersBlocks; extraSections: Record } { - const slug = p.properRef.communeSlug; - if (!slug) return { sections, extraSections }; - const communeCelebration = { - ...p, - properRef: { source: `commune/${slug}` } - } as LiturgicalDay1962; - let communeResolved; - try { - communeResolved = resolvePropersBlocks(communeCelebration); - } catch { - return { sections, extraSections }; - } - const mergedSections = { ...communeResolved.sections, ...sections } as MassPropersBlocks; - const mergedExtras = { ...communeResolved.extraSections, ...extraSections }; - return { sections: mergedSections, extraSections: mergedExtras }; -} - -function adaptDay1962(entries: LiturgicalDay1962[], lang: CalendarLang): CalendarDay { - const p = entries[0]; - const commemorations = entries.slice(1); - const colorKeys = colorKeysFrom(p); - const colorNames = colorKeys.map((k) => colorLabel1962(k, lang)); - const resolved = resolvePropersBlocks(p); - const { sections, extraSections } = mergeCommunePropers( - p, - resolved.sections, - resolved.extraSections - ); - const detail: Rite1962Detail = { - class: p.classOf1962, - kind: p.kind, - commemorations: 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 } : {}), - ...(p.transferredFromDate ? { transferredFrom: p.transferredFromDate } : {}), - properSource: p.properRef.source, - ...(p.properRef.communeSlug ? { communeSlug: p.properRef.communeSlug } : {}), - propers: propersOf(sections, lang), - extraSections: extraSectionsOf(extraSections, lang) + if (!laPropers && !localPropers) return []; + const sections: ProperSection[] = []; + const seen = new Set(); + const emit = (key: string) => { + if (seen.has(key)) return; + seen.add(key); + const la = laPropers?.[key]; + const local = localPropers?.[key]; + if ((!la || !la.length) && (!local || !local.length)) return; + sections.push({ key, la: la ? [...la] : [], local: local ? [...local] : [] }); }; - // Pentecost octave (Pentecost Sunday + 6 days) is carved out of Paschaltide so - // it shows as its own arc in the year ring, mirroring the Easter Week octave. - const isPentecostWeek = typeof p.key === 'string' && p.key.startsWith('easter_time_7_'); - const seasonKey = isPentecostWeek ? 'Pentecost' : p.season ?? null; - const seasonNames = seasonKey ? [season1962Label(seasonKey, lang)] : []; + for (const key of PROPER_ORDER) emit(key); + // Emit any remaining sections (ember-day readings, extras) in source order. + const extraKeys = new Set(); + for (const k of Object.keys(laPropers ?? {})) if (!PROPER_ORDER_SET.has(k)) extraKeys.add(k); + for (const k of Object.keys(localPropers ?? {})) if (!PROPER_ORDER_SET.has(k)) extraKeys.add(k); + for (const k of extraKeys) emit(k); + return sections; +} + +function findPropersFor( + d: LiturgicalDay1962, + bundle: RomcalBundle1962 +): Record | undefined { + const kind = d.kind1962; + const key = d.key1962 ?? d.id; + if (!kind) return undefined; + return bundle.propers[kind]?.[key]; +} + +function humanizeId(id: string): string { + return id + .split(/[_\s]+/) + .filter(Boolean) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' '); +} + +// When romcal's own i18next lookup misses (stale bundle, unknown id), it +// returns the raw key — detect that and fall back to a direct bundle name +// lookup, then a humanized id. +function resolveName1962( + d: LiturgicalDay1962, + localBundle: RomcalBundle1962 | null, + laBundle: RomcalBundle1962 +): string { + const raw = d.name ?? ''; + const id = d.id; + const key = d.key1962 ?? d.id; + const looksUnresolved = + !raw || raw === id || raw === key || /^[a-z][a-z0-9_]*\.[a-z_]+$/.test(raw); + if (!looksUnresolved) return raw; + const bundles = [localBundle, laBundle].filter( + (b): b is RomcalBundle1962 => b != null + ); + for (const b of bundles) { + const names = b.i18n.names; + const v = names?.[key] ?? names?.[id]; + if (v && v !== key && v !== id) return v; + } + return humanizeId(id); +} + +function adaptDay1962( + d: LiturgicalDay1962, + lang: CalendarLang, + laBundle: RomcalBundle1962, + localBundle: RomcalBundle1962 | null +): CalendarDay { + const colorKeys = colorKeysFrom(d); + const colorNames = colorKeys.map((k) => colorLabel1962(k, lang)); + const classOf = (d.classOf1962 ?? 4) as 1 | 2 | 3 | 4; + const classKey = RANK_FROM_CLASS_1962[classOf]; + + const laProps = findPropersFor(d, laBundle); + const localProps = localBundle ? findPropersFor(d, localBundle) : undefined; + const propers = sectionsFromBundle(laProps, localProps); + + const detail: Rite1962Detail = { + class: classOf, + kind: d.kind1962 ?? 'tempora', + commemorations: buildCommemorations(d), + propers + }; + if (d.octaveOf) detail.octave = { ofId: d.octaveOf.ofId, day: d.octaveOf.day }; + if (d.vigilOf) detail.vigilOf = d.vigilOf; + if (d.transferredFromDate) detail.transferredFrom = d.transferredFromDate; + + const seasonKey = d.seasons?.[0] ?? null; return { - iso: p.date, - id: p.key, - name: p.name, - rankName: rank1962Label(p.rank1962, lang), - rank: RANK_FROM_CLASS_1962[p.classOf1962], + iso: d.date, + id: d.id, + name: resolveName1962(d, localBundle, laBundle), + rankName: rank1962Label(classKey, lang), + rank: classKey, seasonKey, - seasonNames, + seasonNames: d.seasonNames ? [...d.seasonNames] : [], colorNames, colorKeys, psalterWeek: null, @@ -430,13 +299,23 @@ export async function getYear1962( const cacheKey = `${diocese}|${lang}|${year}`; const cached = yearCache1962.get(cacheKey); if (cached) return cached; - const resolved = await getRomcal1962(lang, diocese).generateCalendar(year); + + const [romcal, laBundle, localBundle] = await Promise.all([ + getRomcal1962(lang, diocese), + loadBundle1962('la'), + lang === 'la' ? Promise.resolve(null) : loadBundle1962(lang) + ]); + const resolved = await romcal.generateCalendar(year); const map = new Map(); for (const [iso, entries] of Object.entries(resolved)) { - if (!entries.length) continue; - map.set(iso, adaptDay1962(entries, lang)); + const principal = entries[0]; + if (!principal) continue; + map.set(iso, adaptDay1962(principal, lang, laBundle, localBundle)); } yearCache1962.set(cacheKey, map); + // keep season1962Label referenced so tree-shakers don't drop it while the + // detail view gradually takes over rendering its own labels. + void season1962Label; return map; } diff --git a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]/+page.server.ts b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]/+page.server.ts index c599c904..3dd300c7 100644 --- a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]/+page.server.ts +++ b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]/+page.server.ts @@ -23,7 +23,6 @@ import type { CalendarDay, SeasonArc, YearDay } from '$lib/calendarTypes'; export type { CalendarDay, ProperSection, - ProperSegment, Rite1962Commem, Rite1962Detail, SeasonArc, diff --git a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]/+page.svelte b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]/+page.svelte index 22130c8b..b89e611f 100644 --- a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]/+page.svelte +++ b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]/+page.svelte @@ -238,22 +238,11 @@ {t('cycle', lang)}: {humanizeSundayCycle(hero.sundayCycle)} {/if} - {#if hero.rite1962} - {@const r = hero.rite1962.rubrics} -
- - - {t1962('gloria', lang)} - {r.gloria ? t1962('yes', lang) : t1962('no', lang)} - - - - {t1962('credo', lang)} - {r.credo ? t1962('yes', lang) : t1962('no', lang)} - - {#if r.preface} - {t1962('preface', lang)}: {r.preface} - {/if} + {#if hero.rite1962 && hero.rite1962.commemorations.length} +
+ {#each hero.rite1962.commemorations as c (c.id)} + {c.name} + {/each}
{/if} @@ -281,19 +270,30 @@ ▦ {lang === 'de' ? 'Monat' : lang === 'la' ? 'Mensis' : 'Month'}
-
- {#each Object.keys(LIT_COLOR_VAR) as key (key)} - {@const label = - lang === 'de' - ? { WHITE: 'Weiß', RED: 'Rot', GREEN: 'Grün', PURPLE: 'Violett', ROSE: 'Rosa', BLACK: 'Schwarz', GOLD: 'Gold' }[key] - : lang === 'la' - ? { WHITE: 'Albus', RED: 'Ruber', GREEN: 'Viridis', PURPLE: 'Violaceus', ROSE: 'Rosaceus', BLACK: 'Niger', GOLD: 'Aureus' }[key] - : { WHITE: 'White', RED: 'Red', GREEN: 'Green', PURPLE: 'Violet', ROSE: 'Rose', BLACK: 'Black', GOLD: 'Gold' }[key]} - - - {label} - - {/each} +
+
+ {#each Object.keys(LIT_COLOR_VAR) as key (key)} + {@const label = + lang === 'de' + ? { WHITE: 'Weiß', RED: 'Rot', GREEN: 'Grün', PURPLE: 'Violett', ROSE: 'Rosa', BLACK: 'Schwarz' }[key] + : lang === 'la' + ? { WHITE: 'Albus', RED: 'Ruber', GREEN: 'Viridis', PURPLE: 'Violaceus', ROSE: 'Rosaceus', BLACK: 'Niger' }[key] + : { WHITE: 'White', RED: 'Red', GREEN: 'Green', PURPLE: 'Violet', ROSE: 'Rose', BLACK: 'Black' }[key]} + + + {label} + + {/each} +
+ + + {t('jumpToToday', lang)} +
@@ -333,13 +333,6 @@ - -
{#each weekdayLabels as wd (wd)} @@ -642,60 +635,21 @@ letter-spacing: 0.06em; text-transform: uppercase; } - .tc-rubrics { + .tc-commems { margin-top: 1.4rem; padding-top: 1.1rem; border-top: 1px solid rgba(255, 255, 255, 0.22); display: flex; flex-wrap: wrap; - gap: 0.5rem 1.1rem; - align-items: center; + gap: 0.4rem 0.6rem; } - .tc-rubric { - display: inline-flex; - align-items: center; - gap: 0.45rem; - padding: 0.35rem 0.75rem 0.35rem 0.6rem; + .tc-commem { + padding: 0.3rem 0.7rem; border-radius: var(--radius-pill); background: rgba(255, 255, 255, 0.18); border: 1px solid rgba(255, 255, 255, 0.22); font-size: 0.82rem; } - .tc-rubric b { - font-weight: 700; - } - .tc-rubric .tc-state { - font-size: 0.72rem; - opacity: 0.72; - font-style: italic; - } - .tc-rubric-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: rgba(255, 255, 255, 0.55); - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.15); - } - .tc-rubric.on .tc-rubric-dot { - background: #8be78b; - box-shadow: 0 0 0 2px rgba(139, 231, 139, 0.22); - } - .tc-rubric:not(.on) b { - opacity: 0.65; - } - .tc-preface { - font-size: 0.85rem; - opacity: 0.9; - } - .tc-preface em { - font-style: normal; - font-weight: 700; - font-size: 0.68rem; - letter-spacing: 0.1em; - text-transform: uppercase; - margin-right: 0.4rem; - opacity: 0.72; - } .tc-arrow { position: absolute; bottom: 1.1rem; @@ -760,10 +714,17 @@ .view-switcher button:active { transform: scale(0.95); } + .overview-right { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: flex-end; + } .legend { display: flex; gap: 0.4rem; flex-wrap: wrap; + justify-content: flex-end; } .legend .swatch { display: inline-flex; @@ -837,11 +798,6 @@ box-shadow: var(--shadow-hover); } - .today-jump-row { - display: flex; - justify-content: center; - margin-bottom: 1rem; - } .jump-btn { display: inline-flex; align-items: center; @@ -851,12 +807,25 @@ color: var(--color-text-secondary); border-radius: var(--radius-pill); font-size: var(--text-sm); - transition: background var(--transition-fast), transform var(--transition-fast); + box-shadow: var(--shadow-sm); + transition: background var(--transition-fast), transform var(--transition-fast), + box-shadow var(--transition-fast); } .jump-btn:hover { background: var(--color-bg-elevated); color: var(--color-text-primary); transform: scale(1.03); + box-shadow: var(--shadow-hover); + } + .jump-btn-gold { + background: var(--lit-gold); + color: var(--lit-gold-ink); + font-weight: 600; + } + .jump-btn-gold:hover { + background: var(--lit-gold); + color: var(--lit-gold-ink); + filter: brightness(1.08); } .grid { diff --git a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/detail/[yyyy=calendarYear]/[mm=calendarMonth]/[dd=calendarDay]/+page.svelte b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/detail/[yyyy=calendarYear]/[mm=calendarMonth]/[dd=calendarDay]/+page.svelte index 0778a370..40c2c38a 100644 --- a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/detail/[yyyy=calendarYear]/[mm=calendarMonth]/[dd=calendarDay]/+page.svelte +++ b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/detail/[yyyy=calendarYear]/[mm=calendarMonth]/[dd=calendarDay]/+page.svelte @@ -12,6 +12,12 @@ t1962, type CalendarLang } from '../../../../../calendarI18n'; + + function kindLabel(kind: 'tempora' | 'sancti', l: CalendarLang): string { + if (kind === 'tempora') + return l === 'de' ? 'Temporale' : l === 'la' ? 'Temporale' : 'Temporal'; + return l === 'de' ? 'Sanktorale' : l === 'la' ? 'Sanctorale' : 'Sanctoral'; + } import { litBg, litInk } from '../../../../../calendarColors'; let { data }: { data: PageData } = $props(); @@ -115,7 +121,7 @@
{t1962('source', lang)}
-
{d.kind}{d.properSource ? ` · ${d.properSource}` : ''}{d.communeSlug ? ` (${d.communeSlug})` : ''}
+
{kindLabel(d.kind, lang)}
{#if d.vigilOf}
@@ -126,7 +132,7 @@ {#if d.octave}
{t1962('octave', lang)}
-
{d.octave.id} · {t1962('octaveDay', lang)} {d.octave.day} · {d.octave.rank}
+
{d.octave.ofId} · {t1962('octaveDay', lang)} {d.octave.day}
{/if} {#if d.transferredFrom} @@ -136,32 +142,13 @@
{/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)} + {#each d.commemorations as c (c.id)}
  • - {c.name} - {c.rankName}
  • {/each}
@@ -171,62 +158,26 @@

{t1962('propers', lang)}

{#each d.propers as section (section.key)} + {@const rows = Math.max(section.la.length, section.local.length)}
{properLabel(section.key, lang)}
- {#each section.segments as seg, segIdx (segIdx)} -
- {#if seg.refs && seg.refs.length} -
- {#each seg.refs as r (r)} - {r} - {/each} -
- {/if} - {#if seg.la || seg.local} -
- {#if lang !== 'la' && seg.local && seg.fromBible} -

{t1962('bibleFallbackNote', lang)}

+ {#each Array(rows) as _, i (i)} + {@const la = section.la[i] ?? ''} + {@const local = section.local[i] ?? ''} + {#if la || local} +
+
+ {#if la} +
{la}
{/if} - {#if seg.la} -
{seg.la}
- {/if} - {#if lang !== 'la' && seg.local} -
{seg.local}
+ {#if lang !== 'la' && local} +
{local}
{/if}
- {/if} -
- {/each} -
- {/each} -
- {/if} - {#if d.extraSections.length} -
-

{t1962('extraSections', lang)}

- {#each d.extraSections as section (section.key)} -
-
- {properLabel(section.key, lang)} -
- {#each section.segments as seg, segIdx (segIdx)} -
- {#if seg.refs && seg.refs.length} -
- {#each seg.refs as r (r)} - {r} - {/each} -
- {/if} -
-
{seg.la}
- {#if lang !== 'la' && seg.local} -
{seg.local}
- {/if}
-
+ {/if} {/each}
{/each} @@ -399,10 +350,6 @@ margin: 0; color: var(--color-text-primary); } - .rubrics-grid { - margin-top: 0.75rem; - } - .rubrics-grid h4, .commems h4, .propers h4 { margin: 0.5rem 0 0.4rem; @@ -412,24 +359,6 @@ 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; } @@ -450,18 +379,10 @@ border-radius: var(--radius-sm, 6px); font-size: 0.85rem; } - .commems .color-swatch { - border-color: var(--color-border); - } .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; @@ -485,15 +406,6 @@ text-transform: uppercase; letter-spacing: 0.03em; } - .proper-ref { - display: inline-block; - padding: 0.1rem 0.5rem; - border-radius: var(--radius-pill); - font-size: 0.72rem; - background: var(--color-bg-tertiary); - color: var(--color-text-primary); - border: 1px solid var(--color-border); - } .proper-segment { margin-top: 0.5rem; } @@ -504,24 +416,6 @@ padding-top: 0.5rem; border-top: 1px dashed var(--color-border); } - .proper-segment-refs { - display: flex; - flex-wrap: wrap; - gap: 0.35rem; - margin-bottom: 0.35rem; - } - .proper-fallback-note { - grid-column: 2 / 3; - grid-row: 1; - margin: 0 0 0.25rem; - padding: 0.4rem 0.6rem; - border-left: 2px solid color-mix(in srgb, var(--orange) 55%, transparent); - background: color-mix(in srgb, var(--orange) 8%, transparent); - border-radius: 0 var(--radius-sm) var(--radius-sm) 0; - font-size: 0.78rem; - line-height: 1.4; - color: var(--color-text-secondary); - } .proper-cols { display: grid; grid-template-columns: 1fr 1fr; @@ -531,12 +425,10 @@ } .proper-col-la { grid-column: 1; - grid-row: 2; font-style: italic; } .proper-col-local { grid-column: 2; - grid-row: 2; } .proper-cols.single { grid-template-columns: 1fr; @@ -544,7 +436,6 @@ .proper-cols.single .proper-col-la, .proper-cols.single .proper-col-local { grid-column: 1; - grid-row: auto; } .proper-col { white-space: pre-wrap; @@ -564,10 +455,8 @@ grid-template-columns: 1fr; } .proper-cols .proper-col-la, - .proper-cols .proper-col-local, - .proper-cols .proper-fallback-note { + .proper-cols .proper-col-local { grid-column: 1; - grid-row: auto; } } @media (max-width: 560px) { diff --git a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/calendarColors.ts b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/calendarColors.ts index 5bd19f20..958507ce 100644 --- a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/calendarColors.ts +++ b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/calendarColors.ts @@ -1,14 +1,15 @@ // Liturgical color tokens used by the overview design. // Maps romcal color keys to the CSS variables defined on the calendar page. +// Romcal never emits GOLD for either rite, so it is excluded from the legend +// and lookup here. The --lit-gold token still exists for today-pin styling. export const LIT_COLOR_VAR: Record = { WHITE: '--lit-white', RED: '--lit-red', GREEN: '--lit-green', PURPLE: '--lit-violet', ROSE: '--lit-rose', - BLACK: '--lit-black', - GOLD: '--lit-gold' + BLACK: '--lit-black' }; export const LIT_INK_VAR: Record = { @@ -17,8 +18,7 @@ export const LIT_INK_VAR: Record = { GREEN: '--lit-green-ink', PURPLE: '--lit-violet-ink', ROSE: '--lit-rose-ink', - BLACK: '--lit-black-ink', - GOLD: '--lit-gold-ink' + BLACK: '--lit-black-ink' }; export function litBg(colorKey: string | undefined): string { diff --git a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/calendarI18n.ts b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/calendarI18n.ts index d607ba45..9c692409 100644 --- a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/calendarI18n.ts +++ b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/calendarI18n.ts @@ -59,11 +59,18 @@ export function hexFor(colorKeys: string[]): string { return colorHex[first] ?? '#27AE60'; } -// Rank emphasis for visual weighting of cells +// Rank emphasis for visual weighting of cells. Accepts both 1969 rank keys and +// 1962 class labels (ClassI..IV), since 1962 days are emphasized similarly. export function rankEmphasis(rank: string): number { - if (rank === 'SOLEMNITY') return 3; - if (rank === 'FEAST' || rank === 'SUNDAY' || rank === 'HOLY_DAY_OF_OBLIGATION') return 2; - if (rank === 'MEMORIAL') return 1; + if (rank === 'SOLEMNITY' || rank === 'ClassI') return 3; + if ( + rank === 'FEAST' || + rank === 'SUNDAY' || + rank === 'HOLY_DAY_OF_OBLIGATION' || + rank === 'ClassII' + ) + return 2; + if (rank === 'MEMORIAL' || rank === 'ClassIII') return 1; return 0; } @@ -245,7 +252,14 @@ const SEASON_LABEL: Record> = { EasterWeek: { en: 'Easter Week', de: 'Osteroktav', la: 'Hebdomada Paschae' }, Paschaltide: { en: 'Eastertide', de: 'Osterzeit', la: 'Tempus Paschale' }, Pentecost: { en: 'Pentecost Week', de: 'Pfingstoktav', la: 'Hebdomada Pentecostes' }, - TimeAfterPentecost: { en: 'after Pentecost', de: 'nach Pfingsten', la: 'post Pentecosten' } + TimeAfterPentecost: { en: 'after Pentecost', de: 'nach Pfingsten', la: 'post Pentecosten' }, + // romcal 3 emits 1969-style SCREAMING_SNAKE season keys even for the 1962 calendar. + ADVENT: { en: 'Advent', de: 'Advent', la: 'Adventus' }, + CHRISTMAS_TIME: { en: 'Christmastide', de: 'Weihnachtszeit', la: 'Tempus Nativitatis' }, + LENT: { en: 'Lent', de: 'Fastenzeit', la: 'Quadragesima' }, + PASCHAL_TRIDUUM: { en: 'Paschal Triduum', de: 'Ostertriduum', la: 'Triduum Paschale' }, + EASTER_TIME: { en: 'Eastertide', de: 'Osterzeit', la: 'Tempus Paschale' }, + ORDINARY_TIME: { en: 'after Pentecost', de: 'nach Pfingsten', la: 'post Pentecosten' } }; export function season1962Label(season: string, lang: CalendarLang): string { @@ -268,26 +282,12 @@ export function colorLabel1962(colorKey: string, lang: CalendarLang): string { 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' }, - bibleFallbackNote: { - en: 'Translation taken from the Douay-Rheims Bible, since no translated proper is published for this section. Wording will differ from authoritative missals.', - de: 'Übersetzung stammt aus der Allioli-Bibel, da für diesen Abschnitt kein übersetztes Proprium veröffentlicht ist. Wortlaut weicht von maßgeblichen Messbüchern ab.' - } + propers: { en: 'Mass propers', de: 'Messproprium', la: 'Propria Missæ' } } as const; export function t1962(key: keyof typeof ui1962, lang: CalendarLang): string { @@ -296,19 +296,18 @@ export function t1962(key: keyof typeof ui1962, lang: CalendarLang): string { } 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' } + Introitus: { en: 'Introit', de: 'Introitus', la: 'Introitus' }, + Oratio: { en: 'Collect', de: 'Kollekte', la: 'Oratio' }, + Lectio: { en: 'Epistle', de: 'Epistel', la: 'Lectio' }, + Graduale: { en: 'Gradual', de: 'Graduale', la: 'Graduale' }, + GradualeF: { en: 'Alleluia', de: 'Alleluja', la: 'Alleluia' }, + Tractus: { en: 'Tract', de: 'Tractus', la: 'Tractus' }, + Sequentia: { en: 'Sequence', de: 'Sequenz', la: 'Sequentia' }, + Evangelium: { en: 'Gospel', de: 'Evangelium', la: 'Evangelium' }, + Offertorium: { en: 'Offertory', de: 'Offertorium', la: 'Offertorium' }, + Secreta: { en: 'Secret', de: 'Stillgebet', la: 'Secreta' }, + Communio: { en: 'Communion', de: 'Kommunion', la: 'Communio' }, + Postcommunio: { en: 'Postcommunion', de: 'Schlussgebet', la: 'Postcommunio' } }; export function properLabel(key: string, lang: CalendarLang): string {