feat(i18n): bootstrap faith namespace + migrate layout, homepage, apologetik

Three-locale faith dictionary lands at src/lib/i18n/faith/{de,en,la}.ts
with the same satisfies-based completeness enforcement we use for
fitness, cospend, and calendar. faithI18n.ts is the slim shim — exports
m, FaithLang, FaithKey, plus the URL-slug helpers (langFromFaithSlug,
faithSlugFromLang, prayersSlug, rosarySlug, calendarSlug, apologetikSlug)
needed because faith routes do bidirectional slug ↔ locale mapping that
the other namespaces don't.

[faithLang]/+layout.svelte and +page.svelte fully migrated. The
isEnglish/isLatin derived flag dance collapses into a single typed
`lang`; ten inline ternaries per file (display labels and slug
selection) become t.key lookups or slug-helper calls. The "DE" badge
condition for non-German faith locales tightened from
`isEnglish || isLatin` to `lang !== 'de'`. Apologetik latin-fallback
hops through the helpers instead of inline matchers.

Apologetik pages get the shared-label cut: all four pages (contra,
contra detail, pro, pro detail) now use t.objections, t.evidences,
t.alex_pick, t.objection_label, t.answered_by, t.voices_answering,
t.arguments_title, t.positive_case from the dict. Page-specific
marketing copy (the per-page heading/lede/eyebrow object literals)
stays inline — those strings live in exactly one place each, the
structure is already readable, and pulling them into a shared dict
would be noise.

Also: ImageUpload.svelte was the one stray cospend t() caller the
earlier codemod missed (it lives at lib/components/, outside the
codemod's --root scope). Now uses t.key with `as CospendLang` cast.
This commit is contained in:
2026-05-01 13:01:25 +02:00
parent 3347619816
commit 28b96a8dc0
12 changed files with 224 additions and 95 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.54.2",
"version": "1.55.0",
"private": true,
"type": "module",
"scripts": {
+11 -10
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { t } from '$lib/js/cospendI18n';
import { m, type CospendLang } from '$lib/js/cospendI18n';
let {
imagePreview = $bindable(''),
@@ -24,20 +24,21 @@
onimageRemoved?: () => void,
oncurrentImageRemoved?: () => void
}>();
const t = $derived(m[lang as CospendLang]);
const displayTitle = $derived(title ?? t('receipt_image', lang));
const displayTitle = $derived(title ?? t.receipt_image);
function handleImageChange(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) {
if (file.size > 5 * 1024 * 1024) {
onerror?.(t('file_too_large', lang));
onerror?.(t.file_too_large);
return;
}
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
onerror?.(t('invalid_image', lang));
onerror?.(t.invalid_image);
return;
}
@@ -70,10 +71,10 @@
{#if currentImage}
<div class="current-image">
<img src={currentImage} alt={t('receipt', lang)} class="receipt-preview" />
<img src={currentImage} alt={t.receipt} class="receipt-preview" />
<div class="image-actions">
<button type="button" class="btn-remove" onclick={removeCurrentImage}>
{t('remove_image', lang)}
{t.remove_image}
</button>
</div>
</div>
@@ -81,9 +82,9 @@
{#if imagePreview}
<div class="image-preview">
<img src={imagePreview} alt={t('receipt', lang)} />
<img src={imagePreview} alt={t.receipt} />
<button type="button" class="remove-image" onclick={removeImage}>
{t('remove_image', lang)}
{t.remove_image}
</button>
</div>
{:else}
@@ -95,7 +96,7 @@
<line x1="16" y1="5" x2="22" y2="5"/>
<line x1="19" y1="2" x2="19" y2="8"/>
</svg>
<p>{currentImage ? t('replace_image', lang) : t('upload_receipt', lang)}</p>
<p>{currentImage ? t.replace_image : t.upload_receipt}</p>
<small>JPEG, PNG, WebP (max 5MB)</small>
</div>
</label>
@@ -111,7 +112,7 @@
{/if}
{#if uploading}
<div class="upload-status">{t('uploading_image', lang)}</div>
<div class="upload-status">{t.uploading_image}</div>
{/if}
</div>
+23
View File
@@ -0,0 +1,23 @@
/** German faith UI strings — source of truth for the key set. */
export const de = {
title: 'Glaube',
description:
'Hier findet man einige Gebete und einen interaktiven Rosenkranz zum katholischen Glauben. Ein Fokus auf Latein und den alten Ritus wird zu bemerken sein.',
prayers: 'Gebete',
rosary: 'Rosenkranz',
apologetics: 'Apologetik',
calendar: 'Kalender',
catechesis: 'Katechese',
in_season: 'Zur Zeit',
// Apologetik
objections: 'Einwände',
voices_answering: 'Die antwortenden Stimmen',
objection_label: 'EINWAND',
answered_by: 'Beantwortet von',
alex_pick: "Alex' Wahl",
arguments_title: 'Apologetik',
evidences: 'Belege',
positive_case: 'Positives'
} as const;
+23
View File
@@ -0,0 +1,23 @@
import type { de } from './de';
export const en = {
title: 'Faith',
description:
'Here you will find some prayers and an interactive rosary for the Catholic faith. A focus on Latin and the older rite will be noticeable.',
prayers: 'Prayers',
rosary: 'Rosary',
apologetics: 'Apologetics',
calendar: 'Calendar',
catechesis: 'Catechesis',
in_season: 'In season',
// Apologetik
objections: 'Objections',
voices_answering: 'The voices that answer',
objection_label: 'OBJECTION',
answered_by: 'Answered by',
alex_pick: "Alex's pick",
arguments_title: 'Arguments',
evidences: 'Evidences',
positive_case: 'Positive case'
} as const satisfies Record<keyof typeof de, string>;
+23
View File
@@ -0,0 +1,23 @@
import type { de } from './de';
export const la = {
title: 'Fides',
description:
'Hic invenies orationes et rosarium interactivum fidei catholicae.',
prayers: 'Orationes',
rosary: 'Rosarium Vivum',
apologetics: 'Apologetica',
calendar: 'Calendarium',
catechesis: 'Catechesis',
in_season: 'Tempore',
// Apologetik
objections: 'Obiectiones',
voices_answering: 'Voces respondentes',
objection_label: 'OBIECTIO',
answered_by: 'Respondetur a',
alex_pick: 'Alexandri delectus',
arguments_title: 'Apologia',
evidences: 'Argumenta',
positive_case: 'Argumenta pro'
} as const satisfies Record<keyof typeof de, string>;
+72
View File
@@ -0,0 +1,72 @@
/**
* Faith route i18n — UI translations and slug mappings.
*
* Translation tables live per-locale in `$lib/i18n/faith/{de,en,la}.ts`.
* `de.ts` is the source of truth for the key set; `en.ts` and `la.ts` use
* `satisfies Record<keyof typeof de, string>` so any missing translation
* surfaces as a TypeScript error at build time.
*
* Faith routes get `lang` from layout server data (data.lang), derived from
* the [faithLang=faithLang] param matcher: glaube→de, faith→en, fides→la.
* Use `langFromFaithSlug(params.faithLang)` if you need it from the slug
* directly.
*/
import { de } from '$lib/i18n/faith/de';
import { en } from '$lib/i18n/faith/en';
import { la } from '$lib/i18n/faith/la';
/** All faith translations, keyed by locale. */
export const m = { de, en, la } as const;
export type FaithLang = keyof typeof m;
export type FaithKey = keyof typeof de;
/** Map a `[faithLang]` slug to the locale code. */
export function langFromFaithSlug(faithLang: string | null | undefined): FaithLang {
if (faithLang === 'faith') return 'en';
if (faithLang === 'fides') return 'la';
return 'de';
}
/** Reverse: locale → URL slug. */
export function faithSlugFromLang(lang: FaithLang): 'faith' | 'glaube' | 'fides' {
if (lang === 'en') return 'faith';
if (lang === 'la') return 'fides';
return 'glaube';
}
/** URL slug for the `[prayers=prayersLang]` segment per locale. */
export function prayersSlug(lang: FaithLang): 'prayers' | 'gebete' | 'orationes' {
if (lang === 'en') return 'prayers';
if (lang === 'la') return 'orationes';
return 'gebete';
}
/** URL slug for the `[rosary=rosaryLang]` segment per locale. */
export function rosarySlug(lang: FaithLang): 'rosary' | 'rosenkranz' | 'rosarium' {
if (lang === 'en') return 'rosary';
if (lang === 'la') return 'rosarium';
return 'rosenkranz';
}
/** URL slug for the `[calendar=calendarLang]` segment per locale. */
export function calendarSlug(lang: FaithLang): 'calendar' | 'kalender' | 'calendarium' {
if (lang === 'en') return 'calendar';
if (lang === 'la') return 'calendarium';
return 'kalender';
}
/** URL slug for the apologetik section per locale (no Latin variant — falls back to English). */
export function apologetikSlug(lang: FaithLang): 'apologetics' | 'apologetik' {
return lang === 'de' ? 'apologetik' : 'apologetics';
}
/**
* Get a translated string. Prefer `m[lang].key` directly in new code — this
* helper is kept for incremental migration and falls back to English then
* the key itself if the lookup misses.
*/
export function t(key: FaithKey, lang: FaithLang): string {
return m[lang][key] ?? m.en[key] ?? key;
}
+16 -21
View File
@@ -6,19 +6,22 @@ import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
import { isEastertide } from '$lib/js/easter.svelte';
import { m, prayersSlug as prayersSlugFor, rosarySlug, calendarSlug, apologetikSlug, faithSlugFromLang } from '$lib/js/faithI18n';
/** @typedef {import('$lib/js/faithI18n').FaithLang} FaithLang */
let { data, children } = $props();
const isEnglish = $derived(data.lang === 'en');
const isLatin = $derived(data.lang === 'la');
const lang = $derived(/** @type {FaithLang} */ (data.lang));
const t = $derived(m[lang]);
const eastertide = isEastertide();
const prayersSlug = $derived(isLatin ? 'orationes' : isEnglish ? 'prayers' : 'gebete');
const prayersSlug = $derived(prayersSlugFor(lang));
const prayersHref = $derived(resolve('/[faithLang=faithLang]/[prayers=prayersLang]', { faithLang: data.faithLang, prayers: prayersSlug }));
const rosaryHref = $derived(resolve('/[faithLang=faithLang]/[rosary=rosaryLang]', { faithLang: data.faithLang, rosary: isLatin ? 'rosarium' : isEnglish ? 'rosary' : 'rosenkranz' }));
const calendarHref = $derived(resolve('/[faithLang=faithLang]/[calendar=calendarLang]', { faithLang: data.faithLang, calendar: isLatin ? 'calendarium' : isEnglish ? 'calendar' : 'kalender' }));
const rosaryHref = $derived(resolve('/[faithLang=faithLang]/[rosary=rosaryLang]', { faithLang: data.faithLang, rosary: rosarySlug(lang) }));
const calendarHref = $derived(resolve('/[faithLang=faithLang]/[calendar=calendarLang]', { faithLang: data.faithLang, calendar: calendarSlug(lang) }));
// Apologetik has no Latin variant — Latin readers fall back to the English route.
const apologetikHref = $derived(
isLatin
? resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]', { faithLang: 'faith', apologetikSlug: 'apologetics' })
: resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]', { faithLang: data.faithLang, apologetikSlug: isEnglish ? 'apologetics' : 'apologetik' })
lang === 'la'
? resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]', { faithLang: faithSlugFromLang('en'), apologetikSlug: apologetikSlug('en') })
: resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]', { faithLang: data.faithLang, apologetikSlug: apologetikSlug(lang) })
);
const angelusHref = $derived(resolve('/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]', {
faithLang: data.faithLang,
@@ -27,14 +30,6 @@ const angelusHref = $derived(resolve('/[faithLang=faithLang]/[prayers=prayersLan
}));
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',
apologetics: isLatin ? 'Apologetica' : isEnglish ? 'Apologetics' : 'Apologetik',
calendar: isLatin ? 'Calendarium' : isEnglish ? 'Calendar' : 'Kalender'
});
const typedLang = $derived(/** @type {'de' | 'en'} */ (data.lang));
/** @param {string} path */
@@ -51,16 +46,16 @@ const prayersActive = $derived(isActive(prayersHref) && !isActive(angelusHref));
<Header>
{#snippet links()}
<ul class=site_header>
<li style="--active-fill: var(--nord12)"><a href={prayersHref} class:active={prayersActive} title={labels.prayers}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 640 512" fill="currentColor"><path d="M351.2 4.8c3.2-2 6.6-3.3 10-4.1c4.7-1 9.6-.9 14.1 .1c7.7 1.8 14.8 6.5 19.4 13.6L514.6 194.2c8.8 13.1 13.4 28.6 13.4 44.4v73.5c0 6.9 4.4 13 10.9 15.2l79.2 26.4C631.2 358 640 370.2 640 384v96c0 9.9-4.6 19.3-12.5 25.4s-18.1 8.1-27.7 5.5L431 465.9c-56-14.9-95-65.7-95-123.7V224c0-17.7 14.3-32 32-32s32 14.3 32 32v80c0 8.8 7.2 16 16 16s16-7.2 16-16V219.1c0-7-1.8-13.8-5.3-19.8L340.3 48.1c-1.7-3-2.9-6.1-3.6-9.3c-1-4.7-1-9.6 .1-14.1c1.9-8 6.8-15.2 14.3-19.9zm-62.4 0c7.5 4.6 12.4 11.9 14.3 19.9c1.1 4.6 1.2 9.4 .1 14.1c-.7 3.2-1.9 6.3-3.6 9.3L213.3 199.3c-3.5 6-5.3 12.9-5.3 19.8V304c0 8.8 7.2 16 16 16s16-7.2 16-16V224c0-17.7 14.3-32 32-32s32 14.3 32 32V342.3c0 58-39 108.7-95 123.7l-168.7 45c-9.6 2.6-19.9 .5-27.7-5.5S0 490 0 480V384c0-13.8 8.8-26 21.9-30.4l79.2-26.4c6.5-2.2 10.9-8.3 10.9-15.2V238.5c0-15.8 4.7-31.2 13.4-44.4L245.2 14.5c4.6-7.1 11.7-11.8 19.4-13.6c4.6-1.1 9.4-1.2 14.1-.1c3.5 .8 6.9 2.1 10 4.1z"/></svg><span class="nav-label">{labels.prayers}</span></a></li>
<li style="--active-fill: var(--nord11)"><a href={rosaryHref} class:active={isActive(rosaryHref)} title={labels.rosary}><svg class="nav-icon" width="16" height="16" viewBox="0 0 512 512" fill="currentColor"><path d="M241.251,145.056c-39.203-17.423-91.472,17.423-104.54,60.982c-13.068,43.558,8.712,117.608,65.337,143.742 c56.626,26.135,108.896-8.712,87.117-39.202c-74.049-8.712-121.963-87.117-100.184-126.319S280.453,162.479,241.251,145.056z"/><path d="M337.079,271.375c47.914-39.202,21.779-126.319-17.423-135.031c-39.202-8.712-56.626,13.068-26.135,39.202 c39.203,30.491-8.712,91.472-39.202,87.117C254.318,262.663,289.165,310.577,337.079,271.375z"/><path d="M254.318,119.788c43.558-17.423,74.049-9.579,100.184,16.556c13.068-39.202-30.491-104.54-108.896-113.252 S93.153,118.921,127.999,171.191C136.711,153.767,188.981,106.721,254.318,119.788z"/><path d="M110.576,245.24C36.527,262.663,28.87,335.248,45.239,380.27c17.423,47.914,4.356,82.761,26.135,91.472 c20.622,8.253,91.472,13.068,152.454,17.423c60.982,4.356,108.896-47.914,91.472-108.896 C141.067,410.761,110.576,284.442,110.576,245.24z"/><path d="M93.883,235.796c0,0,2.178-28.313,10.89-43.558c-4.356-4.356-8.712-21.779-8.712-21.779 s-4.356-19.601-4.356-34.846c-32.669-6.534-89.295,34.846-91.472,41.38c-2.178,6.534,10.889,80.583,39.202,82.761 C69.927,235.796,93.883,235.796,93.883,235.796z"/><path d="M489.533,175.546c-39.202-82.761-113.252-65.337-113.252-65.337s4.356,21.779-4.356,34.846 c43.558,47.914,13.067,146.643-24.681,158.265c130.675,56.626,159.712-58.081,164.068-75.504 C515.668,210.393,498.245,197.326,489.533,175.546z"/><path d="M454.108,332.076c-22.359,15.841-85.663,11.613-121.964-7.265c1.446,14.514-13.067,37.756-20.325,39.202 c27.59,11.621,53.725,62.436,7.265,116.161c18.878,18.87,95.828,4.356,140.842-24.689c7.325-4.722,18.869-52.27,21.779-79.851 C485.56,339.103,488.963,307.387,454.108,332.076z"/><path d="M257.227,213.294c-18.928,5.164-30.439-6.27-23.234-18.869c5.811-10.167,5.266-20.69-8.712-13.068 c-29.044,17.423-11.612,66.784,24.689,62.428c49.36-17.423,27.581-62.428,14.514-60.982 C251.417,184.249,273.196,208.938,257.227,213.294z"/></svg><span class="nav-label">{labels.rosary}</span></a></li>
<li style="--active-fill: var(--nord12)"><a href={prayersHref} class:active={prayersActive} title={t.prayers}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 640 512" fill="currentColor"><path d="M351.2 4.8c3.2-2 6.6-3.3 10-4.1c4.7-1 9.6-.9 14.1 .1c7.7 1.8 14.8 6.5 19.4 13.6L514.6 194.2c8.8 13.1 13.4 28.6 13.4 44.4v73.5c0 6.9 4.4 13 10.9 15.2l79.2 26.4C631.2 358 640 370.2 640 384v96c0 9.9-4.6 19.3-12.5 25.4s-18.1 8.1-27.7 5.5L431 465.9c-56-14.9-95-65.7-95-123.7V224c0-17.7 14.3-32 32-32s32 14.3 32 32v80c0 8.8 7.2 16 16 16s16-7.2 16-16V219.1c0-7-1.8-13.8-5.3-19.8L340.3 48.1c-1.7-3-2.9-6.1-3.6-9.3c-1-4.7-1-9.6 .1-14.1c1.9-8 6.8-15.2 14.3-19.9zm-62.4 0c7.5 4.6 12.4 11.9 14.3 19.9c1.1 4.6 1.2 9.4 .1 14.1c-.7 3.2-1.9 6.3-3.6 9.3L213.3 199.3c-3.5 6-5.3 12.9-5.3 19.8V304c0 8.8 7.2 16 16 16s16-7.2 16-16V224c0-17.7 14.3-32 32-32s32 14.3 32 32V342.3c0 58-39 108.7-95 123.7l-168.7 45c-9.6 2.6-19.9 .5-27.7-5.5S0 490 0 480V384c0-13.8 8.8-26 21.9-30.4l79.2-26.4c6.5-2.2 10.9-8.3 10.9-15.2V238.5c0-15.8 4.7-31.2 13.4-44.4L245.2 14.5c4.6-7.1 11.7-11.8 19.4-13.6c4.6-1.1 9.4-1.2 14.1-.1c3.5 .8 6.9 2.1 10 4.1z"/></svg><span class="nav-label">{t.prayers}</span></a></li>
<li style="--active-fill: var(--nord11)"><a href={rosaryHref} class:active={isActive(rosaryHref)} title={t.rosary}><svg class="nav-icon" width="16" height="16" viewBox="0 0 512 512" fill="currentColor"><path d="M241.251,145.056c-39.203-17.423-91.472,17.423-104.54,60.982c-13.068,43.558,8.712,117.608,65.337,143.742 c56.626,26.135,108.896-8.712,87.117-39.202c-74.049-8.712-121.963-87.117-100.184-126.319S280.453,162.479,241.251,145.056z"/><path d="M337.079,271.375c47.914-39.202,21.779-126.319-17.423-135.031c-39.202-8.712-56.626,13.068-26.135,39.202 c39.203,30.491-8.712,91.472-39.202,87.117C254.318,262.663,289.165,310.577,337.079,271.375z"/><path d="M254.318,119.788c43.558-17.423,74.049-9.579,100.184,16.556c13.068-39.202-30.491-104.54-108.896-113.252 S93.153,118.921,127.999,171.191C136.711,153.767,188.981,106.721,254.318,119.788z"/><path d="M110.576,245.24C36.527,262.663,28.87,335.248,45.239,380.27c17.423,47.914,4.356,82.761,26.135,91.472 c20.622,8.253,91.472,13.068,152.454,17.423c60.982,4.356,108.896-47.914,91.472-108.896 C141.067,410.761,110.576,284.442,110.576,245.24z"/><path d="M93.883,235.796c0,0,2.178-28.313,10.89-43.558c-4.356-4.356-8.712-21.779-8.712-21.779 s-4.356-19.601-4.356-34.846c-32.669-6.534-89.295,34.846-91.472,41.38c-2.178,6.534,10.889,80.583,39.202,82.761 C69.927,235.796,93.883,235.796,93.883,235.796z"/><path d="M489.533,175.546c-39.202-82.761-113.252-65.337-113.252-65.337s4.356,21.779-4.356,34.846 c43.558,47.914,13.067,146.643-24.681,158.265c130.675,56.626,159.712-58.081,164.068-75.504 C515.668,210.393,498.245,197.326,489.533,175.546z"/><path d="M454.108,332.076c-22.359,15.841-85.663,11.613-121.964-7.265c1.446,14.514-13.067,37.756-20.325,39.202 c27.59,11.621,53.725,62.436,7.265,116.161c18.878,18.87,95.828,4.356,140.842-24.689c7.325-4.722,18.869-52.27,21.779-79.851 C485.56,339.103,488.963,307.387,454.108,332.076z"/><path d="M257.227,213.294c-18.928,5.164-30.439-6.27-23.234-18.869c5.811-10.167,5.266-20.69-8.712-13.068 c-29.044,17.423-11.612,66.784,24.689,62.428c49.36-17.423,27.581-62.428,14.514-60.982 C251.417,184.249,273.196,208.938,257.227,213.294z"/></svg><span class="nav-label">{t.rosary}</span></a></li>
{#if eastertide}
<li style="--active-fill: var(--nord14)"><a href={angelusHref} class:active={isActive(angelusHref)} title={angelusLabel}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-10 -274 532 548" fill="currentColor"><path d="M256-168c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zM6-63l122 199-56 70c-5 7-8 14-8 23 0 19 16 35 36 35h312c20 0 36-16 36-35 0-9-3-16-8-23l-56-70L507-63c3-6 5-13 5-20 0-20-16-37-37-37-7 0-14 2-20 6l-17 12c-13 8-30 6-40-4l-35-35c-7-7-17-11-27-11s-20 4-27 11l-30 30c-13 13-33 13-46 0l-30-30c-7-7-17-11-27-11s-20 4-27 11l-34 34c-11 11-28 13-41 4l-17-11c-6-4-13-6-20-6-20 0-37 17-37 37 0 7 2 14 6 20z"/></svg><span class="nav-label">{angelusLabel}</span></a></li>
{:else}
<li style="--active-fill: var(--nord14)"><a href={angelusHref} class:active={isActive(angelusHref)} title={angelusLabel}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="6 -274 564 548" fill="currentColor"><path d="M392-162c-4-10-9-18-15-26 5-4 7-8 7-12 0-18-43-32-96-32s-96 14-96 32c0 4 3 8 7 12-6 8-11 16-15 26-15-11-24-24-24-38 0-35 57-64 128-64s128 29 128 64c0 14-9 27-24 38zm-104-22c35 0 64 29 64 64s-29 64-64 64-64-29-64-64 29-64 64-64zM82 159c3-22-3-48-20-64C34 68 16 30 16-12v-64c0-42 34-76 76-76 23 0 44 10 59 27l65 78c-21 16-37 40-43 67l-43 195c-4 17-2 34 5 49h-21c-26 0-46-24-42-50l10-55zm364 56L403 20c-6-27-21-51-42-67l64-77c15-18 36-28 59-28 42 0 76 34 76 76v64c0 42-18 80-46 107-17 16-23 42-20 64l10 56c4 26-16 49-42 49h-20c6-15 8-32 4-49zM220 31c7-32 35-55 68-55s61 23 68 55l43 194c5 20-11 39-31 39H208c-21 0-36-19-31-39l43-194z"/></svg><span class="nav-label">{angelusLabel}</span></a></li>
{/if}
<li style="--active-fill: var(--nord13)"><a href={resolve('/[faithLang=faithLang]/katechese', { faithLang: data.faithLang })} class:active={isActive(`/${data.faithLang}/katechese`)} title={labels.catechesis}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M16 12h2"/><path d="M16 8h2"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/><path d="M6 12h2"/><path d="M6 8h2"/></svg><span class="nav-label">{labels.catechesis}</span></a></li>
<li style="--active-fill: var(--nord10)"><a href={apologetikHref} class:active={isActive(apologetikHref)} title={labels.apologetics}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="M7 21h10"/><path d="M12 3v18"/><path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/></svg><span class="nav-label">{labels.apologetics}</span></a></li>
<li style="--active-fill: var(--nord15)"><a href={calendarHref} class:active={isActive(calendarHref)} title={labels.calendar}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></svg><span class="nav-label">{labels.calendar}</span></a></li>
<li style="--active-fill: var(--nord13)"><a href={resolve('/[faithLang=faithLang]/katechese', { faithLang: data.faithLang })} class:active={isActive(`/${data.faithLang}/katechese`)} title={t.catechesis}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M16 12h2"/><path d="M16 8h2"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/><path d="M6 12h2"/><path d="M6 8h2"/></svg><span class="nav-label">{t.catechesis}</span></a></li>
<li style="--active-fill: var(--nord10)"><a href={apologetikHref} class:active={isActive(apologetikHref)} title={t.apologetics}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="M7 21h10"/><path d="M12 3v18"/><path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/></svg><span class="nav-label">{t.apologetics}</span></a></li>
<li style="--active-fill: var(--nord15)"><a href={calendarHref} class:active={isActive(calendarHref)} title={t.calendar}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></svg><span class="nav-label">{t.calendar}</span></a></li>
</ul>
{/snippet}
+21 -31
View File
@@ -2,37 +2,27 @@
import { resolve } from '$app/paths';
import LinksGrid from '$lib/components/LinksGrid.svelte';
import { isEastertide } from '$lib/js/easter.svelte';
import { m, prayersSlug as prayersSlugFor, rosarySlug, calendarSlug, apologetikSlug, faithSlugFromLang } from '$lib/js/faithI18n';
/** @typedef {import('$lib/js/faithI18n').FaithLang} FaithLang */
let { data } = $props();
const isEnglish = $derived(data.lang === 'en');
const isLatin = $derived(data.lang === 'la');
const prayersPath = $derived(isLatin ? 'orationes' : isEnglish ? 'prayers' : 'gebete');
const rosaryPath = $derived(isLatin ? 'rosarium' : isEnglish ? 'rosary' : 'rosenkranz');
const calendarPath = $derived(isLatin ? 'calendarium' : isEnglish ? 'calendar' : 'kalender');
const lang = $derived(/** @type {FaithLang} */ (data.lang));
const t = $derived(m[lang]);
const prayersPath = $derived(prayersSlugFor(lang));
const rosaryPath = $derived(rosarySlug(lang));
const calendarPath = $derived(calendarSlug(lang));
// Apologetik has no Latin variant — Latin readers fall back to English.
const apologetikHref = $derived(
isLatin
? resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]', { faithLang: 'faith', apologetikSlug: 'apologetics' })
: resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]', { faithLang: data.faithLang, apologetikSlug: isEnglish ? 'apologetics' : 'apologetik' })
lang === 'la'
? resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]', { faithLang: faithSlugFromLang('en'), apologetikSlug: apologetikSlug('en') })
: resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]', { faithLang: data.faithLang, apologetikSlug: apologetikSlug(lang) })
);
const eastertide = isEastertide();
const labels = $derived({
title: isLatin ? 'Fides' : isEnglish ? 'Faith' : 'Glaube',
description: isLatin
? 'Hic invenies orationes et rosarium interactivum fidei catholicae.'
: isEnglish
? 'Here you will find some prayers and an interactive rosary for the Catholic faith. A focus on Latin and the older rite will be noticeable.'
: 'Hier findet man einige Gebete und einen interaktiven Rosenkranz zum katholischen Glauben. Ein Fokus auf Latein und den alten Ritus wird zu bemerken sein.',
prayers: isLatin ? 'Orationes' : isEnglish ? 'Prayers' : 'Gebete',
rosary: isLatin ? 'Rosarium Vivum' : isEnglish ? 'Rosary' : 'Rosenkranz',
apologetics: isLatin ? 'Apologetica' : isEnglish ? 'Apologetics' : 'Apologetik',
calendar: isLatin ? 'Calendarium' : isEnglish ? 'Calendar' : 'Kalender'
});
</script>
<svelte:head>
<title>{labels.title} - Bocken</title>
<meta name="description" content={labels.description} />
<title>{t.title} - Bocken</title>
<meta name="description" content={t.description} />
</svelte:head>
<style>
h1{
@@ -80,15 +70,15 @@
</style>
<h1>{labels.title}</h1>
<h1>{t.title}</h1>
<p>
{labels.description}
{t.description}
</p>
<LinksGrid>
<a href={resolve('/[faithLang=faithLang]/[prayers=prayersLang]', { faithLang: data.faithLang, prayers: prayersPath })}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M351.2 4.8c3.2-2 6.6-3.3 10-4.1c4.7-1 9.6-.9 14.1 .1c7.7 1.8 14.8 6.5 19.4 13.6L514.6 194.2c8.8 13.1 13.4 28.6 13.4 44.4v73.5c0 6.9 4.4 13 10.9 15.2l79.2 26.4C631.2 358 640 370.2 640 384v96c0 9.9-4.6 19.3-12.5 25.4s-18.1 8.1-27.7 5.5L431 465.9c-56-14.9-95-65.7-95-123.7V224c0-17.7 14.3-32 32-32s32 14.3 32 32v80c0 8.8 7.2 16 16 16s16-7.2 16-16V219.1c0-7-1.8-13.8-5.3-19.8L340.3 48.1c-1.7-3-2.9-6.1-3.6-9.3c-1-4.7-1-9.6 .1-14.1c1.9-8 6.8-15.2 14.3-19.9zm-62.4 0c7.5 4.6 12.4 11.9 14.3 19.9c1.1 4.6 1.2 9.4 .1 14.1c-.7 3.2-1.9 6.3-3.6 9.3L213.3 199.3c-3.5 6-5.3 12.9-5.3 19.8V304c0 8.8 7.2 16 16 16s16-7.2 16-16V224c0-17.7 14.3-32 32-32s32 14.3 32 32V342.3c0 58-39 108.7-95 123.7l-168.7 45c-9.6 2.6-19.9 .5-27.7-5.5S0 490 0 480V384c0-13.8 8.8-26 21.9-30.4l79.2-26.4c6.5-2.2 10.9-8.3 10.9-15.2V238.5c0-15.8 4.7-31.2 13.4-44.4L245.2 14.5c4.6-7.1 11.7-11.8 19.4-13.6c4.6-1.1 9.4-1.2 14.1-.1c3.5 .8 6.9 2.1 10 4.1z"/></svg>
<h3>{labels.prayers}</h3>
<h3>{t.prayers}</h3>
</a>
<a href={resolve('/[faithLang=faithLang]/[rosary=rosaryLang]', { faithLang: data.faithLang, rosary: rosaryPath })}>
<svg viewBox="0 0 512 512">
@@ -116,11 +106,11 @@
C251.417,184.249,273.196,208.938,257.227,213.294z"/>
</g>
</svg>
<h3>{labels.rosary}</h3>
<h3>{t.rosary}</h3>
</a>
{#if eastertide}
<a href={resolve('/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]', { faithLang: data.faithLang, prayers: prayersPath, prayer: 'regina-caeli' })} class="regina-link">
<span class="easter-badge">{isLatin ? 'Tempore' : isEnglish ? 'In season' : 'Zur Zeit'}</span>
<span class="easter-badge">{t.in_season}</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -274 532 548"><path d="M256-168c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zM6-63l122 199-56 70c-5 7-8 14-8 23 0 19 16 35 36 35h312c20 0 36-16 36-35 0-9-3-16-8-23l-56-70L507-63c3-6 5-13 5-20 0-20-16-37-37-37-7 0-14 2-20 6l-17 12c-13 8-30 6-40-4l-35-35c-7-7-17-11-27-11s-20 4-27 11l-30 30c-13 13-33 13-46 0l-30-30c-7-7-17-11-27-11s-20 4-27 11l-34 34c-11 11-28 13-41 4l-17-11c-6-4-13-6-20-6-20 0-37 17-37 37 0 7 2 14 6 20z"/></svg>
<h3>Regína Cæli</h3>
</a>
@@ -131,16 +121,16 @@
</a>
{/if}
<a href={resolve('/[faithLang=faithLang]/katechese', { faithLang: data.faithLang })} class="katechese-link">
{#if isEnglish || isLatin}<span class="lang-badge">DE</span>{/if}
{#if lang !== 'de'}<span class="lang-badge">DE</span>{/if}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -226 532 506"><path d="M256-107v310l1-1c54-22 113-34 172-34h19v-320h-19c-42 0-84 8-123 25-17 7-34 14-50 20zm-25-79 25 10 25-10c47-20 97-30 148-30h35c27 0 48 22 48 48v352c0 27-21 48-48 48h-35c-51 0-101 10-148 30l-13 5c-8 3-16 3-24 0l-13-5c-47-20-97-30-148-30H48c-26 0-48-21-48-48v-352c0-26 22-48 48-48h35c51 0 101 10 148 30z"/></svg>
<h3>Katechese</h3>
</a>
<a href={apologetikHref}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M384 32H512c17.7 0 32 14.3 32 32s-14.3 32-32 32H398.4c-5.2 25.8-22.9 47.1-46.4 57.3V448H544c17.7 0 32 14.3 32 32s-14.3 32-32 32H320 96c-17.7 0-32-14.3-32-32s14.3-32 32-32H288V153.3c-23.5-10.3-41.2-31.6-46.4-57.3H128c-17.7 0-32-14.3-32-32s14.3-32 32-32H256c14.6-19.4 37.8-32 64-32s49.4 12.6 64 32zm55.6 288H584.4L512 195.8 439.6 320zM512 416c-62.9 0-115.2-34-126-78.9c-2.6-11 1-22.3 6.7-32.1l95.2-163.2c5-8.6 14.2-13.8 24.1-13.8s19.1 5.3 24.1 13.8l95.2 163.2c5.7 9.8 9.3 21.1 6.7 32.1C627.2 382 574.9 416 512 416zM126.8 195.8L54.4 320H199.3L126.8 195.8zM.9 337.1c-2.6-11 1-22.3 6.7-32.1l95.2-163.2c5-8.6 14.2-13.8 24.1-13.8s19.1 5.3 24.1 13.8l95.2 163.2c5.7 9.8 9.3 21.1 6.7 32.1C242 382 189.7 416 126.8 416S11.7 382 .9 337.1z"/></svg>
<h3>{labels.apologetics}</h3>
<h3>{t.apologetics}</h3>
</a>
<a href={resolve('/[faithLang=faithLang]/[calendar=calendarLang]', { faithLang: data.faithLang, calendar: calendarPath })}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M128 0c17.7 0 32 14.3 32 32V64H288V32c0-17.7 14.3-32 32-32s32 14.3 32 32V64h48c26.5 0 48 21.5 48 48v48H0V112C0 85.5 21.5 64 48 64H96V32c0-17.7 14.3-32 32-32zM0 192H448V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V192zm64 80v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm128 0v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H208c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H336zM64 400v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H208zm112 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H336c-8.8 0-16 7.2-16 16z"/></svg>
<h3>{labels.calendar}</h3>
<h3>{t.calendar}</h3>
</a>
</LinksGrid>
@@ -4,11 +4,15 @@
import CaseTabs from '$lib/components/faith/CaseTabs.svelte';
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
import { m, type FaithLang } from '$lib/js/faithI18n';
let { data } = $props();
const faithLang = $derived(data?.faithLang ?? 'faith');
const slug = $derived(faithLang === 'faith' ? 'apologetics' : 'apologetik');
const isLatin = $derived(data?.lang === 'la');
const isGerman = $derived(data?.lang === 'de');
const lang = $derived((data?.lang ?? 'en') as FaithLang);
const t = $derived(m[lang]);
const isLatin = $derived(lang === 'la');
const isGerman = $derived(lang === 'de');
const ARCHETYPES = $derived(data.archetypes);
const ARGUMENTS = $derived(data.args);
@@ -79,18 +83,10 @@
? 'Dreiundzwanzig Einwände, wie sie ein Atheist erheben mag, je in mehreren Stimmen beantwortet.'
: 'Twenty-three objections an atheist might raise, each answered in several voices.'
);
const tocLabel = $derived(
isLatin ? 'Obiectiones' : isGerman ? 'Einwände' : 'Objections'
);
const legendTitle = $derived(
isLatin ? 'Voces respondentes' : isGerman ? 'Die antwortenden Stimmen' : 'The voices that answer'
);
const objectionLabel = $derived(
isLatin ? 'OBIECTIO' : isGerman ? 'EINWAND' : 'OBJECTION'
);
const answeredByLabel = $derived(
isLatin ? 'Respondetur a' : isGerman ? 'Beantwortet von' : 'Answered by'
);
const tocLabel = $derived(t.objections);
const legendTitle = $derived(t.voices_answering);
const objectionLabel = $derived(t.objection_label);
const answeredByLabel = $derived(t.answered_by);
const filterLabels = $derived(
isLatin
? { filteringBy: 'Filtrum:', showAll: 'omnia ostendere' }
@@ -2,21 +2,21 @@
import { resolve } from '$app/paths';
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
import { m, type FaithLang } from '$lib/js/faithI18n';
let { data } = $props();
const faithLang = $derived(data?.faithLang ?? 'faith');
const slug = $derived(faithLang === 'faith' ? 'apologetics' : 'apologetik');
const isLatin = $derived(data?.lang === 'la');
const isGerman = $derived(data?.lang === 'de');
const lang = $derived((data?.lang ?? 'en') as FaithLang);
const t = $derived(m[lang]);
const isLatin = $derived(lang === 'la');
const isGerman = $derived(lang === 'de');
const arg = $derived(data.argument);
const ARCHETYPES = $derived(data.archetypes);
const alexPicks = $derived<string[]>(data.alexPicks ?? []);
const alexPickLabel = $derived(
isLatin ? 'Alexandri delectus' : isGerman ? "Alex' Wahl" : "Alex's pick"
);
const alexPickLabel = $derived(t.alex_pick);
const tocLabel = $derived(
isLatin ? 'Obiectiones' : isGerman ? 'Einwände' : 'Objections'
);
const tocLabel = $derived(t.objections);
const tocItems = $derived(
data.args.map((a) => ({
id: a.id,
@@ -88,7 +88,7 @@
</script>
<svelte:head>
<title>{arg.title} · {isLatin ? 'Apologia' : isGerman ? 'Apologetik' : 'Arguments'} · bocken.org</title>
<title>{arg.title} · {t.arguments_title} · bocken.org</title>
<meta name="description" content={arg.steel} />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
@@ -5,11 +5,15 @@
import CaseTabs from '$lib/components/faith/CaseTabs.svelte';
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
import { m, type FaithLang } from '$lib/js/faithI18n';
let { data } = $props();
const faithLang = $derived(data?.faithLang ?? 'faith');
const slug = $derived(faithLang === 'faith' ? 'apologetics' : 'apologetik');
const isLatin = $derived(data?.lang === 'la');
const isGerman = $derived(data?.lang === 'de');
const lang = $derived((data?.lang ?? 'en') as FaithLang);
const t = $derived(m[lang]);
const isLatin = $derived(lang === 'la');
const isGerman = $derived(lang === 'de');
const POS_VOICES = $derived(data.voices);
const POS_LAYERS = $derived(data.layers);
@@ -129,7 +133,7 @@
})
);
const tocLabel = $derived(
isLatin ? 'Argumenta' : isGerman ? 'Belege' : 'Evidences'
t.evidences
);
</script>
@@ -2,11 +2,15 @@
import { resolve } from '$app/paths';
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
import { m, type FaithLang } from '$lib/js/faithI18n';
let { data } = $props();
const faithLang = $derived(data?.faithLang ?? 'faith');
const slug = $derived(faithLang === 'faith' ? 'apologetics' : 'apologetik');
const isLatin = $derived(data?.lang === 'la');
const isGerman = $derived(data?.lang === 'de');
const lang = $derived((data?.lang ?? 'en') as FaithLang);
const t = $derived(m[lang]);
const isLatin = $derived(lang === 'la');
const isGerman = $derived(lang === 'de');
const arg = $derived(data.argument);
const POS_VOICES = $derived(data.voices);
const POS_LAYERS = $derived(data.layers);
@@ -20,9 +24,7 @@
? { natural: 'Übernatürlich', theism: 'Theismus', christianity: 'Christentum' }
: { natural: 'Supernatural', theism: 'Theism', christianity: 'Christianity' }
);
const tocLabel = $derived(
isLatin ? 'Argumenta' : isGerman ? 'Belege' : 'Evidences'
);
const tocLabel = $derived(t.evidences);
const tocItems = $derived(
POS_ARGUMENTS.map((a) => ({
id: a.id,
@@ -94,7 +96,7 @@
</script>
<svelte:head>
<title>{arg.title} · {isLatin ? 'Argumenta pro' : isGerman ? 'Positives' : 'Positive case'} · bocken.org</title>
<title>{arg.title} · {t.positive_case} · bocken.org</title>
<meta name="description" content={arg.claim} />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />