feat(i18n): wire user settings locale, add nav translations

- Sync svelte-i18n locale from userSettings.locale (backend) via $effect
- Persist language changes to backend via userSettings.updateGlobal
- Add nav/ locale module with navigation labels in 5 languages
- Replace 6 hardcoded German strings in app layout with $_() calls:
  "Alle Themes", "Menü", "Geschenke", "Profil", "Einstellungen", etc.
- Make baseNavItems reactive ($derived) so labels update on language change

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-02 17:08:11 +02:00
parent e152098829
commit bfe11d91ed
7 changed files with 114 additions and 20 deletions

View file

@ -15,20 +15,21 @@ const defaultLocale = 'de';
function registerLocale(lang: SupportedLocale) {
register(lang, async () => {
const [common, dashboard, credits, profile, subscription, todo, app_slider] = await Promise.all(
[
const [common, nav, dashboard, credits, profile, subscription, todo, app_slider] =
await Promise.all([
import(`./locales/common/${lang}.json`),
import(`./locales/nav/${lang}.json`),
import(`./locales/dashboard/${lang}.json`),
import(`./locales/credits/${lang}.json`),
import(`./locales/profile/${lang}.json`),
import(`./locales/subscription/${lang}.json`),
import(`./locales/todo/${lang}.json`),
import(`./locales/app_slider/${lang}.json`),
]
);
]);
return {
common: common.default,
nav: nav.default,
dashboard: dashboard.default,
credits: credits.default,
profile: profile.default,

View file

@ -0,0 +1,16 @@
{
"home": "Home",
"dashboard": "Dashboard",
"spiral": "Spiral",
"observatory": "Observatory",
"credits": "Credits",
"gifts": "Geschenke",
"api_keys": "API Keys",
"profile": "Profil",
"settings": "Einstellungen",
"tags": "Tags",
"admin": "Admin",
"all_themes": "Alle Themes",
"menu": "Menü",
"sign_out": "Abmelden"
}

View file

@ -0,0 +1,16 @@
{
"home": "Home",
"dashboard": "Dashboard",
"spiral": "Spiral",
"observatory": "Observatory",
"credits": "Credits",
"gifts": "Gifts",
"api_keys": "API Keys",
"profile": "Profile",
"settings": "Settings",
"tags": "Tags",
"admin": "Admin",
"all_themes": "All Themes",
"menu": "Menu",
"sign_out": "Sign out"
}

View file

@ -0,0 +1,16 @@
{
"home": "Inicio",
"dashboard": "Panel",
"spiral": "Spiral",
"observatory": "Observatorio",
"credits": "Créditos",
"gifts": "Regalos",
"api_keys": "API Keys",
"profile": "Perfil",
"settings": "Ajustes",
"tags": "Tags",
"admin": "Admin",
"all_themes": "Todos los temas",
"menu": "Menú",
"sign_out": "Cerrar sesión"
}

View file

@ -0,0 +1,16 @@
{
"home": "Accueil",
"dashboard": "Tableau de bord",
"spiral": "Spiral",
"observatory": "Observatoire",
"credits": "Crédits",
"gifts": "Cadeaux",
"api_keys": "Clés API",
"profile": "Profil",
"settings": "Paramètres",
"tags": "Tags",
"admin": "Admin",
"all_themes": "Tous les thèmes",
"menu": "Menu",
"sign_out": "Déconnexion"
}

View file

@ -0,0 +1,16 @@
{
"home": "Home",
"dashboard": "Dashboard",
"spiral": "Spiral",
"observatory": "Osservatorio",
"credits": "Crediti",
"gifts": "Regali",
"api_keys": "Chiavi API",
"profile": "Profilo",
"settings": "Impostazioni",
"tags": "Tag",
"admin": "Admin",
"all_themes": "Tutti i temi",
"menu": "Menu",
"sign_out": "Esci"
}

View file

@ -5,7 +5,7 @@
import { onDestroy, setContext } from 'svelte';
import KeyboardShortcutsModal from '$lib/components/KeyboardShortcutsModal.svelte';
import SessionWarning from '$lib/components/SessionWarning.svelte';
import { locale } from 'svelte-i18n';
import { locale, _ } from 'svelte-i18n';
import {
PillNavigation,
TagStrip,
@ -83,7 +83,7 @@
})),
{
id: 'all-themes',
label: 'Alle Themes',
label: $_('nav.all_themes'),
icon: 'palette',
onClick: () => goto('/themes'),
active: false,
@ -95,15 +95,28 @@
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
userSettings.updateGlobal({ locale: newLocale });
AppEvents.languageChanged(newLocale);
}
// Sync locale from user settings (backend) after login
$effect(() => {
if (userSettings.loaded && userSettings.locale) {
const settingsLocale = userSettings.locale;
if (supportedLocales.includes(settingsLocale as any) && settingsLocale !== $locale) {
setLocale(settingsLocale as any);
}
}
});
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// ── User / Guest awareness ──────────────────────────────
let userEmail = $derived(authStore.isAuthenticated ? authStore.user?.email || 'Menü' : '');
let userEmail = $derived(
authStore.isAuthenticated ? authStore.user?.email || $_('nav.menu') : ''
);
// ── Tags ────────────────────────────────────────────────
const allTags = useAllTags();
@ -124,30 +137,30 @@
});
// ── Navigation ──────────────────────────────────────────
const baseNavItems: PillNavItem[] = [
{ href: '/home', label: 'Home', icon: 'home' },
{ href: '/dashboard', label: 'Dashboard', icon: 'grid' },
{ href: '/spiral', label: 'Spiral', icon: 'spiral' },
{ href: '/observatory', label: 'Observatory', icon: 'eye' },
{ href: '/credits', label: 'Credits', icon: 'creditCard' },
{ href: '/gifts', label: 'Geschenke', icon: 'gift' },
{ href: '/api-keys', label: 'API Keys', icon: 'key' },
{ href: '/profile', label: 'Profil', icon: 'user' },
{ href: '/settings', label: 'Settings', icon: 'settings' },
let baseNavItems = $derived<PillNavItem[]>([
{ href: '/home', label: $_('nav.home'), icon: 'home' },
{ href: '/dashboard', label: $_('nav.dashboard'), icon: 'grid' },
{ href: '/spiral', label: $_('nav.spiral'), icon: 'spiral' },
{ href: '/observatory', label: $_('nav.observatory'), icon: 'eye' },
{ href: '/credits', label: $_('nav.credits'), icon: 'creditCard' },
{ href: '/gifts', label: $_('nav.gifts'), icon: 'gift' },
{ href: '/api-keys', label: $_('nav.api_keys'), icon: 'key' },
{ href: '/profile', label: $_('nav.profile'), icon: 'user' },
{ href: '/settings', label: $_('nav.settings'), icon: 'settings' },
{
href: '/',
label: 'Tags',
label: $_('nav.tags'),
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
];
]);
let isAdmin = $derived(authStore.user?.role === 'admin');
let navItems = $derived<PillNavItem[]>(
isAdmin ? [...baseNavItems, { href: '/admin', label: 'Admin', icon: 'shield' }] : baseNavItems
);
const navRoutes = navItems.map((item) => item.href);
let navRoutes = $derived(navItems.map((item) => item.href));
function handleKeydown(event: KeyboardEvent) {
const target = event.target as HTMLElement;