diff --git a/package.json b/package.json
index 46eaa116..2c2d8196 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "homepage",
- "version": "1.31.4",
+ "version": "1.32.0",
"private": true,
"type": "module",
"scripts": {
@@ -49,6 +49,7 @@
"@huggingface/transformers": "^4.0.1",
"@lucide/svelte": "^1.7.0",
"@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3",
+ "@romcal/calendar.general-roman": "3.0.0-dev.125",
"@sveltejs/adapter-node": "^5.5.4",
"@tauri-apps/plugin-geolocation": "^2.3.2",
"barcode-detector": "^3.1.2",
@@ -59,6 +60,7 @@
"leaflet": "^1.9.4",
"mongoose": "^9.4.1",
"node-cron": "^4.2.1",
+ "romcal": "3.0.0-dev.125",
"sharp": "^0.34.5",
"web-haptics": "^0.0.6"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1ba5fc78..72ca79fe 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -20,6 +20,9 @@ importers:
'@nicolo-ribaudo/chokidar-2':
specifier: 2.1.8-no-fsevents.3
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))
'@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)))
@@ -50,6 +53,9 @@ importers:
node-cron:
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)
sharp:
specifier: ^0.34.5
version: 0.34.5
@@ -174,6 +180,10 @@ packages:
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
+ '@babel/runtime@7.29.2':
+ resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
+ engines: {node: '>=6.9.0'}
+
'@borewit/text-codec@0.2.1':
resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==}
@@ -885,6 +895,12 @@ packages:
cpu: [x64]
os: [win32]
+ '@romcal/calendar.general-roman@3.0.0-dev.125':
+ resolution: {integrity: sha512-6E4D9zfGqkz7NlJTv38v8++tZv234F1Z2jXlyzOithy92ZzZJhOMgUcmw0MOLUbn24lqz3477/VlFdLvRtzQBA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ romcal: 3.0.0-dev.125
+
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@@ -1404,6 +1420,14 @@ packages:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
+ i18next@25.10.10:
+ resolution: {integrity: sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==}
+ peerDependencies:
+ typescript: ^5 || ^6
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -1757,6 +1781,10 @@ 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==}
+ engines: {node: '>=18.0.0'}
+
sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
@@ -2158,6 +2186,8 @@ snapshots:
'@babel/runtime@7.28.4': {}
+ '@babel/runtime@7.29.2': {}
+
'@borewit/text-codec@0.2.1': {}
'@csstools/color-helpers@5.1.0': {}
@@ -2651,6 +2681,10 @@ 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))':
+ dependencies:
+ romcal: 3.0.0-dev.125(typescript@6.0.2)
+
'@sec-ant/readable-stream@0.4.1': {}
'@standard-schema/spec@1.0.0': {}
@@ -3145,6 +3179,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ i18next@25.10.10(typescript@6.0.2):
+ dependencies:
+ '@babel/runtime': 7.29.2
+ optionalDependencies:
+ typescript: 6.0.2
+
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
@@ -3503,6 +3543,12 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.60.1
fsevents: 2.3.3
+ romcal@3.0.0-dev.125(typescript@6.0.2):
+ dependencies:
+ i18next: 25.10.10(typescript@6.0.2)
+ transitivePeerDependencies:
+ - typescript
+
sade@1.8.1:
dependencies:
mri: 1.2.0
diff --git a/src/params/calendarLang.ts b/src/params/calendarLang.ts
new file mode 100644
index 00000000..8deea065
--- /dev/null
+++ b/src/params/calendarLang.ts
@@ -0,0 +1,5 @@
+import type { ParamMatcher } from '@sveltejs/kit';
+
+export const match: ParamMatcher = (param) => {
+ return param === 'calendar' || param === 'kalender' || param === 'calendarium';
+};
diff --git a/src/routes/[faithLang=faithLang]/+layout.svelte b/src/routes/[faithLang=faithLang]/+layout.svelte
index 7c93250f..2c615205 100644
--- a/src/routes/[faithLang=faithLang]/+layout.svelte
+++ b/src/routes/[faithLang=faithLang]/+layout.svelte
@@ -12,6 +12,7 @@ const isLatin = $derived(data.lang === 'la');
const eastertide = isEastertide();
const prayersHref = $derived(isLatin ? '/fides/orationes' : `/${data.faithLang}/${isEnglish ? 'prayers' : 'gebete'}`);
const rosaryHref = $derived(`/${data.faithLang}/${isLatin ? 'rosarium' : isEnglish ? 'rosary' : 'rosenkranz'}`);
+const calendarHref = $derived(`/${data.faithLang}/${isLatin ? 'calendarium' : isEnglish ? 'calendar' : 'kalender'}`);
const angelusHref = $derived(eastertide
? `${prayersHref}/regina-caeli`
: `${prayersHref}/angelus`);
@@ -20,7 +21,8 @@ const angelusLabel = $derived(eastertide ? 'Regína Cæli' : 'Angelus');
const labels = $derived({
prayers: isLatin ? 'Orationes' : isEnglish ? 'Prayers' : 'Gebete',
rosary: isLatin ? 'Rosarium' : isEnglish ? 'Rosary' : 'Rosenkranz',
- catechesis: isEnglish ? 'Catechesis' : 'Katechese'
+ catechesis: isEnglish ? 'Catechesis' : 'Katechese',
+ calendar: isLatin ? 'Calendarium' : isEnglish ? 'Calendar' : 'Kalender'
});
const typedLang = $derived(/** @type {'de' | 'en'} */ (data.lang));
@@ -47,6 +49,7 @@ const prayersActive = $derived(isActive(prayersHref) && !isActive(angelusHref));
{angelusLabel}
{/if}
{labels.catechesis}
+ {labels.calendar}
{/snippet}
diff --git a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/+page.server.ts b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/+page.server.ts
new file mode 100644
index 00000000..5cff3164
--- /dev/null
+++ b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/+page.server.ts
@@ -0,0 +1,173 @@
+import { error, redirect } from '@sveltejs/kit';
+import type { PageServerLoad } from './$types';
+import { Romcal } from 'romcal';
+import {
+ GeneralRoman_De,
+ GeneralRoman_En,
+ GeneralRoman_La
+} from '@romcal/calendar.general-roman';
+import { expectedSlug, isValidRite, type CalendarLang, type Rite } from './calendarI18n';
+
+export interface CalendarDay {
+ iso: string;
+ id: string;
+ name: string;
+ rankName: string;
+ rank: string;
+ seasonNames: string[];
+ colorNames: string[];
+ colorKeys: string[];
+ psalterWeek: string | null;
+ sundayCycle: string | null;
+}
+
+const localeBundles = {
+ en: GeneralRoman_En,
+ de: GeneralRoman_De,
+ la: GeneralRoman_La
+};
+
+// Cache: lang -> Romcal instance
+const romcalByLang = new Map();
+function getRomcal(lang: CalendarLang): Romcal {
+ let r = romcalByLang.get(lang);
+ if (r) return r;
+ r = new Romcal({ localizedCalendar: localeBundles[lang] });
+ romcalByLang.set(lang, r);
+ return r;
+}
+
+// Cache: lang|year -> Map
+const yearCache = new Map>();
+
+async function getYear(lang: CalendarLang, year: number): Promise