Skip to content

feat: i18n with en + zh-CN, namespaced keys, system locale auto-detect#2

Closed
hqhq1025 wants to merge 1 commit intomainfrom
wt/i18n
Closed

feat: i18n with en + zh-CN, namespaced keys, system locale auto-detect#2
hqhq1025 wants to merge 1 commit intomainfrom
wt/i18n

Conversation

@hqhq1025
Copy link
Copy Markdown
Collaborator

Summary

Ships internationalization infrastructure for the renderer, with English and Simplified Chinese as the tier-1 locales. Adds the @open-codesign/i18n workspace package, per-locale demo prompts in @open-codesign/templates, an Electron locale IPC channel, and the renderer wiring needed to swap the entire UI live.

Namespaces

All keys live in packages/i18n/src/locales/{en,zh-CN}.json. Top-level namespaces:

  • common — appName, send, cancel, retry, save, close, settings, advanced, about, learnMore, copy, copied, comingSoon, loading, preAlpha, tagline
  • preview — empty.{title,body,starterChip}, loading.title, error.{title,body,copyError}, ready, noDesign
  • chat — placeholder, sendShortcut, emptyHint
  • settings — title, tabs.{models,appearance,storage,advanced}, language.{label,system}, theme.{label,light,dark,system}
  • onboarding — welcome.{title,subtitle,tryFree,useKey,useOllama}, paste.{title,placeholder,recognized,connected_one,connected_other,howToGet,errors.{401,402,429,network}}, choose.{title,primary,fast,estimatedCost,start}
  • commands — title, placeholder, items.{newDesign,toggleTheme,openSettings,export}
  • errors — generic, providerAuthMissing, providerError, ipcBadInput, exporterNotReady
  • demos — meditationApp, caseStudy, pitchDeck, marketingLanding (each with title/description/prompt)

Dependencies added

Package Size License
i18next@^23.16.5 ~50 KB MIT
react-i18next@^15.1.3 ~25 KB MIT

No other production deps. Stays well under the 30-prod-dep / 80 MB install budget.

Missing-key policy (PRINCIPLES §10)

Never silent. Concretely:

  • missingKeyHandler logs console.warn with key path + namespace + active locale.
  • parseMissingKeyHandler renders ⟦key⟧ in dev mode (loud) and the raw key in prod (visible, never a fake fallback).
  • normalizeLocale warns when given an unsupported locale and falls back to en.

Locale persistence

Saved to ~/.config/open-codesign/locale.json via the new locale:get-system / locale:get-current / locale:set IPC channels. This lives outside config.toml until wt/onboarding lands; once it does, fold the value into [ui] locale = "zh-CN" and remove the standalone file.

Integration notes for parallel worktrees

wt/onboarding — onboarding namespace is reserved with the keys you need:

  • onboarding.welcome.{title,subtitle,tryFree,useKey,useOllama}
  • onboarding.paste.{title,placeholder,recognized,connected_one,connected_other,howToGet,errors.{401,402,429,network}}
  • onboarding.choose.{title,primary,fast,estimatedCost,start}

Use useT() from @open-codesign/i18n. Pluralization works via the _one / _other suffix; interpolation via {{name}}. When you wire config.toml, read ui.locale and call useCodesignStore.getState().setLocale(value) on boot, then delete apps/desktop/src/main/locale-ipc.ts's file persistence (keep the IPC channel, switch the body to read the TOML).

wt/preview-ux — keys provided in the settings, preview, commands namespaces. Settings → Appearance should render a language picker bound to setLocale() from the store (showing availableLocales from @open-codesign/i18n). Top bar tagline is common.tagline, pre-alpha pill is common.preAlpha.

Test plan

  • pnpm install — clean
  • pnpm -r typecheck — green
  • pnpm lint — green (1 pre-existing warning unchanged)
  • pnpm -r test — green (6 new vitest cases in @open-codesign/i18n)
  • pnpm --filter @open-codesign/desktop dev and verify:
    • app boots in system locale (zh-CN on Chinese macOS, en otherwise)
    • starter prompts render translated titles + descriptions
    • chat placeholder reads "想设计什么?" in zh-CN, "Describe what to design…" in en
    • pre-alpha tag reads "预览版" in zh-CN
    • useCodesignStore.getState().setLocale('zh-CN') from DevTools swaps the UI live
    • unsupported locale (setLocale('fr')) falls back to en with a console warning
    • kill + relaunch restores the previous locale from ~/.config/open-codesign/locale.json

Files NOT touched

Per worktree boundaries: apps/desktop/src/renderer/src/onboarding/**, apps/desktop/src/renderer/src/components/**, apps/desktop/src/main/{config,keychain,onboarding-ipc}.ts, and packages/{providers,runtime,exporters,artifacts,core,ui,shared}/**.

Signed-off-by: dco

hqhq1025 added a commit that referenced this pull request Apr 18, 2026
Both PRs (#1 onboarding, #2 i18n) failed because:

1. Dependency Review requires GitHub Advanced Security on private repos.
   Add `visibility == 'public'` gate, mirroring codeql.yml and scorecard.yml.

2. Codex PR Review and Issue Auto Response require OPENAI_API_KEY +
   OPENAI_BASE_URL secrets that don't exist in this repo yet (they're
   org-level on open-cowork only). Add `vars.CODEX_BOT_ENABLED == 'true'`
   guard so the workflows are no-ops until the variable is set in repo
   settings (Settings → Secrets and variables → Actions → Variables).

To enable bot review later: set `CODEX_BOT_ENABLED=true` and add
`OPENAI_API_KEY` + `OPENAI_BASE_URL` repo (or org) secrets.

Signed-off-by: Haoqing Wang <1506751656@qq.com>
Adds @open-codesign/i18n built on i18next + react-i18next, plus per-locale
demo prompts in @open-codesign/templates and the renderer wiring needed to
swap the entire UI live.

- New package packages/i18n (deps: i18next ~50KB, react-i18next ~25KB)
  with namespaced JSON resources for common, preview, chat, settings,
  onboarding, commands, errors, demos.
- Locale auto-detect via Electron app.getLocale() on first run, persisted
  to ~/.config/open-codesign/locale.json (locale-ipc) until wt/onboarding
  folds it into config.toml under ui.locale.
- Renderer boots i18n before createRoot; useCodesignStore exposes a
  locale slice + setLocale that updates persistence and UI atomically.
- BUILTIN_DEMOS becomes getDemos(locale) returning the per-locale variant.
- Missing keys are loud: console.warn + dev-mode bracket markers, never
  silent (PRINCIPLES §10).
- Vitest covers normalization, supported-locale detection, and live
  locale switching.

Tier 1 scope: only en and zh-CN. Translation keys stay shallow so they
remain re-keyable.

Signed-off-by: Haoqing Wang <1506751656@qq.com>
@hqhq1025
Copy link
Copy Markdown
Collaborator Author

Superseded by v2 (clean cherry-pick on top of latest main). See 2-v2 PR.

@hqhq1025 hqhq1025 closed this Apr 18, 2026
@hqhq1025 hqhq1025 deleted the wt/i18n branch April 18, 2026 07:48
hqhq1025 added a commit that referenced this pull request Apr 18, 2026
Major #1: cancelGeneration now waits for the IPC round-trip before
clearing isGenerating/activeGenerationId. If main rejects the cancel
(bad id, already aborted) the error becomes visible via toast instead
of being silently swallowed. Updated store test to reflect async cancel
and drain microtasks between cancel and the follow-up sendPrompt.

Major #2: add GeneratePayloadV1 (schemaVersion: 1, generationId required)
to @open-codesign/shared and rename the IPC channel to codesign:v1:generate,
matching the pattern established by codesign:v1:cancel-generation. Preload
builds the v1 envelope with satisfies check; main validates with
GeneratePayloadV1.parse(). Add 5 schema tests for missing/wrong schemaVersion
and empty generationId. Add notifications.cancellationFailed i18n key.

Signed-off-by: hqhq1025 <1506751656@qq.com>
hqhq1025 added a commit that referenced this pull request Apr 18, 2026
Major #1: cancelGeneration now waits for the IPC round-trip before
clearing isGenerating/activeGenerationId. If main rejects the cancel
(bad id, already aborted) the error becomes visible via toast instead
of being silently swallowed. Updated store test to reflect async cancel
and drain microtasks between cancel and the follow-up sendPrompt.

Major #2: add GeneratePayloadV1 (schemaVersion: 1, generationId required)
to @open-codesign/shared and rename the IPC channel to codesign:v1:generate,
matching the pattern established by codesign:v1:cancel-generation. Preload
builds the v1 envelope with satisfies check; main validates with
GeneratePayloadV1.parse(). Add 5 schema tests for missing/wrong schemaVersion
and empty generationId. Add notifications.cancellationFailed i18n key.

Signed-off-by: hqhq1025 <1506751656@qq.com>
hqhq1025 added a commit that referenced this pull request Apr 18, 2026
* feat(desktop): redesign sidebar input + add cancellation IPC

- Replace single-line input with autosize textarea (1–6 rows, max-h 144 px)
- Move send button into bottom-right corner of textarea container, icon-only 28 px
- During generation, send button becomes a stop button (Square icon) wired to cancelGeneration
- Remove redundant keyboard-hint bar; fold hint into placeholder text
- Enter sends, Shift+Enter inserts newline, ⌘↵ global send preserved
- main: extract runGenerate helper; maintain Map<id, AbortController> for in-flight
  requests; new codesign:cancel-generation IPC handler; logger scope 'ipc'
- preload: expose cancelGeneration(id) on window.codesign
- shared: GeneratePayload accepts optional generationId
- store: add activeGenerationId state + cancelGeneration action; suppress abort
  errors silently without pushing an error toast

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix: harden generation cancellation flows

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix: version cancellation ipc and tokenise sidebar

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix: preserve current-generation failures

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): guard cancel race + version IPC payload + add tests

- applyGenerateSuccess/applyGenerateError already guarded via
  finishIfCurrent and the early-return id check; confirm with tests
- Add noop test: unknown generationId leaves all in-flight intact
- Add schema rejection test: empty generationId and missing
  schemaVersion both throw via CancelGenerationPayloadV1.parse
- Fix biome formatting in store.test.ts (pre-existing)

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): properly merge rebase onto main — restore all origin/main features

Rebase onto origin/main exposed conflicts where PRs #18, #19, #20 had
advanced store, preload, shared and main/index.ts beyond the branch
point. Resolve by taking origin/main as the base and layering our
cancellation additions on top:

- shared/index.ts: add LocalInputFile, SelectedElement, ElementSelectionRect,
  ApplyCommentPayload, StoredDesignSystem exports; add generationId to
  GeneratePayload; keep CancelGenerationPayloadV1
- preload/index.ts: add locale API, pickInputFiles, pickDesignSystemDirectory,
  clearDesignSystem, applyComment; add generationId to generate payload;
  keep cancelGeneration with schemaVersion: 1
- main/index.ts: add applyComment, scanDesignSystem, preparePromptContext,
  registerLocaleIpc; wire AbortController into generate handler; keep
  codesign:v1:cancel-generation handler
- store.ts: merge all origin/main state (selectedElement, inputFiles,
  referenceUrl, i18n tr(), applyInlineComment, etc.) with PR's
  activeGenerationId / cancelGeneration / finishIfCurrent guards
- store.test.ts: update sendPrompt calls to object form; add designSystem:
  null to OnboardingState fixture; relax toast title assertion for i18n

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): wait for cancel IPC ack + version generate payload

Major #1: cancelGeneration now waits for the IPC round-trip before
clearing isGenerating/activeGenerationId. If main rejects the cancel
(bad id, already aborted) the error becomes visible via toast instead
of being silently swallowed. Updated store test to reflect async cancel
and drain microtasks between cancel and the follow-up sendPrompt.

Major #2: add GeneratePayloadV1 (schemaVersion: 1, generationId required)
to @open-codesign/shared and rename the IPC channel to codesign:v1:generate,
matching the pattern established by codesign:v1:cancel-generation. Preload
builds the v1 envelope with satisfies check; main validates with
GeneratePayloadV1.parse(). Add 5 schema tests for missing/wrong schemaVersion
and empty generationId. Add notifications.cancellationFailed i18n key.

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): restore legacy generate IPC shim + use ui tokens in sidebar

- Add codesign:generate legacy handler that accepts old GeneratePayload
  (no schemaVersion) and promotes it to V1 by injecting schemaVersion:1
  and falling back generationId to gen-${Date.now()} when absent.
  Emits a warn log to track removal in the next minor release.
- Add --color-on-accent token to packages/ui tokens.css (light + dark)
  so accent-button text never hard-codes a color value.
- Replace text-white in Sidebar.tsx with text-[var(--color-on-accent)]
  on both the stop and send IconButton variants.
- Add 3 tests for GeneratePayload legacy schema and the V1 promotion
  path that the shim relies on.

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): surface error when renderer bridge missing during cancel

Split the combined `!id || !window.codesign` guard in `cancelGeneration`
into two separate checks. A missing `activeGenerationId` is a legitimate
no-op, but a missing IPC bridge is a real error that must be surfaced via
`errorMessage`/`lastError` and a toast. Adds a Vitest test to cover the
new error path (with `beforeAll` i18n init so `tr()` keys resolve).

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): restore attachments/refURL/designSystem entries + i18n sidebar strings

Restores the local context panel (attachments, reference URL, design
system) that was accidentally dropped during rebase when the cancel
button was added.  All hardcoded English strings are now routed through
useT(); chat.send and chat.stop keys were added to en.json / zh-CN.json.

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): fix import order and trailing blank line after rebase

Biome import sort and formatter fixes introduced during rebase conflict
resolution.

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): remove hardcoded textarea fallback, throw on missing tokens

Removes FALLBACK_FONT_SIZE and FALLBACK_LINE_HEIGHT_MULTIPLIER constants
from getTextareaLineHeight. When computed lineHeight is not a finite
positive number and either fontSize or --leading-body token is absent or
invalid, the function now throws instead of silently using magic numbers.
The --leading-body token is already defined in packages/ui/src/tokens.css.

Updates Sidebar.test.ts: replaces the "falls back" test with two tests —
one verifying valid-token multiplication and one verifying the throw path.

Signed-off-by: hqhq1025 <1506751656@qq.com>

---------

Signed-off-by: hqhq1025 <1506751656@qq.com>
Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Co-authored-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
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