feat: add i18n module (public/i18n.js)
This commit is contained in:
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user