6.6 KiB
i18n migration tracking
Working doc for the incremental UI-string translation effort. The i18n mechanism is done; this tracks moving the app's remaining hardcoded strings into the translation files, one batch at a time. Tick boxes as views are converted.
- Engine: hand-rolled, zero-dependency.
src/lib/i18n/index.svelte.ts - Baseline dictionary:
src/lib/i18n/en.json(English is the source-of-truth key set and the fallback for any missing key) - Selector: Settings → Language (
section === "language") - Scope decision: display language is separate from the transcription
whisper_languagesetting — don't conflate them.
What we translate (and what we never do)
Translate: UI chrome only — fixed labels, buttons, headings, placeholders, empty/loading/status states, tooltips, and app-generated default labels.
Never translate: user-authored content. Meeting titles, tag names, notes, transcript
text, search snippets, dates — these are bound straight from data (item.title, tag
values, s.text, …) and stay exactly as the user wrote them. If a string comes from the
user or the recording, it does not get a key.
So "convert view X" always means "key its fixed labels", never "touch its content". Most views are mostly content with a thin shell of labels — MeetingsList, for example, is ~10 fixed labels (search placeholder, empty states, filter/bulk-export labels) wrapped around a list whose rows are pure user data.
Caveat — the
"Untitled meeting"default title is generated in the Rust backend, not the frontend, so it never reachest(). Localizing app-generated defaults is a separate backend decision, out of scope for this frontend effort.
How to convert a string (the pattern)
- Add a key to
en.json. Naming:<area>.<subarea>.<name>, dotted, grouped by view — e.g.nav.recording,settings.transcription.title,meetings.empty. - Replace the literal in markup with
{t("key")}(importtfrom../i18n/index.svelte). - Dynamic bits use placeholders:
t("meetings.count", { n })against"meetings.count": "{n} meetings". Handle plurals with a caller-side ternary for now (n === 1 ? t("...one") : t("...many")) — the engine is intentionally simple. - Attributes translate the same way:
title={t("...")},aria-label={t("...")},placeholder={t("...")}. npm run checkmust stay at 0 errors/warnings.
When adding a new language (not covered by this doc's batches): copy
en.jsonto<code>.json, translate it, and register it inDICTS+LOCALESinindex.svelte.ts. That one file is the whole job.
Done
-
i18n engine +
en.jsonbaseline + Settings language selector (branchfeature_chore_bug_007) -
Settings.svelte— nav labels (nav.*), the Language section (settings.language.*), and the Transcription Language section (settings.transcription.*) -
src/App.svelte— app shell chrome (app.*): header (tagline, template picker, record/stop/cancel/add-meeting, recording status, retention, backend, device-notice), Settings button, consent + vault-unlock dialogs, pane toggles, splitter labels, and JS strings (discard confirm, SR announcements, vault error). -
src/lib/views/MeetingsList.svelte— labels only (meetings.*): search/filter/ bulk-export controls, empty/loading/status states, status badges (viastatusLabel), resume + delete, delete-confirm, and the pluralized export result. List rows (titles/tags/dates/snippets) left as user data.
Outstanding — suggested batches
Ordered roughly by user-visibility ÷ effort. Sizes are rough (line count / labeled
attributes) to help portion the work, not exact string counts. A file isn't "done" until
its visible text and its title/aria-label/placeholder attributes are keyed.
Batch A — app shell ✅ done (branch feature_chore_bug_007)
Moved to the Done list above.
Batch B — meetings list ✅ done (branch feature_chore_bug_007)
Moved to the Done list above.
Batch C — transcript & notes (large surface)
src/lib/views/TranscriptNotes.svelte(~869 lines, ~40 attrs) — export button labels, editor toolbar tooltips, reprocess controls, pane toggles, empty states. Prefixtranscript.*/notes.*. (Timestamps + the Obsidian/Bundle button labels live here.)
Batch D — summary panel (medium)
src/lib/views/SummaryPanel.svelte(~1335 lines, ~14 attrs) — summary/action-item UI, participants, recording playback labels, LLM status copy. Prefixsummary.*.
Batch E — Settings, remaining sections (largest; split further as needed)
The file is big — do it section by section, one sub-batch each:
- Recording (
section === "recording") — prefixsettings.recording.* - Hardware (
section === "hardware", excluding the already-done Transcription Language) - Storage (incl. the Export/Import copy) —
settings.storage.* - Calendar —
settings.calendar.* - Sync —
settings.sync.* - AI —
settings.ai.* - MCP server —
settings.mcp.* - Privacy —
settings.privacy.* - About —
settings.about.*
Batch F — components
ConsentNotice.svelte(~72 lines) — legal/consent copy; translate carefullyHostedAiBanner.svelte(~93 lines) — third-party-egress disclosure copyImportMeeting.svelte(~229 lines, ~5 attrs) — import dialogThemeToggle.svelte(~72 lines) — one aria-labelTagChip.svelte,LevelMeter.svelte,Splitter.svelte— check for any stray user-facing strings (likely minimal)
Not translated (intentional)
- User-authored content — meeting titles, tag names, notes, transcript text, search snippets. Bound from data; stays as the user wrote it (see "What we translate" above).
- App-generated defaults created in the backend (e.g.
"Untitled meeting") — a separate backend concern; the frontendt()never sees them. - Backend / Rust error messages surfaced via
WaError— out of scope for the frontendt(); revisit only if we localize command errors. - Provider/proper names (Ollama, Anthropic, OpenAI, Obsidian, WebDAV, WhispAssist), model ids, ISO language codes.
- Console/
tracinglogs.
Batch log
Record each landed batch here (date / branch / commit) so progress is auditable.
| Date | Batch | Branch / commit | Notes |
|---|---|---|---|
| 2026-07-12 | Infra + Settings nav/language/transcription | feature_chore_bug_007 |
engine + en.json + selector |
| 2026-07-12 | Batch A (App shell) + Batch B (MeetingsList) | feature_chore_bug_007 |
+~60 keys; app.*, meetings.* |