feat: add i18n module (public/i18n.js)

This commit is contained in:
Ulas
2026-03-31 21:41:20 +02:00
parent 1087bc4c10
commit 3bec77db3b
2 changed files with 97 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
/**
* i18n — Internationalisierung / Übersetzungsmodul
* Bietet t(), initI18n(), setLocale(), getLocale(), getSupportedLocales(),
* formatDate(), formatTime() für die gesamte App.
* Dependencies: none (vanilla JS, Fetch API, Intl API)
*/
const SUPPORTED_LOCALES = ['de', 'en'];
const DEFAULT_LOCALE = 'de';
const STORAGE_KEY = 'oikos-locale';
let currentLocale = DEFAULT_LOCALE;
let translations = {};
let fallbackTranslations = {};
/** Resolve locale: manual override > navigator.language > default */
function resolveLocale() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored && SUPPORTED_LOCALES.includes(stored)) return stored;
const browserLocales = navigator.languages || [navigator.language];
for (const tag of browserLocales) {
const base = tag.split('-')[0].toLowerCase();
if (SUPPORTED_LOCALES.includes(base)) return base;
}
return DEFAULT_LOCALE;
}
/** Lade eine Locale-JSON-Datei */
async function loadLocale(locale) {
const resp = await fetch(`/locales/${locale}.json`);
if (!resp.ok) throw new Error(`Failed to load locale: ${locale}`);
return resp.json();
}
/** Initialisierung — einmal beim App-Start aufrufen */
export async function initI18n() {
currentLocale = resolveLocale();
fallbackTranslations = await loadLocale(DEFAULT_LOCALE);
if (currentLocale !== DEFAULT_LOCALE) {
translations = await loadLocale(currentLocale);
} else {
translations = fallbackTranslations;
}
document.documentElement.lang = currentLocale;
}
/** Sprache wechseln — löst 'locale-changed' Event aus */
export async function setLocale(locale) {
if (!SUPPORTED_LOCALES.includes(locale)) return;
localStorage.setItem(STORAGE_KEY, locale);
currentLocale = locale;
if (locale === DEFAULT_LOCALE) {
translations = fallbackTranslations;
} else {
translations = await loadLocale(locale);
}
document.documentElement.lang = locale;
window.dispatchEvent(new CustomEvent('locale-changed', { detail: { locale } }));
}
/** Übersetzungsfunktion mit Platzhalter-Unterstützung {{variable}} */
export function t(key, params = {}) {
let str = translations[key] ?? fallbackTranslations[key] ?? key;
for (const [k, v] of Object.entries(params)) {
str = str.replaceAll(`{{${k}}}`, String(v));
}
return str;
}
/** Aktuelle Locale abfragen */
export function getLocale() {
return currentLocale;
}
/** Liste der unterstützten Locales */
export function getSupportedLocales() {
return [...SUPPORTED_LOCALES];
}
/** Datum locale-aware formatieren */
export function formatDate(date) {
return new Intl.DateTimeFormat(currentLocale, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
}).format(date instanceof Date ? date : new Date(date));
}
/** Uhrzeit locale-aware formatieren */
export function formatTime(date) {
return new Intl.DateTimeFormat(currentLocale, {
hour: '2-digit',
minute: '2-digit',
hour12: false,
}).format(date instanceof Date ? date : new Date(date));
}
View File