From 9ed977a3fe540edc2fa82dd6dcd30793fae184d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Salavisa?= Date: Fri, 7 Aug 2026 21:45:12 +0200 Subject: [PATCH] docs: correct the web localization status for the finished i18n work (#2400) Co-authored-by: Kandev Agent --- docs/i18n.md | 10 +++++++--- docs/public/feature-status.md | 2 +- docs/public/web-development.md | 17 +++++++++++++++++ docs/specs/platform/i18n.md | 32 +++++++++++++++++--------------- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/docs/i18n.md b/docs/i18n.md index b4c651327e..acca044d2e 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -6,9 +6,11 @@ European Portuguese (`pt-pt`) and Simplified Chinese (`zh-cn`) are the shipped human locales, and a generated **pseudo-locale** is used for QA. Requirements live in [`docs/specs/platform/i18n.md`](specs/platform/i18n.md). -The migration is **in progress, one directory per PR**. The runtime, the gates, -and Settings → General → Appearance are done; everything else still holds -English literals. +The migration is **complete**. Every routed web UI surface renders through `t()` +/ ``, and `i18nGuardFiles` now enumerates the directories that hold them. +Running the guard across `components app hooks lib` reports literals in only the +unrouted QA pages under `app/demo/`. New copy is still guarded everywhere, so a +hardcoded literal is now a regression rather than unfinished migration. Two enforcement layers, and it matters which is which: @@ -176,6 +178,8 @@ pnpm run i18n:pseudo # regenerate the pseudo catalog from en pnpm run lint:i18n # preview the guard on a path that is NOT yet migrated pnpm run i18n:ratchet # new-code guard + allowlist-only-grows (pre-commit + CI) pnpm run i18n:removed # did this change rewrite any English? (diff-based, advisory) +pnpm run i18n:parity # per-locale missing/extra keys (report, never a gate) +pnpm run i18n:sweep # copy the jsx-only rule cannot see (report, never a gate) # Codemods — always inspect the diff; each declines what it cannot prove safe. node scripts/externalize-strings.mjs --write components app hooks lib diff --git a/docs/public/feature-status.md b/docs/public/feature-status.md index a8f83831f5..e1bc4db592 100644 --- a/docs/public/feature-status.md +++ b/docs/public/feature-status.md @@ -75,7 +75,7 @@ Released builds can lag this page. Check **Settings > System > About** or `kande | Language servers | Limited | Desktop Monaco maps TypeScript/JavaScript, Go, Rust, Python, and experimental Kotlin to task-host servers with diagnostics, completion, hover, navigation, signature help, and semantic tokens. Status distinguishes a launched process awaiting `initialize` from server-reported project work; slow Kotlin Gradle imports can take minutes, with no automatic timeout or universal percentage/ETA. Fine-pointer desktop can place active-file status in the editor toolbar or Application status bar; touch tablet falls back to the toolbar, and phone has no LSP control. Auto-start/installation default off; Kotlin is manual-only. Local PC and Local Docker are supported, while SSH, Sprites, remote Docker, and mobile LSP are not. | | Integrated shell terminals | Dependency-bound | Create, reopen, rename, and terminate are shipped. Shell settings affect new terminals, and executor/task-environment availability controls where the PTY runs. | | Prompts, keyboard shortcuts, notifications, appearance, and task actions | Supported | These are normal settings surfaces. Browser notification permission, custom command validity, and local shell/editor availability still apply to their effects. | -| Web UI localization | Limited | **Settings > General > Appearance** offers English and 简体中文. The selection persists across reloads, but localization migration is incremental: currently externalized surfaces use the selected language while unmigrated surfaces remain in English. | +| Web UI localization | Limited | **Settings > General > Appearance** offers English, Português (Portugal), and 简体中文. The choice applies immediately, persists per browser through a `kandev_locale` cookie, and is negotiated from `Accept-Language` before defaulting to English when no cookie is set. Externalization is complete: every reachable web UI surface renders through the translation layer, and dates, numbers, and relative timestamps follow the selected locale. Per-language completeness still differs. Portuguese tracks English closely; Simplified Chinese is maintained out of band and lags, most visibly in the task workbench. An untranslated key falls back to English rather than failing. About fifteen shared `@kandev/ui` accessibility labels — mostly screen-reader-only text such as a dialog's **Close** — stay English even where the visible UI is translated. Desktop-shell and CLI surfaces, API/log diagnostics, and agent output were never in scope; user content such as task titles, chat, and diffs is never translated. Development and E2E builds list an extra **Pseudo (QA)** locale that production builds do not ship. | diff --git a/docs/public/web-development.md b/docs/public/web-development.md index 35a52e07eb..912c33c314 100644 --- a/docs/public/web-development.md +++ b/docs/public/web-development.md @@ -81,6 +81,23 @@ Chat, plan, changes, files, terminal, editor/diff, preview, commit, and pull-req - a restored dock layout and a narrow mobile route; - absent optional providers or executor capabilities. +## Localize user-facing copy + +Every new user-facing string must go through i18next — `t("namespace:key")` or `` — never an inline literal. That covers visible copy, `placeholder`, `aria-label`, `title`, `alt`, toasts, validation and empty-state text, and dialog copy. Externalization of the existing UI is complete, so a hardcoded literal is now a regression rather than unfinished migration. + +Catalogs live in `apps/web/src/locales//.json`. English is the source locale and the only hand-authored catalog; it is bundled into the entry chunk, while each other locale is a lazily fetched chunk loaded before that locale activates. Shipped locales are English, European Portuguese, and Simplified Chinese, plus a generated `pseudo` catalog that only development and E2E builds contain. A missing key falls back to English at runtime. + +Three mistakes cause silent bugs: never translate a string compared with `===`, never call `t()` at module scope, and never pass an English plural ending as a value — use `count` with `_one`/`_other` keys. + +```bash +pnpm run i18n:check # gate: plurals, module-scope t(), Trans indices +pnpm run i18n:ratchet # gate: no new hardcoded literals in changed lines +pnpm run i18n:parity # advisory report: per-locale missing and extra keys +pnpm run i18n:sweep # advisory report: copy the jsx-only lint rule cannot see +``` + +The first two run in pre-commit and CI. The last two are reports that always exit zero and are wired into neither, because real-locale translation is maintained out of band and must not fail an English-only change. A clean lint does not prove a file is done: the rule only inspects JSX literals and skips anything assigned to a SCREAMING_CASE identifier, so review config tables by eye and use the pseudo-locale (**Settings > General > Appearance** in development builds) as the completeness check. Shared `@kandev/ui` primitives cannot import the app's i18n runtime, so their accessibility labels are localized at the call site. Full guide: [`docs/i18n.md`](../i18n.md). + ## Security, accessibility, and responsiveness Frontend checks improve the experience; they are not authorization. The backend must enforce identity, workspace/task scope, and destructive operations. Treat provider text, repository content, Markdown/HTML, URLs, and agent output as untrusted. Keep raw HTML rendering paired with the existing sanitizer and never interpolate shell commands or credentials in the browser. diff --git a/docs/specs/platform/i18n.md b/docs/specs/platform/i18n.md index 025b9d55fe..f25ad6bd28 100644 --- a/docs/specs/platform/i18n.md +++ b/docs/specs/platform/i18n.md @@ -21,13 +21,13 @@ language and the pseudo-locale as the migration QA oracle. `alt`, toast/`sonner` messages, form-validation messages, empty-state and error copy, and confirm/dialog text. - English SHALL remain the source locale. The shipped human locales SHALL be - `en` and `zh-cn`; copy lives in per-namespace JSON catalogs keyed by stable - ids. The `pseudo` catalog is generated from `en`, never hand-edited. -- The app SHALL expose a **language switcher** in Settings. It lists English and - Simplified Chinese by their fixed endonyms. Selecting a locale re-renders the - UI without a full reload and persists the - choice across restarts. Development and E2E builds additionally list the - **pseudo-locale** (see below). + `en`, `pt-pt`, and `zh-cn`; copy lives in per-namespace JSON catalogs keyed by + stable ids. The `pseudo` catalog is generated from `en`, never hand-edited. +- The app SHALL expose a **language switcher** in Settings. It lists English, + European Portuguese, and Simplified Chinese by their fixed endonyms. Selecting + a locale re-renders the UI without a full reload and persists the choice across + restarts. Development and E2E builds additionally list the **pseudo-locale** + (see below). - The app SHALL ship a **pseudo-locale** (`pseudo`) that transforms every extracted message (accents + padding, e.g. `Ĉřéàţé ţàśķ`) so QA can visually confirm a string was externalized and detect truncation. The pseudo-locale is @@ -95,12 +95,12 @@ merge. Sequence: i18n adds no server-side persistent entities. It introduces: - **Message catalogs** — checked-in JSON under - `apps/web/src/locales//.json` for `en`, `zh-cn`, and - `pseudo`. `en` is hand-authored (keys are stable ids; the English copy lives - only here); `zh-cn` is a human-maintained translation, maintained out of band, - whose namespace and key sets aim to mirror `en` but may lag it — a gap warns - rather than failing, and i18next falls back to `en` at runtime; `pseudo` is - generated from `en` by + `apps/web/src/locales//.json` for `en`, `pt-pt`, `zh-cn`, + and `pseudo`. `en` is hand-authored (keys are stable ids; the English copy + lives only here); `pt-pt` and `zh-cn` are human-maintained translations, + maintained out of band, whose namespace and key sets aim to mirror `en` but may + lag it — a gap warns rather than failing, and i18next falls back to `en` at + runtime; `pseudo` is generated from `en` by `scripts/generate-pseudo-locale.mjs` and is never hand-edited. Catalogs load at runtime via Vite glob import — no build plugin. Only `en` is bundled into the initial download: it is the fallback locale, so it has to be in memory the @@ -177,7 +177,8 @@ Frontend module contract (new): - **GIVEN** a plural key (`x_one` / `x_other`) rendered with a `count`, **WHEN** count is 1 vs many, **THEN** i18next selects the matching form. - **GIVEN** a production build, **WHEN** the language switcher renders, **THEN** - English and 简体中文 are offered and the pseudo-locale is not. + English, Português (Portugal), and 简体中文 are offered and the pseudo-locale + is not. - **GIVEN** an English namespace or key changes, **WHEN** `i18n:check` runs, **THEN** every real locale missing or adding a namespace or key is reported as a warning naming the locale, namespace, and missing or extra entries, **AND** @@ -210,7 +211,8 @@ Frontend module contract (new): switching locale is a pure client re-render. ## Out of scope -- Shipping human languages other than English and Simplified Chinese. +- Shipping human languages other than English, European Portuguese, and + Simplified Chinese. - Translating the CLI launcher, logs, or ACP/agent output, and the ~1,100 diagnostic API error strings (`http.Error` / `gin.H{"error": …}`) — the SPA maps API failures onto its own translated copy, so those stay English by design.