Files
oikos/public/i18n.js
T
Ulas Kalayci 6746a5a175 feat: Ukrainian translation, UAH currency, shopping category i18n (closes #52)
- Add Ukrainian (uk) locale to SUPPORTED_LOCALES and locale picker
- Add public/locales/uk.json (622 keys, full Ukrainian translation)
- Add UAH (Ukrainian Hryvnia) to SUPPORTED_CURRENCIES and VALID_CURRENCIES
- Add CATEGORY_I18N map and catLabel() in settings.js to translate default
  shopping category names in the settings panel; rename and delete dialogs
  now also use the translated name instead of the raw German DB string
- Align server VALID_CURRENCIES with frontend: add missing AED, BRL, INR, SAR

Co-Authored-By: baragoon <baragoon@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 22:09:42 +02:00

114 lines
3.5 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', 'fr', 'it', 'sv', 'el', 'ru', 'tr', 'zh', 'ja', 'ar', 'hi', 'pt', 'uk'];
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);
}