// Laufzeit-i18n-Engine für den UI-Sprachumschalter (ADR 0002). Übersetzt die // Bedienoberfläche (Buttons, Labels, Statusmeldungen) live, ohne Reload und // ohne Build-Tooling - passend zur file://-Anforderung aus ADR 0001. Die // Sprachwahl wird über EventMap.store (settings.language) persistiert. // // Setzt EventMap.i18n.STRINGS/SUPPORTED_LANGUAGES aus js/i18n/strings.js // voraus (muss davor geladen sein) sowie EventMap.store (muss vor dieser // Datei in index.html eingebunden sein, da resolveInitialLanguage() bereits // beim Init synchron auf die persistierten Settings zugreift). (function () { const { STRINGS, SUPPORTED_LANGUAGES } = EventMap.i18n; let currentLanguage = 'de'; const languageChangeListeners = new Set(); const warnedMissingKeys = new Set(); function lookup(lang, key) { let node = STRINGS[lang]; for (const part of key.split('.')) { if (node == null) return undefined; node = node[part]; } return typeof node === 'string' ? node : undefined; } function interpolate(str, params) { if (!params) return str; return str.replace(/\{(\w+)\}/g, (match, name) => (params[name] !== undefined ? params[name] : match)); } /** Übersetzt einen dotted Key (z.B. "menu.locate"). Fallback-Kette: aktuelle Sprache -> Deutsch -> Key selbst. */ function t(key, params) { let value = lookup(currentLanguage, key); if (value === undefined) { value = lookup('de', key); } if (value === undefined) { if (!warnedMissingKeys.has(key)) { warnedMissingKeys.add(key); console.warn(`EventMap.i18n: Übersetzungsschlüssel "${key}" nicht gefunden.`); } return key; } return interpolate(value, params); } function getLanguage() { return currentLanguage; } /** Übersetzt alle [data-i18n]/[data-i18n-aria-label]/[data-i18n-placeholder]/[data-i18n-title]-Knoten unterhalb von root. */ function applyStaticTranslations(root) { const scope = root || document; scope.querySelectorAll('[data-i18n]').forEach((node) => { node.textContent = t(node.getAttribute('data-i18n')); }); scope.querySelectorAll('[data-i18n-aria-label]').forEach((node) => { node.setAttribute('aria-label', t(node.getAttribute('data-i18n-aria-label'))); }); scope.querySelectorAll('[data-i18n-placeholder]').forEach((node) => { node.setAttribute('placeholder', t(node.getAttribute('data-i18n-placeholder'))); }); scope.querySelectorAll('[data-i18n-title]').forEach((node) => { node.setAttribute('title', t(node.getAttribute('data-i18n-title'))); }); document.documentElement.lang = currentLanguage; } /** Registriert einen Listener, der bei jedem Sprachwechsel aufgerufen wird. Gibt eine Unsubscribe-Funktion zurück. */ function onLanguageChange(fn) { languageChangeListeners.add(fn); return () => languageChangeListeners.delete(fn); } function notifyLanguageChange() { for (const fn of languageChangeListeners) { fn(currentLanguage); } } /** * Ermittelt die initiale Sprache: persistierte Einstellung falls vorhanden, * sonst Browsersprache (navigator.languages), sonst Deutsch als Fallback. */ function resolveInitialLanguage() { const settings = EventMap.store.getSettings(); if (settings.language && SUPPORTED_LANGUAGES.includes(settings.language)) { return settings.language; } const browserLanguages = navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language]; for (const lang of browserLanguages) { const prefix = (lang || '').slice(0, 2).toLowerCase(); if (SUPPORTED_LANGUAGES.includes(prefix)) { return prefix; } } return 'de'; } /** Wechselt die Anzeigesprache: persistiert, übersetzt statisches HTML neu und benachrichtigt Listener. */ function setLanguage(lang) { if (!SUPPORTED_LANGUAGES.includes(lang) || lang === currentLanguage) { return; } currentLanguage = lang; EventMap.store.updateSettings({ language: lang }); applyStaticTranslations(document); notifyLanguageChange(); // Stößt bei allen Store-Subscribern (Liste, Filter-Panel, ...) einen // Re-Render an, ohne dass sich der eigentliche Datenzustand ändert - // siehe ADR 0002, Abschnitt "Re-Render-Pfad". EventMap.store.setState({}); } /** Ermittelt die Startsprache und übersetzt das initiale statische HTML. Wird als Erstes in main.js:bootstrap() aufgerufen. */ function init() { currentLanguage = resolveInitialLanguage(); applyStaticTranslations(document); } EventMap.i18n.t = t; EventMap.i18n.getLanguage = getLanguage; EventMap.i18n.setLanguage = setLanguage; EventMap.i18n.onLanguageChange = onLanguageChange; EventMap.i18n.applyStaticTranslations = applyStaticTranslations; EventMap.i18n.resolveInitialLanguage = resolveInitialLanguage; EventMap.i18n.init = init; })();