feat(i18n): add Simplified Chinese (zh-CN) translation - #8765
Conversation
Adds a full Simplified Chinese translation for the Goose Desktop UI and
patches the remaining hard-coded English strings so the whole app can be
localised consistently.
What's included:
* ui/desktop/src/i18n/messages/zh-CN.json — translation catalog covering
all 1574 message IDs extracted from the source (verified by
pnpm i18n:check).
* ui/desktop/src/i18n/index.ts — registers zh-CN in SUPPORTED_LOCALES
and makes locale detection smarter:
- checks navigator.languages (not just navigator.language), so a
Chinese-speaking user on an English Windows UI with Region set
to China still gets Chinese;
- maps all Simplified variants (zh, zh-CN, zh-Hans, zh-Hans-CN,
zh-SG, zh-MY) to the zh-CN catalog;
- Traditional variants (zh-TW, zh-HK, zh-Hant*) intentionally fall
through to English — they are a separate language.
* ui/desktop/src/main.ts — picks up app.getSystemLocale() after
app.whenReady() and feeds it into appConfig.GOOSE_LOCALE, so the
renderer prefers OS region over UI language. Also adds a small
MENU_TRANSLATIONS_ZH_CN dictionary + translateMenuLabels() helper
that rewrites Electron's default application-menu labels
(File / Edit / View / Window / Help and their submenu items) into
the active locale before Menu.setApplicationMenu is called.
* ui/desktop/src/hooks/useNavigationItems.ts — adds a getNavItemLabel
helper that looks up an i18n descriptor for each nav item, reusing
the existing navigationCustomization.item* message ids; adds a new
navigationCustomization.itemSkills message for the Skills nav entry.
* ui/desktop/src/components/Layout/{Condensed,Expanded}Renderer.tsx —
switch from {item.label} to {getNavItemLabel(item, intl)} so the
sidebar / overlay use the translated nav names.
* ui/desktop/src/components/ChatInput.tsx — localise the "Send" button
text (previously the only hard-coded label on the submit button)
using the existing chatInput.send message id.
* ui/desktop/src/utils/keyboardShortcuts.ts — getNavigationShortcutText
now takes an optional IntlShape and returns a localised
"Ctrl+↑/Ctrl+↓ to navigate messages" placeholder, via a new
chatInput.navigationShortcut ICU message with a {prefix} parameter
so the ⌘/Ctrl+ prefix stays platform-correct.
* ui/desktop/src/i18n/messages/en.json — regenerated with
pnpm i18n:extract to include the two new message ids
(chatInput.navigationShortcut, navigationCustomization.itemSkills).
Verification:
pnpm run typecheck # clean
pnpm run lint:check # clean (includes i18n:check)
pnpm run test:run src/i18n/i18n.test.ts # 9/9 passing
pnpm run package # produced a working Goose.exe
Goose launched on Windows 11 with Region = China and navigator.language
= en-US → app started without errors, every i18n-routed string renders
in Simplified Chinese, native menu labels localised.
Notes for reviewers:
* compile output (src/i18n/compiled/*.json) is gitignored and not
included — it is regenerated from the messages files on every build.
* The menu-label translation path is intentionally isolated in main.ts;
adding more locales later is just a matter of dropping another
MENU_TRANSLATIONS_<locale> table and extending detectMenuLocale().
* Terminology choices (Session → 会话, Recipe → 配方,
Extension → 扩展, Provider → 提供商, Prompt → 提示词,
Token → 令牌, Context window → 上下文窗口, Subrecipe → 子配方)
follow common Chinese-developer conventions; happy to revise during
review if the project prefers something else.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 15ad55669d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
|
||
| // Simplified Chinese variants | ||
| const isTraditional = /^zh-(hant|tw|hk|mo)\b/.test(lower); | ||
| if (!isTraditional && (lower === 'zh' || lower.startsWith('zh-'))) { |
There was a problem hiding this comment.
Accept zh_CN tags in renderer locale matching
matchSupported only treats zh and zh-* as Simplified Chinese, so an explicit GOOSE_LOCALE like zh_CN (a common POSIX locale form) is not recognized and falls through to navigator/English. This makes the documented env override unreliable for Chinese users depending on underscore-form tags. Normalize underscores to hyphens (or explicitly handle zh_) before the Traditional/Simplified checks so explicit locale selection is honored.
Useful? React with 👍 / 👎.
| const isTraditional = /^zh-(hant|tw|hk|mo)\b/.test(lower); | ||
| const isSimplifiedChinese = !isTraditional && (lower === 'zh' || lower.startsWith('zh-') || lower.startsWith('zh_')); |
There was a problem hiding this comment.
Reject zh_TW underscore tags in menu translation
The menu locale matcher classifies any zh_ locale as Simplified Chinese, but the Traditional guard only checks hyphenated tags. As a result, zh_TW/zh_HK are incorrectly translated with the zh-CN menu dictionary instead of falling through as intended for Traditional Chinese. Apply the same normalization or underscore-aware Traditional check before setting isSimplifiedChinese.
Useful? React with 👍 / 👎.
Fixes two bugs flagged in code review: 1. matchSupported in index.ts: GOOSE_LOCALE=zh_CN (POSIX form) now matches zh-CN 2. menuT in main.ts: zh_TW no longer incorrectly gets Simplified Chinese translations Signed-off-by: Douwe Osinga <douwe@squareup.com>
DOsinga
left a comment
There was a problem hiding this comment.
LGTM. The two locale-detection bugs (underscore normalization) have been fixed in 72ca185 — GOOSE_LOCALE=zh_CN now correctly matches zh-CN, and zh_TW no longer incorrectly triggers Simplified Chinese translations.
Merged with origin/main, no conflicts. All 343 desktop tests pass including the 9 i18n tests. Lint clean.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3daba484f8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) { | ||
| for (const tag of navigator.languages) { | ||
| if (tag) candidates.push(tag); | ||
| } | ||
| } |
There was a problem hiding this comment.
Add navigator.language fallback in locale detection
The new getLocale path only reads navigator.languages, so when that array is unavailable or empty (common in jsdom tests and some embedded browser contexts), locale resolution skips the browser locale entirely and falls back to English unless GOOSE_LOCALE is set. This is a regression from the previous behavior that used navigator.language, and it can silently disable both regional formatting and message catalog selection for users whose locale is otherwise detectable.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Electron ships Chromium, which has had navigator.languages since 2014. There's no Electron runtime where it's unavailable — the only place that case can occur is in jsdom tests, which we control directly.
| // the raw label strings used below. | ||
| // ======================================================================= | ||
|
|
||
| const MENU_TRANSLATIONS_ZH_CN: Record<string, string> = { |
There was a problem hiding this comment.
At the moment this works for one language. My concern is that adding a new language requires adding another MENU_TRANSLATIONS_ table in main.ts and keeping it in sync by hand — these strings aren't picked up by pnpm i18n:extract / i18n:check, so translators working in .json won't see them.
Not a blocker for this PR. It would be good to have a follow-up PR to migrate the main-process menu strings to id-based lookups against the same messages/.json the renderer uses.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dd9e887476
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const tag = resolveChineseAlias(normalized); | ||
|
|
||
| // Exact match first | ||
| if (SUPPORTED_LOCALES.has(tag)) return { locale: tag, messageLocale: tag }; |
There was a problem hiding this comment.
Preserve regional locale when aliasing zh variants
getLocale currently rewrites any non-Traditional zh-* tag to zh-CN before both catalog selection and locale return, so users with locales like zh-SG or zh-MY lose their original regional locale for Intl formatting. This means date/number/currency formatting will follow mainland conventions even when the OS/browser region is Singapore or Malaysia. Keep the aliasing for messageLocale, but preserve the canonicalized original tag for locale when it is a valid BCP 47 locale.
Useful? React with 👍 / 👎.
Signed-off-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: antai <antai12232931@anaiguo.com> Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Lifei Zhou <lifei@squareup.com>
Summary
Adds a full Simplified Chinese (zh-CN) translation for the Goose Desktop UI, and fixes the remaining hard-coded English strings so the whole app can be localised consistently.
All 1574
react-intlmessage ids are translated (verified bypnpm i18n:check). The native menu bar and sidebar nav — previously hard-coded — now also respect the active locale.What's included
Translation catalog
ui/desktop/src/i18n/messages/zh-CN.json— new catalog covering every extracted message id.Terminology (open to review):
Locale detection (
ui/desktop/src/i18n/index.ts)zh-CNinSUPPORTED_LOCALES.navigator.languages(the full preference list) instead of justnavigator.language, so a Chinese-speaking user on an English Windows UI with Region set to China still gets Chinese.zh,zh-CN,zh-Hans,zh-Hans-CN,zh-SG,zh-MY) to thezh-CNcatalog.zh-TW,zh-HK,zh-Hant*) are intentionally not matched — they fall through to English. Adding azh-TWcatalog later is a separate drop-in.System locale injection (
ui/desktop/src/main.ts)app.whenReady(), fillsappConfig.GOOSE_LOCALEfromapp.getSystemLocale()unless the env var is already set. This prefers OS region over UI language, which is what users on en-US Windows + zh-CN Region actually want.MENU_TRANSLATIONS_ZH_CNtable + atranslateMenuLabels()helper that rewrites Electron's default application-menu labels (File / Edit / View / Window / Help and their submenu items like Cut/Copy/Paste/Undo/Redo/Zoom…) beforeMenu.setApplicationMenuinstalls them. Adding more locales is a one-liner — just drop in anotherMENU_TRANSLATIONS_<locale>table.Sidebar nav (
ui/desktop/src/hooks/useNavigationItems.ts+ renderers)getNavItemLabel(item, intl)helper that resolves each nav item viadefineMessages, reusing the existingnavigationCustomization.item*ids (adds a newnavigationCustomization.itemSkillsso the Skills entry has one).CondensedRenderer.tsxandExpandedRenderer.tsxnow render{getNavItemLabel(item, intl)}instead of{item.label}.Remaining hard-coded strings
ChatInput.tsxnow usesintl.formatMessage(i18n.send)(thechatInput.sendmessage already existed but was only used as a tooltip/title, not for the button text).getNavigationShortcutTextnow accepts an optionalIntlShape. Uses a newchatInput.navigationShortcutICU message with a{prefix}argument so the ⌘ vs Ctrl+ prefix stays platform-correct.Regenerated
en.jsonpnpm i18n:extractwas re-run after the source changes; two new ids were added:chatInput.navigationShortcutnavigationCustomization.itemSkillsVerification
Ran locally on Windows 11 (x64, Node 24.15.0, pnpm 10.33.1):
Then launched the packaged
Goose.exeon Windows 11 with UI language = en-US, Region = China (Simplified) andnavigator.language = "en-US":app.getSystemLocale()now only fires afterwhenReady(); see the small main-process patch).zh-CN.Ctrl+↑/Ctrl+↓ 浏览消息.On English locales everything is unchanged (fall-through).
Notes for reviewers
src/i18n/compiled/*.jsonis gitignored and not included — it is regenerated from the message files on every build. Nothing in this PR touches the build pipeline.main.tsbecause the main process can't usereact-intl. I kept the dictionary small and only translated labels that actually reach the user.Test plan
app.getSystemLocale()), Windows, and Linux for a user whose OS region is set to China.GOOSE_LOCALE=zh-CN ./goose-appoverrides locale as before.en-GBstill get English (no regression — base-language fallback preserved).pnpm i18n:extract+pnpm i18n:checkstill pass on CI.Menu.getApplicationMenu()path.