feat(i18n): scaffold svelte-i18n with en/es/de locales and language selector
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>
This commit is contained in:
73
src/lib/i18n/index.ts
Normal file
73
src/lib/i18n/index.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
//! 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);
|
||||
}
|
||||
19
src/lib/i18n/locales/de.json
Normal file
19
src/lib/i18n/locales/de.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"language": "Sprache",
|
||||
"languageDescription": "Oberflächensprache von Kon. Wirkt sich nicht auf die Transkription aus."
|
||||
},
|
||||
"history": {
|
||||
"title": "Verlauf",
|
||||
"empty": "Noch keine Transkripte — drück das Kürzel und leg los."
|
||||
},
|
||||
"tasks": {
|
||||
"title": "Aufgaben"
|
||||
},
|
||||
"dictation": {
|
||||
"ready": "Bereit",
|
||||
"recording": "Aufnahme…",
|
||||
"processing": "Verarbeitung…"
|
||||
}
|
||||
}
|
||||
19
src/lib/i18n/locales/en.json
Normal file
19
src/lib/i18n/locales/en.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"language": "Language",
|
||||
"languageDescription": "Kon's interface language. Affects labels, not transcription."
|
||||
},
|
||||
"history": {
|
||||
"title": "History",
|
||||
"empty": "No transcripts yet — press the hotkey and start talking."
|
||||
},
|
||||
"tasks": {
|
||||
"title": "Tasks"
|
||||
},
|
||||
"dictation": {
|
||||
"ready": "Ready",
|
||||
"recording": "Recording…",
|
||||
"processing": "Processing…"
|
||||
}
|
||||
}
|
||||
19
src/lib/i18n/locales/es.json
Normal file
19
src/lib/i18n/locales/es.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"settings": {
|
||||
"title": "Ajustes",
|
||||
"language": "Idioma",
|
||||
"languageDescription": "Idioma de la interfaz de Kon. No afecta a la transcripción."
|
||||
},
|
||||
"history": {
|
||||
"title": "Historial",
|
||||
"empty": "Sin transcripciones todavía — pulsa el atajo y empieza a hablar."
|
||||
},
|
||||
"tasks": {
|
||||
"title": "Tareas"
|
||||
},
|
||||
"dictation": {
|
||||
"ready": "Listo",
|
||||
"recording": "Grabando…",
|
||||
"processing": "Procesando…"
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@
|
||||
import { clampTextLines } from "$lib/utils/textMeasure.js";
|
||||
import { bodyPretextLineHeight, pretextFontShorthand } from "$lib/utils/accessibilityTypography.js";
|
||||
import { Check, ChevronRight } from "lucide-svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { SUPPORTED_LOCALES, setLocale, currentLocale } from "$lib/i18n";
|
||||
|
||||
const prefs = getPreferences();
|
||||
|
||||
@@ -1603,6 +1605,25 @@
|
||||
data-no-transition
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">
|
||||
{$_("settings.language")}
|
||||
</p>
|
||||
<div class="inline-flex bg-bg-elevated rounded-[10px] p-[3px] gap-[2px]">
|
||||
{#each SUPPORTED_LOCALES as option}
|
||||
<button
|
||||
class="rounded-lg font-medium px-3.5 py-[6px] text-[12px]
|
||||
{$currentLocale === option.code
|
||||
? 'bg-accent text-bg shadow-[0_1px_4px_rgba(232,168,124,0.3)]'
|
||||
: 'text-text-secondary hover:text-text hover:bg-hover'}"
|
||||
style="transition-duration: var(--duration-ui)"
|
||||
onclick={() => setLocale(option.code)}
|
||||
>{option.label}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<p class="text-[11px] text-text-tertiary mt-2">{$_("settings.languageDescription")}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
39
src/lib/shims.d.ts
vendored
Normal file
39
src/lib/shims.d.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
// Ambient module shims. Must stay script-scoped (no top-level imports /
|
||||
// exports) so `declare module` registers globally; adding an import/export
|
||||
// turns the file into a module and scopes the declaration away.
|
||||
|
||||
declare module "@chenglou/pretext" {
|
||||
export interface PretextLayoutLine {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface PretextLayoutResult {
|
||||
height: number;
|
||||
lineCount: number;
|
||||
lines: PretextLayoutLine[];
|
||||
}
|
||||
|
||||
export function prepare(
|
||||
text: string,
|
||||
font: string,
|
||||
options?: Record<string, unknown>,
|
||||
): unknown;
|
||||
|
||||
export function prepareWithSegments(
|
||||
text: string,
|
||||
font: string,
|
||||
options?: Record<string, unknown>,
|
||||
): unknown;
|
||||
|
||||
export function layout(
|
||||
prepared: unknown,
|
||||
maxWidth: number,
|
||||
lineHeight: number,
|
||||
): PretextLayoutResult;
|
||||
|
||||
export function layoutWithLines(
|
||||
prepared: unknown,
|
||||
maxWidth: number,
|
||||
lineHeight: number,
|
||||
): PretextLayoutResult;
|
||||
}
|
||||
Reference in New Issue
Block a user