# 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: `..`, 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
> `.json`, translate it, and register it in `DICTS` + `LOCALES` in
> `index.svelte.ts`. That one file is the whole job.
## Done
- [x] i18n engine + `en.json` baseline + Settings language selector (branch
`feature_chore_bug_007`)
- [x] `Settings.svelte` — nav labels (`nav.*`), the Language section
(`settings.language.*`), and the Transcription Language section
(`settings.transcription.*`)
Current key count in `en.json`: **18** (`nav.*`, `settings.language.*`,
`settings.transcription.*`).
## 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 (high visibility, medium size)
- [ ] `src/App.svelte` (~846 lines, ~21 labeled attrs) — top nav, window/tray strings,
dialog prompts, empty states. Prefix `app.*`.
### Batch B — meetings list (small, high traffic)
- [ ] `src/lib/views/MeetingsList.svelte` (~373 lines) — **labels only** (~10 strings):
search placeholder, `"All tags"`, empty/loading states (`"No meetings yet…"`,
`"No matches."`, `"Loading meetings…"`), status badges (`"recovering"`, `"error"`),
bulk-export button, delete-confirm text, and aria-labels. The list rows themselves
(titles, tags, dates, snippets) are user data — leave them. Prefix `meetings.*`.
### 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.
Prefix `transcript.*` / `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. Prefix `summary.*`.
### 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"`) — prefix `settings.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 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 |