143 lines
8.2 KiB
Markdown
143 lines
8.2 KiB
Markdown
# 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.json` holds ~506 keys. Every user-facing English string flows through
|
||
> `t()`. What remains is intentionally-untranslated data (see the "Not translated" section)
|
||
> — plus the actual work of adding a second language, which is now just translating
|
||
> `en.json` into 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_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
|
||
|
||
- [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.*`)
|
||
|
||
- [x] `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).
|
||
- [x] `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.
|
||
- [x] `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.
|
||
- [x] `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.
|
||
- [x] `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.
|
||
|
||
- [x] Components (Batch F): `ConsentNotice` + `HostedAiBanner` (legal/consent copy,
|
||
`consent.*` / `hosted.*`), `ThemeToggle` (`theme.*`), `ImportMeeting` (`import.*`),
|
||
`TagChip` (`tagchip.*`), `LevelMeter` (`levelmeter.*`). `Splitter`'s `label` is
|
||
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 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 |
|
||
| 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 |
|