Files
WhispAssist/docs/i18n-tracking.md
T

7.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_language setting — 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 reaches t(). Localizing app-generated defaults is a separate backend decision, out of scope for this frontend effort.

How to convert a string (the pattern)

  1. Add a key to en.json. Naming: <area>.<subarea>.<name>, dotted, grouped by view — e.g. nav.recording, settings.transcription.title, meetings.empty.
  2. Replace the literal in markup with {t("key")} (import t from ../i18n/index.svelte).
  3. 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.
  4. Attributes translate the same way: title={t("...")}, aria-label={t("...")}, placeholder={t("...")}.
  5. npm run check must stay at 0 errors/warnings.

When adding a new language (not covered by this doc's batches): copy en.json to <code>.json, translate it, and register it in DICTS + LOCALES in index.svelte.ts. That one file is the whole job.

Done

  • i18n engine + en.json baseline + Settings language selector (branch feature_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 (via statusLabel), 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 (via providerLabel; 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.

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 ✅ 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

  • ConsentNotice.svelte (~72 lines) — legal/consent copy; translate carefully
  • HostedAiBanner.svelte (~93 lines) — third-party-egress disclosure copy
  • ImportMeeting.svelte (~229 lines, ~5 attrs) — import dialog
  • ThemeToggle.svelte (~72 lines) — one aria-label
  • TagChip.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 frontend t() never sees them.
  • Backend / Rust error messages surfaced via WaError — out of scope for the frontend t(); revisit only if we localize command errors.
  • Provider/proper names (Ollama, Anthropic, OpenAI, Obsidian, WebDAV, WhispAssist), model ids, ISO language codes.
  • Console/tracing logs.

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