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
+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;
}