d68226d11e
- Apple CalDAV: ICS events with TZID parameter are now converted to UTC using the Intl API instead of being stored as floating local time, fixing wrong start times for events synced from iOS Calendar - i18n: fallback language for unsupported browser locales changed from German to English
114 lines
3.4 KiB
JavaScript
114 lines
3.4 KiB
JavaScript
/**
|
|
* 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', 'es', 'it', 'sv'];
|
|
const DEFAULT_LOCALE = 'de';
|
|
const STORAGE_KEY = 'oikos-locale';
|
|
|
|
let currentLocale = DEFAULT_LOCALE;
|
|
let translations = {};
|
|
let fallbackTranslations = {};
|
|
|
|
/** Resolve locale: manual override > navigator.language > English > 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 'en';
|
|
}
|
|
|
|
/** 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) {
|
|
try {
|
|
translations = await loadLocale(currentLocale);
|
|
} catch {
|
|
translations = fallbackTranslations;
|
|
currentLocale = DEFAULT_LOCALE;
|
|
}
|
|
} 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;
|
|
const loaded = locale === DEFAULT_LOCALE
|
|
? fallbackTranslations
|
|
: await loadLocale(locale);
|
|
if (currentLocale !== locale) return;
|
|
translations = loaded;
|
|
document.documentElement.lang = locale;
|
|
window.dispatchEvent(new CustomEvent('locale-changed', { detail: { locale } }));
|
|
}
|
|
|
|
/** Hilfsfunktion: Dot-Notation in verschachteltem Objekt auflösen */
|
|
function resolve(obj, key) {
|
|
return key.split('.').reduce((o, k) => (o != null ? o[k] : undefined), obj);
|
|
}
|
|
|
|
/** Übersetzungsfunktion mit Platzhalter-Unterstützung {{variable}} */
|
|
export function t(key, params = {}) {
|
|
let str = resolve(translations, key) ?? resolve(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) {
|
|
if (date == null) return '';
|
|
const d = date instanceof Date ? date : new Date(date);
|
|
if (isNaN(d.getTime())) return '';
|
|
return new Intl.DateTimeFormat(currentLocale, {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
}).format(d);
|
|
}
|
|
|
|
/** Uhrzeit locale-aware formatieren */
|
|
export function formatTime(date) {
|
|
if (date == null) return '';
|
|
const d = date instanceof Date ? date : new Date(date);
|
|
if (isNaN(d.getTime())) return '';
|
|
return new Intl.DateTimeFormat(currentLocale, {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false,
|
|
}).format(d);
|
|
}
|