8.2 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.
Status (2026-07-12): migration complete. All views and components (Batches A–F) are converted;
en.jsonholds ~506 keys. Every user-facing English string flows throught(). What remains is intentionally-untranslated data (see the "Not translated" section) — plus the actual work of adding a second language, which is now just translatingen.jsoninto a new<code>.json.
- 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. -
src/lib/views/TranscriptNotes.svelte— transcript + notes chrome (transcript.*,notes.*): pane headings/toggles, reprocess controls, notes toolbar (bold/heading/ list/preview), export button tooltips + dialog filter names, empty states, segment tooltips + note placeholders. Transcript/notes/speaker text left as user data. -
src/lib/views/SummaryPanel.svelte— all panel chrome (summary.*): section headings (Recording/Sync/Tags/Summary/Briefs/Action items/Calendar/Participants/ Speakers), buttons, placeholders, empty states, provider labels (viaproviderLabel; brand names kept literal), action-item + brief + speaker controls. Summary text, tags, brief content, participant/speaker names left as user data. -
src/lib/views/Settings.svelte— all sections done (settings.*): the modal shell (dialog title, Close), plus recording, hardware (incl. models/diarization), storage (+ export/import status), calendar (.pst import/cleanup/events), sync (targets + WebDAV/OAuth forms), AI (provider + Ollama advanced params), MCP (transport/scope/token/access log), privacy (egress self-check + vault), about. OLLAMA_OPTIONS param catalog labels/help left as data (config catalog, like model ids); example URLs/model-id placeholders left literal. -
Components (Batch F):
ConsentNotice+HostedAiBanner(legal/consent copy,consent.*/hosted.*),ThemeToggle(theme.*),ImportMeeting(import.*),TagChip(tagchip.*),LevelMeter(levelmeter.*).Splitter'slabelis caller-supplied and already translated by the parent; no strings of its own.
Outstanding — suggested batches
None — all batches complete. The section below is kept as a record of the plan.
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 ✅ done (branch feature_chore_bug_007)
Moved to the Done list above.
Batch D — summary panel ✅ done (branch feature_chore_bug_007)
Moved to the Done list above.
Batch E — Settings, all sections ✅ done (branch feature_chore_bug_007)
All nine sections + the modal shell converted, one commit per section. Moved to the Done list above.
Batch F — components ✅ done (branch feature_chore_bug_007)
Moved to the Done list above.
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.* |
| 2026-07-12 | Batch C (TranscriptNotes) + Batch D (SummaryPanel) | feature_chore_bug_007 |
+~120 keys; transcript.*, notes.*, summary.*; en.json now 198 keys |
| 2026-07-12 | Batch E (Settings, all 9 sections + shell) | feature_chore_bug_007 |
+~270 keys; settings.*; en.json now 470 keys; one commit per section |
| 2026-07-12 | Batch F (components) | feature_chore_bug_007 |
+~36 keys; consent.*/hosted.*/theme.*/import.*/tagchip.*/levelmeter.*; en.json now 506 keys — migration complete |