i18n(common): bootstrap shared namespace + migrate top-level UI

Add a per-locale common dictionary at src/lib/i18n/common/{de,en}.ts and
the shim src/lib/js/commonI18n.ts. Migrate inline lang ternaries on the
homepage (welcome/sections/links), OfflineSyncButton (all label
ternaries), DatePicker (today/select date), ErrorView (Error/Fehler
eyebrow), and UserHeader (login aria/title) to use the shared dict.

The long marketing intro paragraphs on the homepage stay inline since
they're one-shot content with no drift risk and don't benefit from
per-key extraction.

Bump site version to 1.57.0 (new namespace).
This commit is contained in:
2026-05-01 14:03:52 +02:00
parent 71f7322624
commit 79f4dbb101
9 changed files with 131 additions and 27 deletions
+6 -3
View File
@@ -2,7 +2,10 @@
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import Calendar from '@lucide/svelte/icons/calendar';
import { m } from '$lib/js/commonI18n';
/** @typedef {import('$lib/js/commonI18n').CommonLang} CommonLang */
let { value = $bindable(''), lang = 'en', min = '', max = '' } = $props();
const t = $derived(m[/** @type {CommonLang} */ (lang)]);
let open = $state(false);
/** @type {HTMLDivElement | null} */
@@ -35,8 +38,8 @@
const todayStr = new Date().toISOString().slice(0, 10);
const displayDate = $derived.by(() => {
if (!value) return lang === 'en' ? 'Select date' : 'Datum wählen';
if (value === todayStr) return lang === 'en' ? 'Today' : 'Heute';
if (!value) return t.select_date;
if (value === todayStr) return t.today;
const d = new Date(value + 'T12:00:00');
return d.toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' });
});
@@ -182,7 +185,7 @@
{#if value !== todayStr}
<button type="button" class="dp-today-btn" onclick={goToday}>
{lang === 'en' ? 'Today' : 'Heute'}
{t.today}
</button>
{/if}
</div>
+3 -1
View File
@@ -5,6 +5,7 @@
import SearchX from '@lucide/svelte/icons/search-x';
import TriangleAlert from '@lucide/svelte/icons/triangle-alert';
import CircleAlert from '@lucide/svelte/icons/circle-alert';
import { m } from '$lib/js/commonI18n';
interface BibleQuote {
text: string;
reference: string;
@@ -43,6 +44,7 @@
}
let Icon = $derived(icon ?? defaultIcon(status));
const t = $derived(m[isEnglish ? 'en' : 'de']);
let openQuote = $derived(isEnglish ? '\u201C' : '\u201E');
let closeQuote = $derived(isEnglish ? '\u201D' : '\u201C');
</script>
@@ -52,7 +54,7 @@
<header class="eyebrow">
<Icon size={14} strokeWidth={1.5} aria-hidden="true" />
<span class="eyebrow-label">
{isEnglish ? 'Error' : 'Fehler'}
{t.error_label}
</span>
</header>
+9 -7
View File
@@ -1,20 +1,22 @@
<script lang="ts">
import { onMount } from 'svelte';
import { pwaStore } from '$lib/stores/pwa.svelte';
import { m, type CommonLang } from '$lib/js/commonI18n';
let { lang = 'de' }: { lang?: string } = $props();
let showTooltip = $state(false);
let mounted = $state(false);
const t = $derived(m[lang as CommonLang]);
const labels = $derived({
syncForOffline: lang === 'en' ? 'Save for offline' : 'Offline speichern',
syncing: lang === 'en' ? 'Syncing...' : 'Synchronisiere...',
offlineReady: lang === 'en' ? 'Offline ready' : 'Offline bereit',
lastSync: lang === 'en' ? 'Last sync' : 'Letzte Sync',
recipes: lang === 'en' ? 'recipes' : 'Rezepte',
syncNow: lang === 'en' ? 'Sync now' : 'Jetzt synchronisieren',
clearData: lang === 'en' ? 'Clear offline data' : 'Offline-Daten löschen'
syncForOffline: t.sync_for_offline,
syncing: t.syncing,
offlineReady: t.offline_ready,
lastSync: t.last_sync,
recipes: t.recipes_word,
syncNow: t.sync_now,
clearData: t.clear_offline_data
});
onMount(async () => {
+4 -2
View File
@@ -4,8 +4,10 @@
import { page } from '$app/state';
import { browser } from '$app/environment';
import LogIn from '@lucide/svelte/icons/log-in';
import { m, type CommonLang } from '$lib/js/commonI18n';
let { user, recipeLang = 'rezepte', lang = 'de' } = $props();
const t = $derived(m[lang as CommonLang]);
function toggle_options(){
const el = document.querySelector("#options-wrap") as HTMLElement | null;
@@ -167,8 +169,8 @@
<a
class="entry login-link"
href={`${resolve('/login')}?callbackUrl=${encodeURIComponent(page.url.pathname + (browser ? page.url.search : ''))}`}
aria-label={lang === 'de' ? 'Anmelden' : 'Login'}
title={lang === 'de' ? 'Anmelden' : 'Login'}
aria-label={t.login}
title={t.login}
>
<LogIn size={18} />
</a>
+37
View File
@@ -0,0 +1,37 @@
/** German common UI strings — source of truth for the key set. */
export const de = {
// Auth / user header
login: 'Anmelden',
// (main) homepage
welcome: 'Willkommen auf bocken.org',
pages: 'Seiten',
recipes: 'Rezepte',
family_photos: 'Familienbilder',
video_conferences: 'Videokonferenzen',
search_engine: 'Suchmaschine',
shopping: 'Einkauf',
family_tree: 'Stammbaum',
faith: 'Glaube',
documents: 'Dokumente',
audiobooks_podcasts: 'Hörbücher & Podcasts',
nutrition: 'Ernährung',
tasks: 'Aufgaben',
// Offline sync button
sync_for_offline: 'Offline speichern',
syncing: 'Synchronisiere…',
offline_ready: 'Offline bereit',
last_sync: 'Letzte Sync',
recipes_word: 'Rezepte',
sync_now: 'Jetzt synchronisieren',
clear_offline_data: 'Offline-Daten löschen',
// Date picker
select_date: 'Datum wählen',
today: 'Heute',
// Error view
error_label: 'Fehler'
} as const;
+37
View File
@@ -0,0 +1,37 @@
import type { de } from './de';
export const en = {
// Auth / user header
login: 'Login',
// (main) homepage
welcome: 'Welcome to bocken.org',
pages: 'Pages',
recipes: 'Recipes',
family_photos: 'Family Photos',
video_conferences: 'Video Conferences',
search_engine: 'Search Engine',
shopping: 'Shopping',
family_tree: 'Family Tree',
faith: 'Faith',
documents: 'Documents',
audiobooks_podcasts: 'Audiobooks & Podcasts',
nutrition: 'Nutrition',
tasks: 'Tasks',
// Offline sync button
sync_for_offline: 'Save for offline',
syncing: 'Syncing…',
offline_ready: 'Offline ready',
last_sync: 'Last sync',
recipes_word: 'recipes',
sync_now: 'Sync now',
clear_offline_data: 'Clear offline data',
// Date picker
select_date: 'Select date',
today: 'Today',
// Error view
error_label: 'Error'
} as const satisfies Record<keyof typeof de, string>;
+19
View File
@@ -0,0 +1,19 @@
/**
* Shared/top-level UI strings used across the site (homepage, auth header,
* offline sync button, date picker, error view).
*
* Per-locale tables live in `$lib/i18n/common/{de,en}.ts`. Use
* `m[lang].key` (or `m[lang][expr]` for dynamic keys) directly:
*
* import { m } from '$lib/js/commonI18n';
* const t = $derived(m[lang]);
* ... t.login ...
*/
import { de } from '$lib/i18n/common/de';
import { en } from '$lib/i18n/common/en';
export const m = { de, en } as const;
export type CommonLang = keyof typeof m;
export type CommonKey = keyof typeof de;