From 3bec77db3bddab7ce3b1277ff341415af1af66e0 Mon Sep 17 00:00:00 2001 From: Ulas Date: Tue, 31 Mar 2026 21:41:20 +0200 Subject: [PATCH] feat: add i18n module (public/i18n.js) --- public/i18n.js | 97 +++++++++++++++++++++++++++++++++++++++++ public/locales/.gitkeep | 0 2 files changed, 97 insertions(+) create mode 100644 public/i18n.js create mode 100644 public/locales/.gitkeep diff --git a/public/i18n.js b/public/i18n.js new file mode 100644 index 0000000..a37f571 --- /dev/null +++ b/public/i18n.js @@ -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)); +} diff --git a/public/locales/.gitkeep b/public/locales/.gitkeep new file mode 100644 index 0000000..e69de29