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:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.56.2",
|
||||
"version": "1.57.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>;
|
||||
@@ -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;
|
||||
@@ -34,33 +34,35 @@
|
||||
};
|
||||
});
|
||||
|
||||
import { m } from '$lib/js/commonI18n';
|
||||
const t = $derived(m[lang]);
|
||||
const isEnglish = $derived(lang === 'en');
|
||||
const labels = $derived({
|
||||
welcome: isEnglish ? 'Welcome to bocken.org' : 'Willkommen auf bocken.org',
|
||||
welcome: t.welcome,
|
||||
intro1: isEnglish
|
||||
? 'Hello, I\'m Alexander Bocken. On this website you\'ll find some software projects for friends, family, and myself. Everything is self-hosted at my home on a small mini-server (Arch, btw).'
|
||||
: 'Hallo, ich bin Alexander Bocken. Auf dieser Seite findest du einige Softwareprojekte für Freunde, Familie und mich. Alles ist selbst gehostet bei mir daheim auf einem kleinen Mini-Server (Arch, btw).',
|
||||
intro2: isEnglish
|
||||
? 'I recommend my continuously growing recipe collection. There you\'ll find many delicious recipes that I\'ve tried myself and constantly refine. You\'re also welcome to use my search engine or Jitsi instance for video conferences. Some things are hidden behind a login, others are publicly accessible. If you know a bit about programming, feel free to browse my Git repositories.'
|
||||
: 'Zu empfehlen ist meine stetig wachsende Rezeptsammlung. Dort findest du viele leckere Rezepte, die ich selbst ausprobiert habe und ständig weiterfeilsche. Zudem kannst du gerne meine Suchmaschine oder auch Jitsi-instanz für Videokonferenzen nutzen. Einiges ist hinter einem Login versteckt, anderes ist öffentlich zugänglich. Wer sich ein bisschen mit Programmieren auskennt, kann auch gerne in meinen Git-Repositories stöbern.',
|
||||
pages: isEnglish ? 'Pages' : 'Seiten',
|
||||
recipes: isEnglish ? 'Recipes' : 'Rezepte',
|
||||
pages: t.pages,
|
||||
recipes: t.recipes,
|
||||
git: 'Git',
|
||||
streaming: 'Streaming',
|
||||
familyPhotos: isEnglish ? 'Family Photos' : 'Familienbilder',
|
||||
familyPhotos: t.family_photos,
|
||||
cloud: 'Cloud',
|
||||
videoConferences: isEnglish ? 'Video Conferences' : 'Videokonferenzen',
|
||||
searchEngine: isEnglish ? 'Search Engine' : 'Suchmaschine',
|
||||
shopping: isEnglish ? 'Shopping' : 'Einkauf',
|
||||
familyTree: isEnglish ? 'Family Tree' : 'Stammbaum',
|
||||
faith: isEnglish ? 'Faith' : 'Glaube',
|
||||
videoConferences: t.video_conferences,
|
||||
searchEngine: t.search_engine,
|
||||
shopping: t.shopping,
|
||||
familyTree: t.family_tree,
|
||||
faith: t.faith,
|
||||
chat: 'Chat',
|
||||
transmission: 'Transmission',
|
||||
documents: isEnglish ? 'Documents' : 'Dokumente',
|
||||
audiobooksPodcasts: isEnglish ? 'Audiobooks & Podcasts' : 'Hörbücher & Podcasts',
|
||||
documents: t.documents,
|
||||
audiobooksPodcasts: t.audiobooks_podcasts,
|
||||
fitness: 'Fitness',
|
||||
nutrition: isEnglish ? 'Nutrition' : 'Ernährung',
|
||||
tasks: isEnglish ? 'Tasks' : 'Aufgaben'
|
||||
nutrition: t.nutrition,
|
||||
tasks: t.tasks
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user