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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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>;
|
||||
@@ -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>;
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user