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:
+50
-22
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { auth } from '/api.js';
|
import { auth } from '/api.js';
|
||||||
import { initI18n, getLocale } from '/i18n.js';
|
import { initI18n, getLocale, t } from '/i18n.js';
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Routen-Definitionen
|
// Routen-Definitionen
|
||||||
@@ -216,8 +216,8 @@ async function renderPage(route, previousPath = null) {
|
|||||||
*/
|
*/
|
||||||
function renderAppShell(container) {
|
function renderAppShell(container) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<a href="#page-content" class="sr-only">Zum Inhalt springen</a>
|
<a href="#page-content" class="sr-only">${t('common.skipToContent')}</a>
|
||||||
<nav class="nav-sidebar" aria-label="Hauptnavigation">
|
<nav class="nav-sidebar" aria-label="${t('nav.main')}">
|
||||||
<div class="nav-sidebar__logo"><span>Oikos</span></div>
|
<div class="nav-sidebar__logo"><span>Oikos</span></div>
|
||||||
<div class="nav-sidebar__items" role="list">
|
<div class="nav-sidebar__items" role="list">
|
||||||
${navItems().map(navItemHtml).join('')}
|
${navItems().map(navItemHtml).join('')}
|
||||||
@@ -227,7 +227,7 @@ function renderAppShell(container) {
|
|||||||
<main class="app-content" id="page-content" aria-live="polite">
|
<main class="app-content" id="page-content" aria-live="polite">
|
||||||
</main>
|
</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">
|
<div class="nav-bottom__dots" aria-hidden="true">
|
||||||
<span class="nav-bottom__dot nav-bottom__dot--active"></span>
|
<span class="nav-bottom__dot nav-bottom__dot--active"></span>
|
||||||
<span class="nav-bottom__dot"></span>
|
<span class="nav-bottom__dot"></span>
|
||||||
@@ -286,15 +286,15 @@ function scrollNavToActive() {
|
|||||||
|
|
||||||
function navItems() {
|
function navItems() {
|
||||||
return [
|
return [
|
||||||
{ path: '/', label: 'Übersicht', icon: 'layout-dashboard' },
|
{ path: '/', label: t('nav.dashboard'), icon: 'layout-dashboard' },
|
||||||
{ path: '/tasks', label: 'Aufgaben', icon: 'check-square' },
|
{ path: '/tasks', label: t('nav.tasks'), icon: 'check-square' },
|
||||||
{ path: '/calendar', label: 'Kalender', icon: 'calendar' },
|
{ path: '/calendar', label: t('nav.calendar'), icon: 'calendar' },
|
||||||
{ path: '/meals', label: 'Essen', icon: 'utensils' },
|
{ path: '/meals', label: t('nav.meals'), icon: 'utensils' },
|
||||||
{ path: '/shopping', label: 'Einkauf', icon: 'shopping-cart' },
|
{ path: '/shopping', label: t('nav.shopping'), icon: 'shopping-cart' },
|
||||||
{ path: '/notes', label: 'Pinnwand', icon: 'sticky-note' },
|
{ path: '/notes', label: t('nav.notes'), icon: 'sticky-note' },
|
||||||
{ path: '/contacts', label: 'Kontakte', icon: 'book-user' },
|
{ path: '/contacts', label: t('nav.contacts'), icon: 'book-user' },
|
||||||
{ path: '/budget', label: 'Budget', icon: 'wallet' },
|
{ path: '/budget', label: t('nav.budget'), icon: 'wallet' },
|
||||||
{ path: '/settings', label: 'Einstellungen', icon: 'settings' },
|
{ path: '/settings', label: t('nav.settings'), icon: 'settings' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,9 +332,9 @@ function updateNav(path) {
|
|||||||
function renderError(container, err) {
|
function renderError(container, err) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="empty-state">
|
<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>
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.querySelector('#error-reload-btn')?.addEventListener('click', () => location.reload());
|
container.querySelector('#error-reload-btn')?.addEventListener('click', () => location.reload());
|
||||||
@@ -378,14 +378,14 @@ window.addEventListener('error', (e) => {
|
|||||||
// Ressource-Ladefehler (z.B. fehlgeschlagenes Bild): ignorieren
|
// Ressource-Ladefehler (z.B. fehlgeschlagenes Bild): ignorieren
|
||||||
if (e.target && e.target !== window) return;
|
if (e.target && e.target !== window) return;
|
||||||
console.error('[Oikos] Unbehandelter Fehler:', e.error ?? e.message);
|
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) => {
|
window.addEventListener('unhandledrejection', (e) => {
|
||||||
// Auth-Fehler werden bereits von auth:expired behandelt
|
// Auth-Fehler werden bereits von auth:expired behandelt
|
||||||
if (e.reason?.status === 401) return;
|
if (e.reason?.status === 401) return;
|
||||||
console.error('[Oikos] Unbehandeltes Promise-Rejection:', e.reason);
|
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');
|
showToast(msg, 'danger');
|
||||||
e.preventDefault(); // Konsolenfehler unterdrücken (bereits geloggt)
|
e.preventDefault(); // Konsolenfehler unterdrücken (bereits geloggt)
|
||||||
});
|
});
|
||||||
@@ -396,11 +396,7 @@ if ('serviceWorker' in navigator) {
|
|||||||
if (e.data?.type === 'SW_UPDATED') {
|
if (e.data?.type === 'SW_UPDATED') {
|
||||||
// Modul-Cache leeren damit nächste Navigation frische Module lädt
|
// Modul-Cache leeren damit nächste Navigation frische Module lädt
|
||||||
moduleCache.clear();
|
moduleCache.clear();
|
||||||
showToast(
|
showToast(t('common.updateAvailable'), 'default', 8000);
|
||||||
'Update verfügbar — Seite neu laden für die neueste Version.',
|
|
||||||
'default',
|
|
||||||
8000
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -416,6 +412,38 @@ window.addEventListener('auth:expired', () => {
|
|||||||
navigate('/login');
|
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
|
// Virtuelle Tastatur: FAB ausblenden wenn Keyboard offen
|
||||||
// Erkennung via visualViewport — Höhe < 75% des Fensters = Keyboard aktiv.
|
// Erkennung via visualViewport — Höhe < 75% des Fensters = Keyboard aktiv.
|
||||||
|
|||||||
Reference in New Issue
Block a user