import { derived, writable, type Readable } from 'svelte/store'; import { browser } from '$app/environment'; /** * Minimal i18n layer — no external deps. * * Lookup is key-based (`t('panel.title')`) against flat or nested JSON * dictionaries. Interpolation uses `{name}` placeholders. Missing keys fall * back to the key itself so screens remain functional during translation. * * Adding a locale * --------------- * 1. Drop a JSON file into `src/lib/i18n/locales/.json`. * 2. Register it in `loaders` below. * 3. Expose it in the `Locale` type. */ export type Locale = 'ru' | 'en'; export const DEFAULT_LOCALE: Locale = 'ru'; export const SUPPORTED_LOCALES: Locale[] = ['ru', 'en']; const STORAGE_KEY = 'locale'; type Messages = Record; const loaders: Record Promise<{ default: Messages }>> = { ru: () => import('./locales/ru.json'), en: () => import('./locales/en.json'), }; const messages = writable>({} as Record); const locale = writable(DEFAULT_LOCALE); async function loadLocale(code: Locale): Promise { const mod = await loaders[code](); messages.update((m) => ({ ...m, [code]: mod.default })); } export async function setLocale(code: Locale): Promise { if (!SUPPORTED_LOCALES.includes(code)) return; await loadLocale(code); locale.set(code); if (browser) localStorage.setItem(STORAGE_KEY, code); } export async function initI18n(): Promise { const stored = browser ? (localStorage.getItem(STORAGE_KEY) as Locale | null) : null; const next = stored && SUPPORTED_LOCALES.includes(stored) ? stored : DEFAULT_LOCALE; await setLocale(next); } function lookup(dict: Messages, key: string): string | undefined { const parts = key.split('.'); let node: unknown = dict; for (const p of parts) { if (node && typeof node === 'object' && p in (node as Messages)) { node = (node as Messages)[p]; } else { return undefined; } } return typeof node === 'string' ? node : undefined; } function interpolate(template: string, values: Record): string { return template.replace(/\{(\w+)\}/g, (_, name) => name in values ? String(values[name]) : `{${name}}`, ); } export interface Translator { (key: string, values?: Record): string; } export const t: Readable = derived( [locale, messages], ([$locale, $messages]) => { const dict = $messages[$locale]; return (key: string, values?: Record) => { const str = dict ? lookup(dict, key) : undefined; if (str === undefined) return key; return values ? interpolate(str, values) : str; }; }, ); export const currentLocale: Readable = { subscribe: locale.subscribe };