Pre-compute romcal year maps on server boot for current + next civil year across en/de/la in each rite's default diocese, non-blocking so startup is unaffected. Also fixes several 1962-rite rendering bugs: commemorations previously leaked 1969-shape ids (e.g. andrew_apostle) next to proper 1962 sancti; station church names came through unresolved because RomcalConfig's internal i18next has no bundle loaded; season names arrived as raw keys (advent.season) for the same reason. All three now resolve locally via the shipped 1962 bundle with Latin as fallback. ClassIV ferias get a small dot on the grid.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.37.6",
|
||||
"version": "1.37.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Generated
+11
-11
@@ -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/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2))
|
||||
version: 3.0.0-dev.125(romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/e4530cf926486d229ae4a22c32e7d8b4a56e5077(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)))
|
||||
@@ -55,7 +55,7 @@ importers:
|
||||
version: 4.2.1
|
||||
romcal:
|
||||
specifier: github:AlexBocken/romcal#dev
|
||||
version: https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2)
|
||||
version: https://codeload.github.com/AlexBocken/romcal/tar.gz/e4530cf926486d229ae4a22c32e7d8b4a56e5077(typescript@6.0.2)
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
@@ -1421,8 +1421,8 @@ packages:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
i18next@26.0.4:
|
||||
resolution: {integrity: sha512-gXF7U9bfioXPLv7mw8Qt2nfO7vij5MyINvPgVv99pX3fL1Y01pw2mKBFrlYpRxRCl2wz3ISenj6VsMJT2isfuA==}
|
||||
i18next@26.0.6:
|
||||
resolution: {integrity: sha512-A4U6eCXodIbrhf8EarRurB9/4ebyaurH4+fu4gig9bqxmpSt+fCAFm/GpRQDcN1Xzu/LdFCx4nYHsnM1edIIbg==}
|
||||
peerDependencies:
|
||||
typescript: ^5 || ^6
|
||||
peerDependenciesMeta:
|
||||
@@ -1782,8 +1782,8 @@ packages:
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde:
|
||||
resolution: {tarball: https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde}
|
||||
romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/e4530cf926486d229ae4a22c32e7d8b4a56e5077:
|
||||
resolution: {tarball: https://codeload.github.com/AlexBocken/romcal/tar.gz/e4530cf926486d229ae4a22c32e7d8b4a56e5077}
|
||||
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/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2))':
|
||||
'@romcal/calendar.general-roman@3.0.0-dev.125(romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/e4530cf926486d229ae4a22c32e7d8b4a56e5077(typescript@6.0.2))':
|
||||
dependencies:
|
||||
romcal: https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2)
|
||||
romcal: https://codeload.github.com/AlexBocken/romcal/tar.gz/e4530cf926486d229ae4a22c32e7d8b4a56e5077(typescript@6.0.2)
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
|
||||
@@ -3181,7 +3181,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
i18next@26.0.4(typescript@6.0.2):
|
||||
i18next@26.0.6(typescript@6.0.2):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
optionalDependencies:
|
||||
@@ -3545,9 +3545,9 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.60.1
|
||||
fsevents: 2.3.3
|
||||
|
||||
romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2):
|
||||
romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/e4530cf926486d229ae4a22c32e7d8b4a56e5077(typescript@6.0.2):
|
||||
dependencies:
|
||||
i18next: 26.0.4(typescript@6.0.2)
|
||||
i18next: 26.0.6(typescript@6.0.2)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as auth from "./auth"
|
||||
import { initializeScheduler } from "./lib/server/scheduler"
|
||||
import { dbConnect } from "./utils/db"
|
||||
import { errorWithVerse, getRandomVerse } from "$lib/server/errorQuote"
|
||||
import { warmLiturgicalCache } from "$lib/server/liturgicalCalendar"
|
||||
|
||||
async function timing({ event, resolve }: Parameters<Handle>[0]) {
|
||||
const marks: Record<string, number> = {};
|
||||
@@ -43,6 +44,16 @@ await dbConnect().then(() => {
|
||||
// Don't crash the server - API routes will attempt reconnection
|
||||
});
|
||||
|
||||
// Warm liturgical calendar cache in the background — non-blocking so the
|
||||
// server starts accepting requests immediately; any request arriving before
|
||||
// warmup completes falls back to lazy computation (still correct, just cold).
|
||||
{
|
||||
const t0 = performance.now();
|
||||
warmLiturgicalCache()
|
||||
.then(() => console.log(`✅ Liturgical calendar cache warmed in ${Math.round(performance.now() - t0)}ms`))
|
||||
.catch((error) => console.error('⚠️ Liturgical calendar warmup failed:', error));
|
||||
}
|
||||
|
||||
async function authorization({ event, resolve }: Parameters<Handle>[0]) {
|
||||
const session = await event.locals.timing.measure('auth', () => event.locals.auth());
|
||||
event.locals.session = session;
|
||||
|
||||
@@ -39,10 +39,17 @@ export interface Rite1962Commem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Rite1962StationChurch {
|
||||
key: string;
|
||||
name: string;
|
||||
mass?: string;
|
||||
}
|
||||
|
||||
export interface Rite1962Detail {
|
||||
class: 1 | 2 | 3 | 4;
|
||||
kind: 'tempora' | 'sancti';
|
||||
commemorations: Rite1962Commem[];
|
||||
stationChurches?: Rite1962StationChurch[];
|
||||
octave?: {
|
||||
ofId: string;
|
||||
day: number;
|
||||
|
||||
@@ -28,6 +28,8 @@ import { createRequire } from 'node:module';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import {
|
||||
colorLabel1962,
|
||||
DEFAULT_DIOCESE_1962,
|
||||
DEFAULT_DIOCESE_1969,
|
||||
rank1962Label,
|
||||
season1962Label,
|
||||
type CalendarLang,
|
||||
@@ -153,16 +155,17 @@ function getRomcal1962(lang: CalendarLang, diocese: Diocese1962): Promise<Romcal
|
||||
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];
|
||||
// Do NOT pass `localizedCalendar` here: RomcalConfig's bundle-only branch
|
||||
// (`if (config?.localizedCalendar)` in rites/roman1969/src/models/config.ts)
|
||||
// pushes only the bundle's inputs and skips `particularCalendar` AND the
|
||||
// 1962 sanctoral layering entirely. The 1962 names live on the 1962
|
||||
// propers bundle and are supplied via `createI18n1962` + `resolveName1962`.
|
||||
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<string, unknown> = {
|
||||
i18next,
|
||||
localizedCalendar: base1969,
|
||||
scope: 'liturgical'
|
||||
};
|
||||
if (calendar) base.particularCalendar = calendar;
|
||||
@@ -203,8 +206,57 @@ function colorKeysFrom(c: LiturgicalDay1962): string[] {
|
||||
return c.colors ? [...c.colors] : [];
|
||||
}
|
||||
|
||||
function buildCommemorations(d: LiturgicalDay1962): Rite1962Commem[] {
|
||||
return (d.commemorations ?? []).map((c) => ({ id: c.id, name: c.name }));
|
||||
function buildCommemorations(
|
||||
d: LiturgicalDay1962,
|
||||
localBundle: RomcalBundle1962 | null,
|
||||
laBundle: RomcalBundle1962
|
||||
): Rite1962Commem[] {
|
||||
const out: Rite1962Commem[] = [];
|
||||
for (const c of d.commemorations ?? []) {
|
||||
const resolved = resolveCommemName(c.id, c.name, localBundle, laBundle);
|
||||
// Drop 1969 GRC leaks: the hardcoded `GeneralRoman` import in
|
||||
// RomcalConfig adds 1969-shaped ids (e.g. `andrew_apostle`) that are
|
||||
// not in either 1962 bundle. They show up as losers on the same date
|
||||
// as proper 1962 sancti — filter them out.
|
||||
if (resolved == null) continue;
|
||||
out.push({ id: c.id, name: resolved });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function resolveCommemName(
|
||||
id: string,
|
||||
raw: string | undefined,
|
||||
localBundle: RomcalBundle1962 | null,
|
||||
laBundle: RomcalBundle1962
|
||||
): string | null {
|
||||
const bundles = [localBundle, laBundle].filter(
|
||||
(b): b is RomcalBundle1962 => b != null
|
||||
);
|
||||
for (const b of bundles) {
|
||||
const v = b.i18n.names?.[id];
|
||||
if (v && v !== id) return v;
|
||||
}
|
||||
// Not in any 1962 bundle → treat as 1969 leak and drop.
|
||||
if (!raw || raw === id) return null;
|
||||
// Defensive: raw looks like an i18n key path (namespace/key) — also drop.
|
||||
if (/^[a-z][a-z0-9_]*[/.][a-z_]/.test(raw)) return null;
|
||||
return raw;
|
||||
}
|
||||
|
||||
function resolveStationName(
|
||||
key: string,
|
||||
localBundle: RomcalBundle1962 | null,
|
||||
laBundle: RomcalBundle1962
|
||||
): string {
|
||||
const bundles = [localBundle, laBundle].filter(
|
||||
(b): b is RomcalBundle1962 => b != null
|
||||
);
|
||||
for (const b of bundles) {
|
||||
const v = (b.i18n as { stationChurches?: Record<string, string> }).stationChurches?.[key];
|
||||
if (v && v !== key) return v;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
function sectionsFromBundle(
|
||||
@@ -292,9 +344,20 @@ function adaptDay1962(
|
||||
const detail: Rite1962Detail = {
|
||||
class: classOf,
|
||||
kind: d.kind1962 ?? 'tempora',
|
||||
commemorations: buildCommemorations(d),
|
||||
commemorations: buildCommemorations(d, localBundle, laBundle),
|
||||
propers
|
||||
};
|
||||
if (d.stationChurches && d.stationChurches.length > 0) {
|
||||
// Romcal's internal i18next has no resource bundles loaded (RomcalConfig
|
||||
// ignores the `i18next` we pass in input), so `s.name` arrives equal to
|
||||
// `s.key`. Resolve from the ships-with-bundle lookup table here, with
|
||||
// Latin as a fallback floor.
|
||||
detail.stationChurches = d.stationChurches.map((s) => ({
|
||||
key: s.key,
|
||||
name: resolveStationName(s.key, localBundle, laBundle),
|
||||
...(s.mass ? { mass: s.mass } : {})
|
||||
}));
|
||||
}
|
||||
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;
|
||||
@@ -307,7 +370,11 @@ function adaptDay1962(
|
||||
rankName: rank1962Label(classKey, lang),
|
||||
rank: classKey,
|
||||
seasonKey,
|
||||
seasonNames: d.seasonNames ? [...d.seasonNames] : [],
|
||||
// Romcal's own seasonNames come through unresolved ("advent.season") because
|
||||
// RomcalConfig's internal i18next has no resource bundle loaded — we pass
|
||||
// neither `localizedCalendar` nor a positional `locale`. Resolve here via
|
||||
// our own helper, which handles both 1962 CamelCase and 1969 SCREAMING_SNAKE.
|
||||
seasonNames: seasonKey ? [season1962Label(seasonKey, lang)] : [],
|
||||
colorNames,
|
||||
colorKeys,
|
||||
psalterWeek: null,
|
||||
@@ -340,9 +407,6 @@ export async function getYear1962(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -351,3 +415,22 @@ export function isoFor(year: number, month: number, day: number): string {
|
||||
const dd = String(day).padStart(2, '0');
|
||||
return `${year}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
// Pre-compute liturgical-calendar maps for the current and next civil year
|
||||
// across all supported languages for each rite's default diocese. Pages hit
|
||||
// year N and N+1 on every request (AdventI-rollover logic), so warming this
|
||||
// slice means the hot path — today's view in the default rite/diocese — is
|
||||
// cache-hot on the first request after boot. Non-default dioceses stay lazy.
|
||||
const WARMUP_LANGS: readonly CalendarLang[] = ['en', 'de', 'la'] as const;
|
||||
export async function warmLiturgicalCache(): Promise<void> {
|
||||
const year = new Date().getFullYear();
|
||||
const years = [year, year + 1];
|
||||
const tasks: Promise<unknown>[] = [];
|
||||
for (const y of years) {
|
||||
for (const lang of WARMUP_LANGS) {
|
||||
tasks.push(getYear(lang, DEFAULT_DIOCESE_1969, y));
|
||||
tasks.push(getYear1962(lang, DEFAULT_DIOCESE_1962, y));
|
||||
}
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
+42
@@ -250,6 +250,17 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if hero.rite1962?.stationChurches?.length}
|
||||
<div class="tc-stations">
|
||||
<span class="tc-stations-label" aria-hidden="true">✦</span>
|
||||
<span class="tc-stations-text">
|
||||
<span class="tc-stations-title">{t1962('stationChurch', lang)}:</span>
|
||||
{#each hero.rite1962.stationChurches as s, i (s.key + (s.mass ?? ''))}
|
||||
{#if i > 0}<span class="tc-stations-sep"> · </span>{/if}<span class="tc-station-name">{s.name}</span>{#if s.mass}<span class="tc-station-mass"> ({s.mass.replace(/_/g, ' ')})</span>{/if}
|
||||
{/each}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="tc-arrow" aria-hidden="true">→</span>
|
||||
</section>
|
||||
</a>
|
||||
@@ -660,6 +671,37 @@
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.tc-stations {
|
||||
margin-top: 0.9rem;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.55rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.tc-stations-label {
|
||||
font-size: 0.95rem;
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tc-stations-title {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.72rem;
|
||||
opacity: 0.85;
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
.tc-station-name {
|
||||
font-style: italic;
|
||||
}
|
||||
.tc-station-mass {
|
||||
opacity: 0.75;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.tc-stations-sep {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.tc-arrow {
|
||||
position: absolute;
|
||||
bottom: 1.1rem;
|
||||
|
||||
+32
-4
@@ -154,6 +154,21 @@
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if d.stationChurches?.length}
|
||||
<div class="stations">
|
||||
<h4>{t1962('stationChurch', lang)}</h4>
|
||||
<ul>
|
||||
{#each d.stationChurches as s (s.key + (s.mass ?? ''))}
|
||||
<li>
|
||||
<span class="station-name">{s.name}</span>
|
||||
{#if s.mass}
|
||||
<span class="station-mass">{s.mass.replace(/_/g, ' ')}</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if d.propers.length}
|
||||
<section class="propers">
|
||||
<h4>{t1962('propers', lang)}</h4>
|
||||
@@ -351,6 +366,7 @@
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.commems h4,
|
||||
.stations h4,
|
||||
.propers h4 {
|
||||
margin: 0.5rem 0 0.4rem;
|
||||
font-size: 0.72rem;
|
||||
@@ -359,10 +375,12 @@
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.commems {
|
||||
.commems,
|
||||
.stations {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.commems ul {
|
||||
.commems ul,
|
||||
.stations ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -370,7 +388,8 @@
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.commems li {
|
||||
.commems li,
|
||||
.stations li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -379,10 +398,19 @@
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.commem-name {
|
||||
.commem-name,
|
||||
.station-name {
|
||||
flex: 1 1 auto;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.station-name {
|
||||
font-style: italic;
|
||||
}
|
||||
.station-mass {
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: 0.78rem;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.propers {
|
||||
margin-top: 1rem;
|
||||
|
||||
@@ -68,5 +68,6 @@ export function rankDotSize(rank: string): number {
|
||||
if (rank === 'ClassII' || rank === 'FEAST' || rank === 'SUNDAY' || rank === 'HOLY_DAY_OF_OBLIGATION')
|
||||
return 4;
|
||||
if (rank === 'ClassIII' || rank === 'MEMORIAL') return 3;
|
||||
return 0; // don't render ferias/weekdays as dots
|
||||
if (rank === 'ClassIV') return 2;
|
||||
return 0; // 1969 weekdays/opt-memorials still skipped
|
||||
}
|
||||
|
||||
@@ -287,7 +287,8 @@ export const ui1962 = {
|
||||
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' },
|
||||
propers: { en: 'Mass propers', de: 'Messproprium', la: 'Propria Missæ' }
|
||||
propers: { en: 'Mass propers', de: 'Messproprium', la: 'Propria Missæ' },
|
||||
stationChurch: { en: 'Station church', de: 'Stationskirche', la: 'Statio' }
|
||||
} as const;
|
||||
|
||||
export function t1962(key: keyof typeof ui1962, lang: CalendarLang): string {
|
||||
|
||||
Reference in New Issue
Block a user