i18n(faith): migrate streak components, BibleModal, katechese notices

Adds streak/angelus and Bible-modal keys to the faith dictionary, plus
the three-fragment "this catechesis is only available in German" notice
used by both katechese pages. Pluralization for day/days handled by two
explicit keys (day_singular/day_plural) chosen at the call site —
Latin's "Dies" is invariant so both keys hold the same string.

StreakCounter and AngelusStreakCounter collapse their per-component
labels objects into direct t.foo lookups; the rosary page's BibleModal
call site now passes the typed `lang` derived value (was data.lang as
plain string, didn't satisfy the tightened FaithLang prop type).

BibleModal isn't actually used in Latin context, but the dict requires
every key in every locale, so reasonable Latin equivalents got filled
in for completeness.
This commit is contained in:
2026-05-01 13:10:29 +02:00
parent 28b96a8dc0
commit 3dcb5c7f2b
10 changed files with 108 additions and 50 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "homepage", "name": "homepage",
"version": "1.55.0", "version": "1.55.1",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -5,6 +5,7 @@ import StreakAura from '$lib/components/faith/StreakAura.svelte';
import Coffee from '@lucide/svelte/icons/coffee'; import Coffee from '@lucide/svelte/icons/coffee';
import Sun from '@lucide/svelte/icons/sun'; import Sun from '@lucide/svelte/icons/sun';
import Moon from '@lucide/svelte/icons/moon'; import Moon from '@lucide/svelte/icons/moon';
import { m, type FaithLang } from '$lib/js/faithI18n';
import { tick, onMount } from 'svelte'; import { tick, onMount } from 'svelte';
let burst = $state(false); let burst = $state(false);
@@ -13,14 +14,13 @@ let selectedSlot = $state<TimeSlot>('morning');
interface Props { interface Props {
streakData?: { streak: number; lastComplete: string | null; todayPrayed: number; todayDate: string | null } | null; streakData?: { streak: number; lastComplete: string | null; todayPrayed: number; todayDate: string | null } | null;
lang?: 'de' | 'en' | 'la'; lang?: FaithLang;
isLoggedIn?: boolean; isLoggedIn?: boolean;
} }
let { streakData = null, lang = 'de', isLoggedIn = false }: Props = $props(); let { streakData = null, lang = 'de', isLoggedIn = false }: Props = $props();
const isEnglish = $derived(lang === 'en'); const t = $derived(m[lang]);
const isLatin = $derived(lang === 'la');
// Display values: store when available, SSR fallback // Display values: store when available, SSR fallback
const displayStreak = $derived(store?.streak ?? streakData?.streak ?? 0); const displayStreak = $derived(store?.streak ?? streakData?.streak ?? 0);
@@ -52,20 +52,12 @@ const slots: { key: TimeSlot; icon: typeof Coffee; color: string }[] = [
{ key: 'evening', icon: Moon, color: 'var(--nord15)' } { key: 'evening', icon: Moon, color: 'var(--nord15)' }
]; ];
const labels = $derived({ const dayLabel = $derived(displayStreak === 1 && !showFraction ? t.day_singular : t.day_plural);
days: isLatin ? 'Dies' : isEnglish ? (displayStreak === 1 && !showFraction ? 'Day' : 'Days') : (displayStreak === 1 && !showFraction ? 'Tag' : 'Tage'),
pray: isLatin ? 'Oravi' : isEnglish ? 'Prayed' : 'Gebetet',
done: isLatin ? 'Hodie completa' : isEnglish ? 'Done today' : 'Heute fertig',
morning: isLatin ? 'Mane' : isEnglish ? 'Morning' : 'Morgens',
noon: isLatin ? 'Meridie' : isEnglish ? 'Noon' : 'Mittags',
evening: isLatin ? 'Vespere' : isEnglish ? 'Evening' : 'Abends',
ariaLabel: isLatin ? 'Orationem notatam fac' : isEnglish ? 'Mark prayer as prayed' : 'Gebet als gebetet markieren'
});
const slotLabels: Record<TimeSlot, string> = $derived({ const slotLabels: Record<TimeSlot, string> = $derived({
morning: labels.morning, morning: t.morning,
noon: labels.noon, noon: t.noon,
evening: labels.evening evening: t.evening
}); });
function isSlotPrayed(slot: TimeSlot): boolean { function isSlotPrayed(slot: TimeSlot): boolean {
@@ -105,7 +97,7 @@ async function pray() {
{displayStreak}{#if showFraction}<span class="fraction"><span class="num">{partialCount}</span><span class="slash">/</span><span class="den">3</span></span>{/if} {displayStreak}{#if showFraction}<span class="fraction"><span class="num">{partialCount}</span><span class="slash">/</span><span class="den">3</span></span>{/if}
</span> </span>
</StreakAura> </StreakAura>
<span class="streak-label">{labels.days}</span> <span class="streak-label">{dayLabel}</span>
</div> </div>
<div class="prayer-controls"> <div class="prayer-controls">
@@ -132,12 +124,12 @@ async function pray() {
class="pray-button" class="pray-button"
type="submit" type="submit"
disabled={todayComplete || selectedSlotPrayed} disabled={todayComplete || selectedSlotPrayed}
aria-label={labels.ariaLabel} aria-label={t.mark_prayer}
> >
{#if todayComplete} {#if todayComplete}
{labels.done} {t.done_today}
{:else} {:else}
{labels.pray} {t.prayed}
{/if} {/if}
</button> </button>
</form> </form>
+7 -6
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { VerseData } from '$lib/data/mysteryDescriptions'; import type { VerseData } from '$lib/data/mysteryDescriptions';
import { m, type FaithLang } from '$lib/js/faithI18n';
let { let {
reference = '', reference = '',
@@ -11,11 +12,11 @@
reference?: string, reference?: string,
title?: string, title?: string,
verseData?: VerseData | null, verseData?: VerseData | null,
lang?: string, lang?: FaithLang,
onClose: () => void onClose: () => void
} = $props(); } = $props();
const isEnglish = $derived(lang === 'en'); const t = $derived(m[lang]);
// svelte-ignore state_referenced_locally // svelte-ignore state_referenced_locally
let book: string = $state(verseData?.book || ''); let book: string = $state(verseData?.book || '');
@@ -25,7 +26,7 @@
let verses: Array<{ verse: number; text: string }> = $state(verseData?.verses || []); let verses: Array<{ verse: number; text: string }> = $state(verseData?.verses || []);
let loading = $state(false); let loading = $state(false);
// svelte-ignore state_referenced_locally // svelte-ignore state_referenced_locally
let error = $state(verseData ? '' : (lang === 'en' ? 'No verse data available' : 'Keine Versdaten verfügbar')); let error = $state(verseData ? '' : m[lang].no_verse_data);
function handleBackdropClick(event: MouseEvent) { function handleBackdropClick(event: MouseEvent) {
if (event.target === event.currentTarget) { if (event.target === event.currentTarget) {
@@ -57,7 +58,7 @@
{/if} {/if}
<p class="modal-reference">{reference}</p> <p class="modal-reference">{reference}</p>
</div> </div>
<button class="close-button" onclick={onClose} aria-label={isEnglish ? 'Close' : 'Schliessen'}> <button class="close-button" onclick={onClose} aria-label={t.close}>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line> <line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line>
@@ -67,7 +68,7 @@
<div class="modal-body"> <div class="modal-body">
{#if loading} {#if loading}
<p class="loading">{isEnglish ? 'Loading...' : 'Lädt...'}</p> <p class="loading">{t.loading}</p>
{:else if error} {:else if error}
<p class="error">{error}</p> <p class="error">{error}</p>
{:else if verses.length > 0} {:else if verses.length > 0}
@@ -80,7 +81,7 @@
{/each} {/each}
</div> </div>
{:else} {:else}
<p class="error">{isEnglish ? 'No verses found' : 'Keine Verse gefunden'}</p> <p class="error">{t.no_verses_found}</p>
{/if} {/if}
</div> </div>
</div> </div>
+8 -14
View File
@@ -2,6 +2,7 @@
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { getRosaryStreak } from '$lib/stores/rosaryStreak.svelte'; import { getRosaryStreak } from '$lib/stores/rosaryStreak.svelte';
import StreakAura from '$lib/components/faith/StreakAura.svelte'; import StreakAura from '$lib/components/faith/StreakAura.svelte';
import { m, type FaithLang } from '$lib/js/faithI18n';
import { tick, onMount } from 'svelte'; import { tick, onMount } from 'svelte';
let burst = $state(false); let burst = $state(false);
@@ -9,26 +10,19 @@ let streak = $state<ReturnType<typeof getRosaryStreak> | null>(null);
interface Props { interface Props {
streakData?: { length: number; lastPrayed: string | null } | null; streakData?: { length: number; lastPrayed: string | null } | null;
lang?: 'de' | 'en' | 'la'; lang?: FaithLang;
isLoggedIn?: boolean; isLoggedIn?: boolean;
} }
let { streakData = null, lang = 'de', isLoggedIn = false }: Props = $props(); let { streakData = null, lang = 'de', isLoggedIn = false }: Props = $props();
const isEnglish = $derived(lang === 'en'); const t = $derived(m[lang]);
// Derive display values: use store when available, fall back to server data for SSR // Derive display values: use store when available, fall back to server data for SSR
let displayLength = $derived(streak?.length ?? streakData?.length ?? 0); let displayLength = $derived(streak?.length ?? streakData?.length ?? 0);
let prayedToday = $derived(streak?.prayedToday ?? (streakData?.lastPrayed === new Date().toISOString().split('T')[0])); let prayedToday = $derived(streak?.prayedToday ?? (streakData?.lastPrayed === new Date().toISOString().split('T')[0]));
// Labels need to come after displayLength since they depend on it const dayLabel = $derived(displayLength === 1 ? t.day_singular : t.day_plural);
const isLatin = $derived(lang === 'la');
const labels = $derived({
days: isLatin ? (displayLength === 1 ? 'Dies' : 'Dies') : isEnglish ? (displayLength === 1 ? 'Day' : 'Days') : (displayLength === 1 ? 'Tag' : 'Tage'),
prayed: isLatin ? 'Oravi' : isEnglish ? 'Prayed' : 'Gebetet',
prayedToday: isLatin ? 'Hodie oravi' : isEnglish ? 'Prayed today' : 'Heute gebetet',
ariaLabel: isLatin ? 'Orationem notatam fac' : isEnglish ? 'Mark prayer as prayed' : 'Gebet als gebetet markieren'
});
// Initialize store on mount (client-side only) // Initialize store on mount (client-side only)
// Init with server data BEFORE assigning to streak, so displayLength // Init with server data BEFORE assigning to streak, so displayLength
@@ -50,7 +44,7 @@ async function pray() {
<div class="streak-container" class:no-js-hidden={!isLoggedIn}> <div class="streak-container" class:no-js-hidden={!isLoggedIn}>
<div class="streak-display"> <div class="streak-display">
<StreakAura value={displayLength} {burst} /> <StreakAura value={displayLength} {burst} />
<span class="streak-label">{labels.days}</span> <span class="streak-label">{dayLabel}</span>
</div> </div>
<form method="POST" action="?/pray" onsubmit={(e) => { e.preventDefault(); pray(); } <form method="POST" action="?/pray" onsubmit={(e) => { e.preventDefault(); pray(); }
}> }>
@@ -58,12 +52,12 @@ async function pray() {
class="streak-button" class="streak-button"
type="submit" type="submit"
disabled={prayedToday} disabled={prayedToday}
aria-label={labels.ariaLabel} aria-label={t.mark_prayer}
> >
{#if prayedToday} {#if prayedToday}
{labels.prayedToday} {t.prayed_today}
{:else} {:else}
{labels.prayed} {t.prayed}
{/if} {/if}
</button> </button>
</form> </form>
+23 -1
View File
@@ -19,5 +19,27 @@ export const de = {
alex_pick: "Alex' Wahl", alex_pick: "Alex' Wahl",
arguments_title: 'Apologetik', arguments_title: 'Apologetik',
evidences: 'Belege', evidences: 'Belege',
positive_case: 'Positives' positive_case: 'Positives',
// Streak counters (rosary, angelus)
day_singular: 'Tag',
day_plural: 'Tage',
prayed: 'Gebetet',
prayed_today: 'Heute gebetet',
mark_prayer: 'Gebet als gebetet markieren',
done_today: 'Heute fertig',
morning: 'Morgens',
noon: 'Mittags',
evening: 'Abends',
// Bible modal
close: 'Schliessen',
loading: 'Lädt…',
no_verses_found: 'Keine Verse gefunden',
no_verse_data: 'Keine Versdaten verfügbar',
// Language-availability notice (catechesis is German-only)
only_german_pre: 'Diese Katechese ist nur auf ',
only_german_link: 'Deutsch',
only_german_post: ' verfügbar.'
} as const; } as const;
+23 -1
View File
@@ -19,5 +19,27 @@ export const en = {
alex_pick: "Alex's pick", alex_pick: "Alex's pick",
arguments_title: 'Arguments', arguments_title: 'Arguments',
evidences: 'Evidences', evidences: 'Evidences',
positive_case: 'Positive case' positive_case: 'Positive case',
// Streak counters
day_singular: 'Day',
day_plural: 'Days',
prayed: 'Prayed',
prayed_today: 'Prayed today',
mark_prayer: 'Mark prayer as prayed',
done_today: 'Done today',
morning: 'Morning',
noon: 'Noon',
evening: 'Evening',
// Bible modal
close: 'Close',
loading: 'Loading…',
no_verses_found: 'No verses found',
no_verse_data: 'No verse data available',
// Language-availability notice (catechesis is German-only)
only_german_pre: 'This catechesis is only available in ',
only_german_link: 'German',
only_german_post: '.'
} as const satisfies Record<keyof typeof de, string>; } as const satisfies Record<keyof typeof de, string>;
+23 -1
View File
@@ -19,5 +19,27 @@ export const la = {
alex_pick: 'Alexandri delectus', alex_pick: 'Alexandri delectus',
arguments_title: 'Apologia', arguments_title: 'Apologia',
evidences: 'Argumenta', evidences: 'Argumenta',
positive_case: 'Argumenta pro' positive_case: 'Argumenta pro',
// Streak counters — Latin "Dies" is invariant
day_singular: 'Dies',
day_plural: 'Dies',
prayed: 'Oravi',
prayed_today: 'Hodie oravi',
mark_prayer: 'Orationem notatam fac',
done_today: 'Hodie completa',
morning: 'Mane',
noon: 'Meridie',
evening: 'Vespere',
// Bible modal — not used in Latin context but the dict requires every key
close: 'Claude',
loading: 'Carico…',
no_verses_found: 'Versus non inventi',
no_verse_data: 'Nulli versus praesto',
// Language-availability notice (catechesis is German-only)
only_german_pre: 'Haec catechesis tantum in ',
only_german_link: 'lingua Germanica',
only_german_post: ' praesto est.'
} as const satisfies Record<keyof typeof de, string>; } as const satisfies Record<keyof typeof de, string>;
@@ -1156,5 +1156,5 @@ h1 {
<!-- Bible citation modal --> <!-- Bible citation modal -->
{#if showModal} {#if showModal}
<BibleModal reference={selectedReference} title={selectedTitle} verseData={selectedVerseData} lang={data.lang} onClose={() => showModal = false} /> <BibleModal reference={selectedReference} title={selectedTitle} verseData={selectedVerseData} {lang} onClose={() => showModal = false} />
{/if} {/if}
@@ -1,9 +1,12 @@
<script> <script>
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import LinksGrid from '$lib/components/LinksGrid.svelte'; import LinksGrid from '$lib/components/LinksGrid.svelte';
import { m } from '$lib/js/faithI18n';
/** @typedef {import('$lib/js/faithI18n').FaithLang} FaithLang */
let { data } = $props(); let { data } = $props();
const isGerman = $derived(data.lang === 'de'); const lang = $derived(/** @type {FaithLang} */ (data.lang));
const isLatin = $derived(data.lang === 'la'); const t = $derived(m[lang]);
const isGerman = $derived(lang === 'de');
</script> </script>
<svelte:head> <svelte:head>
@@ -49,7 +52,7 @@
<h1>Katechese</h1> <h1>Katechese</h1>
{#if !isGerman} {#if !isGerman}
<p class="lang-notice">{isLatin ? 'Haec catechesis tantum in ' : 'This catechesis is only available in '}<a href={resolve('/glaube/katechese')}>{isLatin ? 'lingua Germanica' : 'German'}</a>{isLatin ? ' praesto est.' : '.'}</p> <p class="lang-notice">{t.only_german_pre}<a href={resolve('/glaube/katechese')}>{t.only_german_link}</a>{t.only_german_post}</p>
{/if} {/if}
<p> <p>
Aufgearbeitete Lehrinhalte aus dem Glaubenskurs von P. Martin Ramm FSSP. Aufgearbeitete Lehrinhalte aus dem Glaubenskurs von P. Martin Ramm FSSP.
@@ -5,10 +5,12 @@
import ArrowLeft from '@lucide/svelte/icons/arrow-left'; import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import { page } from '$app/state'; import { page } from '$app/state';
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte'; import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
import { m, langFromFaithSlug } from '$lib/js/faithI18n';
/** @type {number | string | null} */ /** @type {number | string | null} */
let expanded = $state(null); let expanded = $state(null);
const isGerman = $derived(page.url.pathname.startsWith('/glaube')); const lang = $derived(langFromFaithSlug(page.url.pathname.split('/')[1]));
const isLatin = $derived(page.url.pathname.startsWith('/fides')); const t = $derived(m[lang]);
const isGerman = $derived(lang === 'de');
/** @param {number | string} id */ /** @param {number | string} id */
function toggle(id) { function toggle(id) {
@@ -93,7 +95,7 @@
</header> </header>
{#if !isGerman} {#if !isGerman}
<p class="lang-notice">{isLatin ? 'Haec catechesis tantum in ' : 'This catechesis is only available in '}<a href={resolve('/glaube/katechese/zehn-gebote')}>{isLatin ? 'lingua Germanica' : 'German'}</a>{isLatin ? ' praesto est.' : '.'}</p> <p class="lang-notice">{t.only_german_pre}<a href={resolve('/glaube/katechese/zehn-gebote')}>{t.only_german_link}</a>{t.only_german_post}</p>
{/if} {/if}
<section id="ursprung"> <section id="ursprung">