initI18n (src/lib/i18n/index.ts) registers three locales and picks the
initial one in order: kon_locale in localStorage > navigator.language short
code > en. +layout.svelte calls it once at app boot; guarded so per-window
re-init is a no-op.
Locale files are deliberately sparse — this is a scaffolding pass so strings
can be migrated incrementally. The Settings → Appearance → Language picker
plus its own description is the first real consumer; everything else
continues to render as hardcoded text until extracted.
Also: split the @chenglou/pretext ambient shim into src/lib/shims.d.ts. The
declaration previously lived in app.d.ts alongside a top-level `export {}`,
which made app.d.ts a module — scoping `declare module` to its own imports
and breaking resolution from src/lib/utils/textMeasure.ts. The fresh
.svelte-kit sync triggered by installing svelte-i18n surfaced it. Ambient
shim files must stay script-scoped (no top-level imports/exports).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
74 lines
2.1 KiB
TypeScript
74 lines
2.1 KiB
TypeScript
//! i18n scaffolding via svelte-i18n.
|
|
//!
|
|
//! Scope for the initial pass: wire the stack (loader, persisted choice,
|
|
//! Settings selector) so strings can be migrated incrementally. Only a
|
|
//! handful of labels are actually translated today — everything else
|
|
//! continues to render as-is via hardcoded text until extracted.
|
|
//!
|
|
//! Locales currently shipped: en (source of truth), es, de. Adding a new
|
|
//! locale is one `register` call + a new `locales/<code>.json`.
|
|
|
|
import { init, register, locale as svelteLocale } from "svelte-i18n";
|
|
import { derived, get } from "svelte/store";
|
|
|
|
export type Locale = "en" | "es" | "de";
|
|
|
|
export const SUPPORTED_LOCALES: { code: Locale; label: string }[] = [
|
|
{ code: "en", label: "English" },
|
|
{ code: "es", label: "Español" },
|
|
{ code: "de", label: "Deutsch" },
|
|
];
|
|
|
|
const STORAGE_KEY = "kon_locale";
|
|
|
|
register("en", () => import("./locales/en.json"));
|
|
register("es", () => import("./locales/es.json"));
|
|
register("de", () => import("./locales/de.json"));
|
|
|
|
function detectInitialLocale(): Locale {
|
|
if (typeof localStorage !== "undefined") {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if (stored && SUPPORTED_LOCALES.some((option) => option.code === stored)) {
|
|
return stored as Locale;
|
|
}
|
|
}
|
|
if (typeof navigator !== "undefined" && navigator.language) {
|
|
const short = navigator.language.split("-")[0] as Locale;
|
|
if (SUPPORTED_LOCALES.some((option) => option.code === short)) {
|
|
return short;
|
|
}
|
|
}
|
|
return "en";
|
|
}
|
|
|
|
let initialised = false;
|
|
|
|
export function initI18n(): void {
|
|
if (initialised) return;
|
|
initialised = true;
|
|
|
|
init({
|
|
fallbackLocale: "en",
|
|
initialLocale: detectInitialLocale(),
|
|
});
|
|
}
|
|
|
|
export function setLocale(code: Locale): void {
|
|
svelteLocale.set(code);
|
|
if (typeof localStorage !== "undefined") {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, code);
|
|
} catch {
|
|
// non-fatal: private browsing, quota, etc.
|
|
}
|
|
}
|
|
}
|
|
|
|
export const currentLocale = derived(svelteLocale, ($locale) =>
|
|
((($locale ?? "en").split("-")[0]) as Locale),
|
|
);
|
|
|
|
export function getCurrentLocale(): Locale {
|
|
return get(currentLocale);
|
|
}
|