Skip to content

feat(i18n): add Simplified Chinese (zh-CN) translation - #8765

Merged
lifeizhou-ap merged 5 commits into
aaif-goose:mainfrom
Anai-Guo:i18n/zh-cn-simplified-chinese
May 13, 2026
Merged

feat(i18n): add Simplified Chinese (zh-CN) translation#8765
lifeizhou-ap merged 5 commits into
aaif-goose:mainfrom
Anai-Guo:i18n/zh-cn-simplified-chinese

Conversation

@Anai-Guo

Copy link
Copy Markdown
Contributor

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-intl message ids are translated (verified by pnpm 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):

English 中文
Session 会话
Recipe 配方
Subrecipe 子配方
Extension 扩展
Provider 提供商
Prompt 提示词
Token 令牌
Context window 上下文窗口
Deeplink 深层链接
Schedule 计划
Skill 技能

Locale detection (ui/desktop/src/i18n/index.ts)

  • Registers zh-CN in SUPPORTED_LOCALES.
  • Uses navigator.languages (the full preference list) instead of just navigator.language, so a Chinese-speaking user on an English Windows UI with Region set to China still gets Chinese.
  • Maps all Simplified-Chinese aliases (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*) are intentionally not matched — they fall through to English. Adding a zh-TW catalog later is a separate drop-in.

System locale injection (ui/desktop/src/main.ts)

  • After app.whenReady(), fills appConfig.GOOSE_LOCALE from app.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.
  • Adds a small MENU_TRANSLATIONS_ZH_CN table + a translateMenuLabels() 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…) before Menu.setApplicationMenu installs them. Adding more locales is a one-liner — just drop in another MENU_TRANSLATIONS_<locale> table.

Sidebar nav (ui/desktop/src/hooks/useNavigationItems.ts + renderers)

  • Adds a getNavItemLabel(item, intl) helper that resolves each nav item via defineMessages, reusing the existing navigationCustomization.item* ids (adds a new navigationCustomization.itemSkills so the Skills entry has one).
  • CondensedRenderer.tsx and ExpandedRenderer.tsx now render {getNavItemLabel(item, intl)} instead of {item.label}.

Remaining hard-coded strings

  • Send button in ChatInput.tsx now uses intl.formatMessage(i18n.send) (the chatInput.send message already existed but was only used as a tooltip/title, not for the button text).
  • "Ctrl+↑/Ctrl+↓ to navigate messages" placeholder — getNavigationShortcutText now accepts an optional IntlShape. Uses a new chatInput.navigationShortcut ICU message with a {prefix} argument so the ⌘ vs Ctrl+ prefix stays platform-correct.

Regenerated en.json

pnpm i18n:extract was re-run after the source changes; two new ids were added:

  • chatInput.navigationShortcut
  • navigationCustomization.itemSkills

Verification

Ran locally on Windows 11 (x64, Node 24.15.0, pnpm 10.33.1):

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                                # clean build

Then launched the packaged Goose.exe on Windows 11 with UI language = en-US, Region = China (Simplified) and navigator.language = "en-US":

  • App starts without errors (app.getSystemLocale() now only fires after whenReady(); see the small main-process patch).
  • Locale resolves to zh-CN.
  • Every message-id-routed string renders in Simplified Chinese.
  • Native menu bar shows 文件 / 编辑 / 视图 / 窗口 / 帮助 with translated submenus (新建聊天, 打开目录…, 退出, 聚焦 Goose 窗口, 快速启动器, 窗口置顶, 切换导航, 关于 Goose…).
  • Sidebar shows 首页 / 聊天 / 配方 / 技能 / 应用 / 调度 / 扩展 / 设置.
  • "Send" button is 发送; chat input placeholder is Ctrl+↑/Ctrl+↓ 浏览消息.

On English locales everything is unchanged (fall-through).

Notes for reviewers

  • src/i18n/compiled/*.json is gitignored and not included — it is regenerated from the message files on every build. Nothing in this PR touches the build pipeline.
  • The Electron menu translation path is intentionally isolated in main.ts because the main process can't use react-intl. I kept the dictionary small and only translated labels that actually reach the user.
  • Happy to revise any term (Session / Recipe / Extension / …) during review if the project has existing conventions.

Test plan

  • Locale auto-selection works on macOS (app.getSystemLocale()), Windows, and Linux for a user whose OS region is set to China.
  • GOOSE_LOCALE=zh-CN ./goose-app overrides locale as before.
  • Existing English users on e.g. en-GB still get English (no regression — base-language fallback preserved).
  • pnpm i18n:extract + pnpm i18n:check still pass on CI.
  • Native menu labels render correctly on Windows; macOS should also localise since Electron's default app menu goes through the same Menu.getApplicationMenu() path.

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread ui/desktop/src/i18n/index.ts Outdated

// Simplified Chinese variants
const isTraditional = /^zh-(hant|tw|hk|mo)\b/.test(lower);
if (!isTraditional && (lower === 'zh' || lower.startsWith('zh-'))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread ui/desktop/src/main.ts Outdated
Comment on lines +147 to +148
const isTraditional = /^zh-(hant|tw|hk|mo)\b/.test(lower);
const isSimplifiedChinese = !isTraditional && (lower === 'zh' || lower.startsWith('zh-') || lower.startsWith('zh_'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Douwe Osinga added 2 commits May 12, 2026 13:35
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 DOsinga left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The two locale-detection bugs (underscore normalization) have been fixed in 72ca185GOOSE_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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +52 to 56
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
for (const tag of navigator.languages) {
if (tag) candidates.push(tag);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread ui/desktop/src/main.ts
// the raw label strings used below.
// =======================================================================

const MENU_TRANSLATIONS_ZH_CN: Record<string, string> = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@lifeizhou-ap
lifeizhou-ap enabled auto-merge May 13, 2026 01:30

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +61 to 64
const tag = resolveChineseAlias(normalized);

// Exact match first
if (SUPPORTED_LOCALES.has(tag)) return { locale: tag, messageLocale: tag };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@lifeizhou-ap
lifeizhou-ap added this pull request to the merge queue May 13, 2026
Merged via the queue into aaif-goose:main with commit 8c36ba8 May 13, 2026
21 checks passed
shafqatevo pushed a commit to shafqatevo/goose that referenced this pull request Aug 7, 2026
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>
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.

3 participants