feat: i18n navigation labels

Replace all hardcoded German strings in router.js (navItems labels,
aria-labels, skip-link, error/toast messages) with t() calls. Add a
locale-changed event listener that re-renders sidebar and bottom-nav
items on language switch.
This commit is contained in:
Ulas
2026-03-31 23:19:05 +02:00
parent fe70cbf979
commit 3aefca0a44
+50 -22
View File
@@ -5,7 +5,7 @@
*/
import { auth } from '/api.js';
import { initI18n, getLocale } from '/i18n.js';
import { initI18n, getLocale, t } from '/i18n.js';
// --------------------------------------------------------
// Routen-Definitionen
@@ -216,8 +216,8 @@ async function renderPage(route, previousPath = null) {
*/
function renderAppShell(container) {
container.innerHTML = `
<a href="#page-content" class="sr-only">Zum Inhalt springen</a>
<nav class="nav-sidebar" aria-label="Hauptnavigation">
<a href="#page-content" class="sr-only">${t('common.skipToContent')}</a>
<nav class="nav-sidebar" aria-label="${t('nav.main')}">
<div class="nav-sidebar__logo"><span>Oikos</span></div>
<div class="nav-sidebar__items" role="list">
${navItems().map(navItemHtml).join('')}
@@ -227,7 +227,7 @@ function renderAppShell(container) {
<main class="app-content" id="page-content" aria-live="polite">
</main>
<nav class="nav-bottom" aria-label="Navigation">
<nav class="nav-bottom" aria-label="${t('nav.navigation')}">
<div class="nav-bottom__dots" aria-hidden="true">
<span class="nav-bottom__dot nav-bottom__dot--active"></span>
<span class="nav-bottom__dot"></span>
@@ -286,15 +286,15 @@ function scrollNavToActive() {
function navItems() {
return [
{ path: '/', label: 'Übersicht', icon: 'layout-dashboard' },
{ path: '/tasks', label: 'Aufgaben', icon: 'check-square' },
{ path: '/calendar', label: 'Kalender', icon: 'calendar' },
{ path: '/meals', label: 'Essen', icon: 'utensils' },
{ path: '/shopping', label: 'Einkauf', icon: 'shopping-cart' },
{ path: '/notes', label: 'Pinnwand', icon: 'sticky-note' },
{ path: '/contacts', label: 'Kontakte', icon: 'book-user' },
{ path: '/budget', label: 'Budget', icon: 'wallet' },
{ path: '/settings', label: 'Einstellungen', icon: 'settings' },
{ path: '/', label: t('nav.dashboard'), icon: 'layout-dashboard' },
{ path: '/tasks', label: t('nav.tasks'), icon: 'check-square' },
{ path: '/calendar', label: t('nav.calendar'), icon: 'calendar' },
{ path: '/meals', label: t('nav.meals'), icon: 'utensils' },
{ path: '/shopping', label: t('nav.shopping'), icon: 'shopping-cart' },
{ path: '/notes', label: t('nav.notes'), icon: 'sticky-note' },
{ path: '/contacts', label: t('nav.contacts'), icon: 'book-user' },
{ path: '/budget', label: t('nav.budget'), icon: 'wallet' },
{ path: '/settings', label: t('nav.settings'), icon: 'settings' },
];
}
@@ -332,9 +332,9 @@ function updateNav(path) {
function renderError(container, err) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state__title">Etwas ist schiefgelaufen.</div>
<div class="empty-state__title">${t('common.errorOccurred')}</div>
<div class="empty-state__description">${err.message}</div>
<button class="btn btn--primary" id="error-reload-btn">Neu laden</button>
<button class="btn btn--primary" id="error-reload-btn">${t('common.reload')}</button>
</div>
`;
container.querySelector('#error-reload-btn')?.addEventListener('click', () => location.reload());
@@ -378,14 +378,14 @@ window.addEventListener('error', (e) => {
// Ressource-Ladefehler (z.B. fehlgeschlagenes Bild): ignorieren
if (e.target && e.target !== window) return;
console.error('[Oikos] Unbehandelter Fehler:', e.error ?? e.message);
showToast('Ein unerwarteter Fehler ist aufgetreten.', 'danger');
showToast(t('common.unexpectedError'), 'danger');
});
window.addEventListener('unhandledrejection', (e) => {
// Auth-Fehler werden bereits von auth:expired behandelt
if (e.reason?.status === 401) return;
console.error('[Oikos] Unbehandeltes Promise-Rejection:', e.reason);
const msg = e.reason?.message || 'Ein Fehler ist aufgetreten.';
const msg = e.reason?.message || t('common.errorGeneric');
showToast(msg, 'danger');
e.preventDefault(); // Konsolenfehler unterdrücken (bereits geloggt)
});
@@ -396,11 +396,7 @@ if ('serviceWorker' in navigator) {
if (e.data?.type === 'SW_UPDATED') {
// Modul-Cache leeren damit nächste Navigation frische Module lädt
moduleCache.clear();
showToast(
'Update verfügbar — Seite neu laden für die neueste Version.',
'default',
8000
);
showToast(t('common.updateAvailable'), 'default', 8000);
}
});
}
@@ -416,6 +412,38 @@ window.addEventListener('auth:expired', () => {
navigate('/login');
});
// Sprache geändert: Navigation neu rendern damit Labels aktualisiert werden
window.addEventListener('locale-changed', () => {
const navSidebarItems = document.querySelector('.nav-sidebar__items');
const navBottomPages = document.querySelectorAll('.nav-bottom__page');
const skipLink = document.querySelector('.sr-only[href="#page-content"]');
const navSidebar = document.querySelector('.nav-sidebar');
const navBottom = document.querySelector('.nav-bottom');
if (skipLink) skipLink.textContent = t('common.skipToContent');
if (navSidebar) navSidebar.setAttribute('aria-label', t('nav.main'));
if (navBottom) navBottom.setAttribute('aria-label', t('nav.navigation'));
if (navSidebarItems) {
navSidebarItems.innerHTML = navItems().map(navItemHtml).join('');
}
if (navBottomPages.length >= 2) {
navBottomPages[0].innerHTML = navItems().slice(0, 5).map(navItemHtml).join('');
navBottomPages[1].innerHTML = navItems().slice(5).map(navItemHtml).join('');
}
// Klick-Handler für neu gerenderte Nav-Links
document.querySelectorAll('[data-route]').forEach((el) => {
el.addEventListener('click', (e) => {
e.preventDefault();
navigate(el.dataset.route);
});
});
// Aktiven Zustand und Icons wiederherstellen
updateNav(currentPath);
});
// --------------------------------------------------------
// Virtuelle Tastatur: FAB ausblenden wenn Keyboard offen
// Erkennung via visualViewport — Höhe < 75% des Fensters = Keyboard aktiv.