feat: polish
This commit is contained in:
parent
2e6177fe74
commit
4bd927bb4e
137 changed files with 6357 additions and 137560 deletions
87
src/lib/i18n/index.ts
Normal file
87
src/lib/i18n/index.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
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/<code>.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<string, unknown>;
|
||||
|
||||
const loaders: Record<Locale, () => Promise<{ default: Messages }>> = {
|
||||
ru: () => import('./locales/ru.json'),
|
||||
en: () => import('./locales/en.json'),
|
||||
};
|
||||
|
||||
const messages = writable<Record<Locale, Messages>>({} as Record<Locale, Messages>);
|
||||
const locale = writable<Locale>(DEFAULT_LOCALE);
|
||||
|
||||
async function loadLocale(code: Locale): Promise<void> {
|
||||
const mod = await loaders[code]();
|
||||
messages.update((m) => ({ ...m, [code]: mod.default }));
|
||||
}
|
||||
|
||||
export async function setLocale(code: Locale): Promise<void> {
|
||||
if (!SUPPORTED_LOCALES.includes(code)) return;
|
||||
await loadLocale(code);
|
||||
locale.set(code);
|
||||
if (browser) localStorage.setItem(STORAGE_KEY, code);
|
||||
}
|
||||
|
||||
export async function initI18n(): Promise<void> {
|
||||
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, string | number>): string {
|
||||
return template.replace(/\{(\w+)\}/g, (_, name) =>
|
||||
name in values ? String(values[name]) : `{${name}}`,
|
||||
);
|
||||
}
|
||||
|
||||
export interface Translator {
|
||||
(key: string, values?: Record<string, string | number>): string;
|
||||
}
|
||||
|
||||
export const t: Readable<Translator> = derived(
|
||||
[locale, messages],
|
||||
([$locale, $messages]) => {
|
||||
const dict = $messages[$locale];
|
||||
return (key: string, values?: Record<string, string | number>) => {
|
||||
const str = dict ? lookup(dict, key) : undefined;
|
||||
if (str === undefined) return key;
|
||||
return values ? interpolate(str, values) : str;
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const currentLocale: Readable<Locale> = { subscribe: locale.subscribe };
|
||||
Loading…
Add table
Add a link
Reference in a new issue