diff --git a/docs/superpowers/specs/2026-08-11-i18n-design.md b/docs/superpowers/specs/2026-08-11-i18n-design.md new file mode 100644 index 0000000..0269149 --- /dev/null +++ b/docs/superpowers/specs/2026-08-11-i18n-design.md @@ -0,0 +1,158 @@ +# i18n (English + Chinese) — Design + +Date: 2026-08-11 +Status: Approved (section-by-section approval; approach C: URL locale segment; error strategy: sub2api code mapping) + +## Goal + +Add bilingual (English/中文) support to the EvoScientist WebUI using +`next-intl` with URL locale segments (`/en/...`, `/zh/...`). All UI static +text is translated; API error messages are localized client-side by mapping +stable server error codes (the sub2api pattern). Agent output, workspace +content, thread titles, and admin-configured branding/terms text are data, +not UI copy, and are not translated. + +Decisions locked in during brainstorming: + +- **Languages:** `en` + `zh`. +- **Approach:** URL locale segment (user chose C over cookie-only). + `localePrefix: "always"` — `/en`, `/zh` always visible; `/` redirects. +- **Default:** first visit negotiates from `Accept-Language` (`zh*` → zh, + else en); next-intl's `NEXT_LOCALE` cookie remembers preference for + subsequent root visits. No localStorage, no account/DB storage, no + System-config site default. +- **Switcher:** header dropdown (English / 中文), modeled on sub2api's + LocaleSwitcher but built with our shadcn DropdownMenu. Switching replaces + the URL prefix and preserves query params (`threadId`, `view`, ...). +- **Errors:** server responses carry stable `{ code, message }`; the client + maps `code` → `errors.` catalog entry, falling back to the server's + English `message`. Server logs / errorLogStore stay English. +- **Reference implementation:** sub2api (`~/Projects/EvoSci/sub2api/sub2api`) + conventions borrowed: per-domain locale modules, aggregation with + key-collision test, parity tests, code-mapping error localization. + +## 1. Architecture & locale resolution + +- **next-intl routing** (`defineRouting({ locales: ["en", "zh"], defaultLocale: "en", localePrefix: "always" })`). + `defaultLocale: "en"` is only the fallback when `Accept-Language` + negotiation finds no `zh` — matching the locked browser-detection rule. +- **Directory migration:** all pages move under `src/app/[locale]/` + (`page.tsx`, `login/page.tsx`, `layout.tsx`). **API routes stay at + `src/app/api/`** — outside the locale segment. +- **Proxy composition:** `src/proxy.ts` combines the existing auth proxy + with next-intl middleware. Auth redirects become locale-aware + (`/login` → `/zh/login`). Public-path allowlist logic is preserved; + unprefixed legacy URLs 301 to the prefixed form with query preserved + (handled by next-intl middleware). +- **Root layout:** `src/app/[locale]/layout.tsx` validates the locale param + (`notFound()` on unknown), provides `NextIntlClientProvider` with the + locale's messages. Compatible with the System config feature's + `force-dynamic` + `generateMetadata` (dynamic rendering; next-intl + resolves locale per-request). The configured wordmark/favicon metadata + logic carries over unchanged. +- **Navigation:** all `Link`/`useRouter`/`usePathname` imports switch to + `@/i18n/navigation` (next-intl `createNavigation`) so internal + navigation retains the current locale automatically. +- **Server components & route handlers:** `getTranslations` in RSC; + API routes do not translate user-facing messages (see §4). + +## 2. Message catalog + +- `src/i18n/messages/{en,zh}/` with per-domain modules: + `common` (shared buttons/states), `header`, `threadList`, `chat`, + `dialogs` (config / imageModels / systemConfig / account / changePassword), + `panels` (skills / memory / schedule / workspace / inspector), `login`, + `errors` (API code mapping). `index.ts` aggregates into one Messages + object per locale. +- **Keys:** semantic (`chat.inputPlaceholder`, `systemConfig.tabs.security`), + never English source text. Interpolation via ICU (`{count}`, + `{min}`/`{max}`). Both trees structurally identical. +- **Type safety:** `IntlMessages` type augmentation (next-intl) so `t()` + key typos are compile errors. +- **Formatting:** dates/numbers via next-intl `useFormatter` / + `Intl.DateTimeFormat` per locale — never hand-rolled. + +## 3. Client integration + +- Components use `useTranslations("")` + `t("key")`; RSC uses + `getTranslations`. +- **LocaleSwitcher** in the header next to `ThemeToggle`: dropdown listing + English / 中文 with a check on the active locale; on select, + `router.replace(pathname, { locale })` from `@/i18n/navigation` — + query string preserved. +- Login page is covered by the same `[locale]/layout.tsx` provider + (unauthenticated). Branding wordmark and login-terms content come from + `/api/system/config/public` and are admin-entered **data — not + translated**. +- Not translated: agent output, workspace file content, thread titles, + error-log store entries, server logs. + +## 4. Error message localization (sub2api code-mapping pattern) + +- **Server:** `routeErrorResponse` already emits `{ code, message }`. + Audit the remaining hand-written `NextResponse.json({ error: ... })` + routes and unify them onto stable `{ code, message }` with an enumerated + code set (`INVALID_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, + `PAYLOAD_TOO_LARGE`, `UNSUPPORTED_MEDIA_TYPE`, `CONFLICT`, `INTERNAL`, + plus domain codes already in use). The English `message` stays as the + fallback. No per-request locale negotiation on the server. +- **Client:** `src/lib/i18nError.ts` — `localizedErrorMessage(err, t, namespace?)`: + extract `code` from the error body → look up `errors.` (or + `.errors.` when a namespace is given) with ICU metadata + interpolation → on miss, fall back to the server `message`. +- **Single entry point:** `errorToast` routes through the mapper so every + existing dialog/panel toast benefits without per-call-site changes; the + few components rendering `body.error` directly are converted one by one + in batch 6. + +## 5. Migration batches + +Six batches, each independently deliverable with the suite green +(baseline: 340 passed / 1 skipped, `tsc` clean): + +1. **Infrastructure:** next-intl dependency, `[locale]` directory migration + (pure move, no translation), proxy composition, `@/i18n/navigation`, + empty en/zh catalog skeletons + the three catalog tests. Verify legacy + 301s, build, and `force-dynamic` compatibility. +2. **Chrome + switcher:** layout, header, login page, navigation menus into + the catalog; LocaleSwitcher ships. Untranslated areas still English. +3. **Main chat:** ChatInterface, ThreadList, message rendering, composer. +4. **Dialogs:** Config, ImageModels, SystemConfig (4 tabs), Account, + ChangePassword, plus their toasts. +5. **Panels:** Skills, Memory, Schedule, Workspace, Inspector, empty states. +6. **Errors:** server code audit/unification + `errors.*` catalog + + errorToast mapping + stray direct-`error` renderers. + +## Error handling + +- Unknown locale in URL → `notFound()`. +- Missing key in one locale → parity test fails in CI (prevented, not + handled at runtime); next-intl dev-mode onError warns. +- Unmapped error code → server English message displayed (never blank). +- Corrupt/missing catalog module → build failure (imports are static). + +## Testing & verification + +- **Catalog tests (vitest, land in batch 1):** + 1. en/zh key-set parity + ICU variable-set parity. + 2. Aggregation key-collision (sub2api pattern — spread modules must not + shadow each other). + 3. `errors.*` covers every code `routeErrorResponse` can emit. +- Existing tests asserting English copy are updated per batch (assert keys + or parametrize both locales). Suite stays green throughout. +- Manual gate per batch (dev :4716): switched areas render in both + languages; batch 6 ends with a full-site pass in both languages, + including: switcher preserves `threadId` query, legacy unprefixed link + 301s, login page + terms card, System config dialog in zh, error toast + localization (e.g. oversize logo upload), auth redirect → `/zh/login`. + +## Out of scope + +- Server-side message translation via Accept-Language (rejected in favor + of code mapping, 2026-08-11). +- Locale-prefixed API routes; translating agent/backend-generated content. +- Translating admin-entered data (branding wordmark, login terms). +- localStorage/account-persisted locale (URL + cookie chosen). +- More than two locales; RTL languages. +- Translating errorLogStore entries / server logs (admin diagnostics stay + English).