Skip to content

feat: i18n infrastructure + en/zh-CN translations + per-locale demos#9

Merged
hqhq1025 merged 4 commits intomainfrom
wt/i18n-v2
Apr 18, 2026
Merged

feat: i18n infrastructure + en/zh-CN translations + per-locale demos#9
hqhq1025 merged 4 commits intomainfrom
wt/i18n-v2

Conversation

@hqhq1025
Copy link
Copy Markdown
Collaborator

Goal

Land the i18n substrate so the app can ship in English and Simplified Chinese without any further infrastructure work. Strings, demos, and the main-process locale IPC are all in place. Renderer wiring (App.tsx, store, preload) is intentionally not in this PR — those files are owned by the parallel wt/preview-ux-v2 branch and will be hooked up by the maintainer in a follow-up integration commit.

This is the rebuilt successor to the abandoned wt/i18n branch: same substance, clean diff against today's main, zero touches to renderer files.

Files in

Path Purpose
packages/i18n/package.json + tsconfig.json New workspace package @open-codesign/i18n
packages/i18n/src/index.ts initI18n / setLocale / useT / normalizeLocale / isSupportedLocale / availableLocales / getCurrentLocale
packages/i18n/src/locales/{en,zh-CN}.json Full key tree across common, preview, chat, settings, onboarding, commands, errors, demos
packages/i18n/src/i18n.test.ts 8 vitest cases: normalization, support detection, live switching, missing-key warning
packages/templates/src/locales/{en,zh-CN}.ts Per-locale DemoTemplate[] for the four built-in demos
packages/templates/src/index.ts Adds getDemos(locale) / getDemo(id, locale). BUILTIN_DEMOS retained as English alias for back-compat.
packages/templates/package.json Adds @open-codesign/i18n workspace dep
apps/desktop/src/main/locale-ipc.ts registerLocaleIpc() exposing locale:get-system, locale:get-current, locale:set — persists to ~/.config/open-codesign/locale.json (separate from config.toml, with schemaVersion: 1)
docs/I18N.md How to add a string, add a locale, naming convention

Files out (parallel-worktree boundary — NOT touched)

  • apps/desktop/src/main/index.ts
  • apps/desktop/src/preload/index.ts
  • apps/desktop/src/renderer/src/App.tsx
  • apps/desktop/src/renderer/src/store.ts
  • apps/desktop/src/renderer/src/main.tsx
  • apps/desktop/src/renderer/src/onboarding/**
  • apps/desktop/src/renderer/src/components/**

New production dependencies

Dep Version Size (min+gzip) License Why
i18next ^23.16.5 ~14 KB gzip (~50 KB raw) MIT De-facto JS i18n core; tree-shakable, no runtime fetching
react-i18next ^15.1.3 ~7 KB gzip (~25 KB raw) MIT React bindings (useTranslation hook) for i18next

Both are MIT, Apache-2.0 compatible, zero transitive bloat. Combined raw cost: < 80 KB. Well inside the §1 lean budget. No alternatives considered seriously: react-intl is ~3x the size, hand-rolling means re-implementing pluralization rules.

Hard rules respected

  • No silent fallbacks (§10): missing keys log console.warn(key, namespace, locale) and render as \u27E6key\u27E7 in dev so they jump out in the UI.
  • Schema-versioned persistence (§5b): locale file carries schemaVersion: 1.
  • TypeScript strict, no any, bracket notation, import type for type-only imports.
  • Lazy-friendly: i18next initializes on first render via initI18n(locale); resources are inlined JSON, no async fetch.
  • DCO sign-off on every commit.

Integration notes for the maintainer

After wt/preview-ux-v2 lands, wire up the renderer in a single follow-up commit. Three steps:

1. Main process — apps/desktop/src/main/index.ts

import { registerLocaleIpc } from './locale-ipc';
// inside app.whenReady():
registerLocaleIpc();

2. Preload — apps/desktop/src/preload/index.ts

Add to the bridged API:

locale: {
  getSystem: () => ipcRenderer.invoke('locale:get-system'),
  getCurrent: () => ipcRenderer.invoke('locale:get-current'),
  set: (locale: string) => ipcRenderer.invoke('locale:set', locale),
}

3. Renderer — apps/desktop/src/renderer/src/main.tsx

import { initI18n } from '@open-codesign/i18n';
const initialLocale = await window.electronAPI.locale.getCurrent();
await initI18n(initialLocale);
// then mount <App />

4. t(...) migration table

Replace each hard-coded string in App.tsx, onboarding, and components with the matching key. Use const t = useT() at the top of each component.

Hard-coded string New call
'pre-alpha' chip t('common.preAlpha')
'BYOK · local-first · multi-model' t('common.tagline')
'Send' button t('common.send')
Settings labels t('settings.title'), t('settings.tabs.models'), etc.
Welcome screen title t('onboarding.welcome.title')
Welcome subtitle t('onboarding.welcome.subtitle')
Try-free / use-key / use-Ollama buttons t('onboarding.welcome.{tryFree,useKey,useOllama}')
Paste-key title / placeholder t('onboarding.paste.{title,placeholder}')
Detected provider toast t('onboarding.paste.recognized', { provider })
Connected models count t('onboarding.paste.connected', { count })
Advanced base-URL section t('onboarding.paste.advanced.{title,description}')
Paste-key error 401/402/429/network t('onboarding.paste.errors.{401,402,429,network}')
Choose model title / labels t('onboarding.choose.{title,primary,fast,start}'), t('onboarding.choose.estimatedCost', { amount })
Preview empty state t('preview.empty.{title,body,starterChip}')
Preview loading t('preview.loading.title')
Preview error t('preview.error.{title,body,copyError}')
Chat input placeholder t('chat.placeholder')
Chat send shortcut hint t('chat.sendShortcut')
Command palette items t('commands.{title,placeholder}'), t('commands.items.{newDesign,toggleTheme,openSettings,export}')
Generic error toast t('errors.generic') / t('errors.providerError', { message }) / t('errors.providerAuthMissing')

For demos, swap BUILTIN_DEMOS import to getDemos(currentLocale) (and have the store hold currentLocale). The BUILTIN_DEMOS alias stays in place during migration so nothing breaks mid-flight.

5. Settings → Language dropdown

Three options: System default (passes undefined so normalizeLocale falls back to app.getLocale()), English, 中文. On change: await window.electronAPI.locale.set(value); await setLocale(value); and re-render.

§5b checklist

  • Compatible: ✅ — BUILTIN_DEMOS alias kept; new IPC channels are net-additive; locale file is schema-versioned.
  • Upgradeable: ✅ — adding a third locale is an add-only change (one JSON, one TS file, one availableLocales entry). Documented in docs/I18N.md.
  • No bloat: ✅ — two prod deps totaling < 80 KB gzipped; both peer-friendly; no transitive heavies pulled in.
  • Elegant: ✅ — no silent fallbacks, missing keys visibly fail in dev, idiomatic Chinese (not machine-literal), demo prompts feel native in both languages.

Verification

pnpm install          # ok
pnpm -r typecheck     # green across all 11 packages
pnpm lint             # green (1 pre-existing warning unrelated to this PR)
pnpm -r test          # 28 tests pass (8 new in i18n)

Ships an i18next-backed translation layer with two locales (English and
Simplified Chinese), a normalize/detect helper that coalesces zh-Hans*
variants, and a missing-key warner that surfaces gaps as visible markers
in dev. No silent fallbacks.

Signed-off-by: hqhq1025 <1506751656@qq.com>
Splits the four built-in demos into ./locales/en.ts and ./locales/zh-CN.ts
and exposes locale-aware getDemos() / getDemo(). BUILTIN_DEMOS is kept as
an English alias so renderer code that has not migrated yet keeps working.

Signed-off-by: hqhq1025 <1506751656@qq.com>
Adds registerLocaleIpc() exposing locale:get-system, locale:get-current,
and locale:set. Persists user choice to ~/.config/open-codesign/locale.json
with a schemaVersion field — separate from config.toml so i18n can boot
before the encrypted config loader finishes. Renderer wiring is deferred
to the maintainer because apps/desktop/src/main/index.ts and the preload
bridge are owned by the parallel preview-ux-v2 branch.

Signed-off-by: hqhq1025 <1506751656@qq.com>
@hqhq1025 hqhq1025 merged commit 5e84085 into main Apr 18, 2026
7 checks passed
@hqhq1025 hqhq1025 deleted the wt/i18n-v2 branch April 18, 2026 07:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant