class I18n { constructor() { this.translations = {}; this.currentLocale = 'de'; this.fallbackLocale = 'de'; this.supportedLocales = ['de', 'en', 'fr']; this.subscribers = new Set(); this.loaded = false; } async init() { const savedLocale = localStorage.getItem('locale'); const browserLocale = navigator.language.split('-')[0]; this.currentLocale = savedLocale || (this.supportedLocales.includes(browserLocale) ? browserLocale : this.fallbackLocale); await this.loadTranslations(this.currentLocale); this.loaded = true; this.updateDOM(); } async loadTranslations(locale) { if (this.translations[locale]) return; try { const response = await fetch(`/locales/${locale}.json`); if (!response.ok) throw new Error(`Failed to load ${locale}`); this.translations[locale] = await response.json(); } catch (error) { console.error(`Failed to load translations for ${locale}:`, error); if (locale !== this.fallbackLocale) { await this.loadTranslations(this.fallbackLocale); } } } async setLocale(locale) { if (!this.supportedLocales.includes(locale)) { console.warn(`Locale ${locale} is not supported`); return; } await this.loadTranslations(locale); this.currentLocale = locale; localStorage.setItem('locale', locale); document.documentElement.lang = locale; this.updateDOM(); this.notifySubscribers(); } getLocale() { return this.currentLocale; } t(key, params = {}) { const translations = this.translations[this.currentLocale] || this.translations[this.fallbackLocale] || {}; let text = this.getNestedValue(translations, key); if (text === undefined) { console.warn(`Missing translation: ${key}`); return key; } Object.entries(params).forEach(([param, value]) => { text = text.replace(new RegExp(`{{${param}}}`, 'g'), value); }); return text; } getNestedValue(obj, path) { return path.split('.').reduce((current, key) => { return current && current[key] !== undefined ? current[key] : undefined; }, obj); } updateDOM() { document.querySelectorAll('[data-i18n]').forEach((el) => { const key = el.getAttribute('data-i18n'); let params = {}; if (el.dataset.i18nParams) { try { params = JSON.parse(el.dataset.i18nParams); } catch (e) { console.warn(`Invalid i18n params for key "${key}":`, e); } } el.textContent = this.t(key, params); }); document.querySelectorAll('[data-i18n-placeholder]').forEach((el) => { const key = el.getAttribute('data-i18n-placeholder'); el.placeholder = this.t(key); }); document.querySelectorAll('[data-i18n-title]').forEach((el) => { const key = el.getAttribute('data-i18n-title'); el.title = this.t(key); }); document.querySelectorAll('[data-i18n-aria]').forEach((el) => { const key = el.getAttribute('data-i18n-aria'); el.setAttribute('aria-label', this.t(key)); }); } subscribe(callback) { this.subscribers.add(callback); return () => this.subscribers.delete(callback); } notifySubscribers() { this.subscribers.forEach(callback => callback(this.currentLocale)); } getSupportedLocales() { return this.supportedLocales; } getLocaleDisplayName(locale) { const names = { de: 'Deutsch', en: 'English', fr: 'Français' }; return names[locale] || locale; } } export const i18n = new I18n(); export const t = (key, params) => i18n.t(key, params);