From 04d1ba082534b672e840b41e837d5ee9915c3d13 Mon Sep 17 00:00:00 2001 From: JLee Date: Thu, 27 Aug 2026 15:18:07 +0800 Subject: [PATCH 1/9] feat(core): define region-aware UI locales Generated-by: ChatGPT --- packages/core/src/__tests__/settings.test.ts | 26 ++++++++ packages/core/src/__tests__/ui-locale.test.ts | 65 +++++++++++++++++++ packages/core/src/settings.ts | 14 ++-- packages/core/src/ui-locale.ts | 16 +++-- 4 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 packages/core/src/__tests__/ui-locale.test.ts diff --git a/packages/core/src/__tests__/settings.test.ts b/packages/core/src/__tests__/settings.test.ts index 3192715e14..50467ce9c4 100644 --- a/packages/core/src/__tests__/settings.test.ts +++ b/packages/core/src/__tests__/settings.test.ts @@ -95,6 +95,32 @@ describe('custom pet selection settings', () => { }); }); +describe('UI locale preferences', () => { + test('preserves every supported preference and migrates the former generic zh value', () => { + for (const uiLocale of ['auto', 'zh-CN', 'zh-TW', 'en'] as const) { + const normalized = normalizeSettings({ + personalization: { + displayName: '', + assistantTone: '', + uiLocale, + selectedPetId: null, + }, + }); + expect(normalized.personalization.uiLocale).toBe(uiLocale); + } + + const normalizedLegacy = normalizeSettings({ + personalization: { + displayName: '', + assistantTone: '', + uiLocale: 'zh', + selectedPetId: null, + }, + }); + expect(normalizedLegacy.personalization.uiLocale).toBe('zh-CN'); + }); +}); + test('shell settings default, normalize, and merge through their shared boundary', () => { const defaults = createDefaultSettings(); expect(defaults.shell).toEqual({ preference: 'auto', executable: '' }); diff --git a/packages/core/src/__tests__/ui-locale.test.ts b/packages/core/src/__tests__/ui-locale.test.ts new file mode 100644 index 0000000000..78f0965cb9 --- /dev/null +++ b/packages/core/src/__tests__/ui-locale.test.ts @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { describe, test } from 'node:test'; +import { expect } from './test-helpers.js'; +import { + isUiLocale, + isUiLocalePreference, + resolveSystemUiLocale, + resolveUiLocale, + uiLocaleToIntlLocale, +} from '../ui-locale.js'; + +describe('UI locale', () => { + test('accepts only the supported resolved locales and preferences', () => { + expect(['zh-CN', 'zh-TW', 'en'].every(isUiLocale)).toBe(true); + expect(isUiLocale('zh')).toBe(false); + expect(['auto', 'zh-CN', 'zh-TW', 'en'].every(isUiLocalePreference)).toBe(true); + }); + + for (const [languages, expected] of [ + [['zh-CN'], 'zh-CN'], + [['zh-SG'], 'zh-CN'], + [['zh-Hans'], 'zh-CN'], + [['zh-TW'], 'zh-TW'], + [['zh-Hant-TW'], 'zh-TW'], + [['zh-HK'], 'zh-TW'], + [['zh_MO'], 'zh-TW'], + [['zh_TW.UTF-8'], 'zh-TW'], + [['fr-FR', 'en-US'], 'en'], + [[], 'en'], + ] as const) { + test(`resolves system languages ${languages.join(',')} to ${expected}`, () => { + expect(resolveSystemUiLocale(languages)).toBe(expected); + }); + } + + test('resolves explicit preferences and overrides before the system locale', () => { + expect(resolveUiLocale('auto', 'zh-TW')).toBe('zh-TW'); + expect(resolveUiLocale('zh-CN', 'zh-TW')).toBe('zh-CN'); + expect(resolveUiLocale('zh-CN', 'zh-CN', 'en')).toBe('en'); + }); + + for (const locale of ['zh-CN', 'zh-TW', 'en'] as const) { + test(`uses ${locale} for Intl formatting`, () => { + expect(uiLocaleToIntlLocale(locale)).toBe(locale); + }); + } +}); diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index ac47f3515d..9ed48cacba 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -877,13 +877,17 @@ export function normalizeSettings(input: unknown): AppSettings { // PR-LANG-PREF-0: closed-enum fail-closed for the new // `personalization.uiLocale` preference. mergeSettings spreads // raw user values, so an unknown value would otherwise reach the - // renderer outside the closed reactive-locale contract. Fall back to - // 'auto' on any miss. + // renderer outside the closed reactive-locale contract. Preserve the + // former generic `zh` preference as Simplified Chinese, then fall back to + // 'auto' on any other miss. personalization: { ...base.personalization, - uiLocale: isUiLocalePreference(base.personalization.uiLocale) - ? base.personalization.uiLocale - : 'auto', + uiLocale: + (base.personalization.uiLocale as unknown) === 'zh' + ? 'zh-CN' + : isUiLocalePreference(base.personalization.uiLocale) + ? base.personalization.uiLocale + : 'auto', selectedPetId: normalizeSelectedPetId(base.personalization.selectedPetId), }, botChat: normalizeBotChatSettings(base.botChat, value.botChat), diff --git a/packages/core/src/ui-locale.ts b/packages/core/src/ui-locale.ts index 1f58e75036..3ed289c2a8 100644 --- a/packages/core/src/ui-locale.ts +++ b/packages/core/src/ui-locale.ts @@ -18,7 +18,7 @@ */ /** Resolved locales supported by human-facing Maka clients. */ -export const UI_LOCALES = ['zh', 'en'] as const; +export const UI_LOCALES = ['zh-CN', 'zh-TW', 'en'] as const; export type UiLocale = (typeof UI_LOCALES)[number]; @@ -31,7 +31,7 @@ export const UI_LOCALE_PREFERENCES = ['auto', ...UI_LOCALES] as const; export type UiCatalog = Record; export function isUiLocale(value: unknown): value is UiLocale { - return value === 'zh' || value === 'en'; + return value === 'zh-CN' || value === 'zh-TW' || value === 'en'; } export function isUiLocalePreference(value: unknown): value is UiLocalePreference { @@ -41,8 +41,12 @@ export function isUiLocalePreference(value: unknown): value is UiLocalePreferenc /** Resolve the first supported language in the operating system preference list. */ export function resolveSystemUiLocale(languages: readonly string[] | null | undefined): UiLocale { for (const language of languages ?? []) { - const normalized = language.trim(); - if (/^zh(?:[-_]|$)/iu.test(normalized)) return 'zh'; + const normalized = language.trim().replaceAll('_', '-'); + if (/^zh(?:[-.]|$)/iu.test(normalized)) { + if (/^zh-(?:tw|hk|mo)(?:[-.]|$)/iu.test(normalized)) return 'zh-TW'; + if (/^zh-hant(?:[-.]|$)/iu.test(normalized)) return 'zh-TW'; + return 'zh-CN'; + } if (/^en(?:[-_]|$)/iu.test(normalized)) return 'en'; } return 'en'; @@ -65,6 +69,6 @@ export function resolveUiLocale( } /** Locale identifier used by every locale-sensitive Intl formatter. */ -export function uiLocaleToIntlLocale(locale: UiLocale): 'zh-CN' | 'en' { - return locale === 'zh' ? 'zh-CN' : 'en'; +export function uiLocaleToIntlLocale(locale: UiLocale): UiLocale { + return locale; } From 030cf646d3156911887bc4fc6544280f84f9dc48 Mon Sep 17 00:00:00 2001 From: JLee Date: Thu, 27 Aug 2026 15:18:07 +0800 Subject: [PATCH 2/9] feat(desktop): add Traditional Chinese locale Generated-by: ChatGPT --- apps/desktop/.storybook/preview.tsx | 6 +- .../e2e/accessibility-coverage.spec.ts | 2 +- apps/desktop/e2e/fixtures.ts | 20 +- .../e2e/request-header-row-contract.spec.ts | 2 +- ...tive-execution-boundary-read-model.test.ts | 2 +- ...app-shell-session-settings-actions.test.ts | 2 +- .../computer-use-status-item-locale.test.ts | 2 +- .../desktop-locale-authority.test.ts | 24 +- .../__tests__/desktop-native-copy.test.ts | 4 +- .../main/__tests__/goal-arm-outcome.test.ts | 2 +- .../main/__tests__/health-center-copy.test.ts | 2 +- .../import-tasks-settings-page.test.ts | 4 +- .../__tests__/message-queue-ui-state.test.ts | 2 +- .../__tests__/notifications-policy.test.ts | 16 +- .../__tests__/permission-center-copy.test.ts | 2 +- .../src/main/__tests__/plan-mode-copy.test.ts | 2 +- .../provider-connection-status.test.ts | 10 +- .../__tests__/runtime-host-quit-copy.test.ts | 4 +- .../runtime-host-upgrade-dialog.test.ts | 2 +- .../session-status-presentation.test.ts | 4 +- .../settings-preferences-copy.test.ts | 51 ++ .../main/__tests__/streaming-handoff.test.ts | 20 +- .../__tests__/task-readiness-notice.test.ts | 2 +- .../__tests__/ui-locale-update-gate.test.ts | 18 +- .../__tests__/use-onboarding-snapshot.test.ts | 12 +- .../main/client-settings-confirmation-copy.ts | 2 +- .../src/main/computer-use/status-item.ts | 7 +- apps/desktop/src/main/e2e-fixture.ts | 4 +- .../src/main/native-diagnostic-dialog.ts | 25 +- .../permission-overlay-copy.ts | 21 +- apps/desktop/src/main/project-picker-copy.ts | 2 +- apps/desktop/src/main/runtime-host-boot.ts | 8 +- .../src/main/runtime-host-quit-copy.ts | 11 +- .../src/main/runtime-host-upgrade-copy.ts | 20 +- .../src/renderer/agent-graph-panel.tsx | 2 +- apps/desktop/src/renderer/app-shell-copy.ts | 2 +- apps/desktop/src/renderer/app-shell.tsx | 2 +- .../src/renderer/attachment-preflight.ts | 2 +- .../src/renderer/conversation-markdown.ts | 2 +- .../src/renderer/daily-review-actions.ts | 2 +- .../renderer/derive-turn-lineage-badges.ts | 2 +- .../model/session-project-grouping.ts | 2 +- .../workbar/tools/artifacts/artifact-pane.tsx | 4 +- .../tools/inspector/use-session-trace.ts | 4 +- .../tools/review/session-review-panel.tsx | 2 +- .../workbar/tools/tasks/use-session-tasks.ts | 2 +- .../tools/terminal/session-terminal-panel.tsx | 4 +- .../src/renderer/locales/artifact-copy.ts | 38 +- .../src/renderer/locales/browser-copy.ts | 26 +- .../src/renderer/locales/conversation-copy.ts | 227 +++++++- .../locales/external-session-import-copy.ts | 49 +- apps/desktop/src/renderer/locales/mcp-copy.ts | 61 +- .../src/renderer/locales/onboarding-copy.ts | 51 +- .../locales/permission-center-copy.ts | 49 +- .../src/renderer/locales/plan-mode-copy.ts | 21 +- .../src/renderer/locales/settings-bot-copy.ts | 92 ++- .../locales/settings-daily-review-copy.ts | 21 +- .../renderer/locales/settings-data-copy.ts | 29 +- .../renderer/locales/settings-health-copy.ts | 21 +- .../renderer/locales/settings-memory-copy.ts | 47 +- .../locales/settings-navigation-copy.ts | 28 +- .../locales/settings-preferences-copy.ts | 89 ++- .../locales/settings-projects-copy.ts | 208 ++++++- .../locales/settings-provider-copy.ts | 179 +++++- .../renderer/locales/settings-shared-copy.ts | 44 +- .../locales/settings-subagents-copy.ts | 84 ++- .../renderer/locales/settings-tasks-copy.ts | 27 +- .../locales/settings-test-result-copy.ts | 26 +- .../renderer/locales/settings-usage-copy.ts | 22 +- .../locales/settings-web-search-copy.ts | 19 +- .../src/renderer/locales/shell-copy.ts | 526 +++++++++++++++++- .../renderer/locales/shell-remaining-copy.ts | 78 ++- apps/desktop/src/renderer/mcp-catalog.ts | 2 +- .../src/renderer/model-catalog-choices.ts | 4 +- .../src/renderer/model-connection-errors.ts | 6 +- .../renderer/session-error-presentation.ts | 2 +- .../renderer/session-status-presentation.ts | 4 +- .../src/renderer/settings/bot-chat-detail.tsx | 8 +- .../renderer/settings/bot-chat-overview.tsx | 4 +- .../src/renderer/settings/bot-chat-shared.tsx | 4 +- .../settings/bot-onboarding-modal.tsx | 6 +- .../settings/daily-review-settings-page.tsx | 2 +- .../renderer/settings/data-settings-page.tsx | 2 +- .../settings/memory-settings-view-model.ts | 2 +- .../settings/permission-center-page.tsx | 2 +- .../personalization-settings-section.tsx | 23 +- .../renderer/settings/provider-add-form.tsx | 2 +- .../settings/provider-catalog-page.tsx | 2 +- .../settings/provider-connection-status.ts | 2 +- .../settings/provider-display-copy.ts | 183 ++++-- .../settings/provider-oauth-section.tsx | 2 +- .../settings/provider-panel-shared.ts | 18 +- .../src/renderer/settings/providers-panel.tsx | 2 +- .../renderer/settings/settings-error-copy.ts | 6 +- .../use-memory-settings-controller.ts | 2 +- .../renderer/settings/use-oauth-login-flow.ts | 12 +- .../src/renderer/task-readiness-notice.ts | 4 +- .../src/renderer/turn-footer-actions.ts | 2 +- .../src/renderer/use-onboarding-snapshot.ts | 2 +- apps/desktop/src/renderer/workhub-surface.tsx | 2 +- apps/desktop/stories/app-shell.stories.tsx | 2 +- .../settings/settings-pages.stories.tsx | 6 +- .../cli/src/__tests__/cli-ui-locale.test.ts | 16 +- packages/cli/src/__tests__/cli.test.ts | 2 +- .../cli/src/__tests__/pi-transcript.test.ts | 2 +- .../cli/src/__tests__/pi-tui-runner.test.ts | 2 +- .../__tests__/tui-primary-guidance.test.ts | 10 +- .../src/__tests__/tui-session-status.test.ts | 4 +- packages/cli/src/cli-ui-locale.ts | 10 +- packages/cli/src/tui-primary-guidance.ts | 50 +- packages/cli/src/tui-session-status.ts | 10 +- .../core/src/__tests__/relative-time.test.ts | 10 +- packages/core/src/relative-time.ts | 9 +- packages/core/src/tool-quiet-preview.ts | 19 +- .../src/__tests__/conversation-copy.test.ts | 7 +- .../__tests__/live-turn-projection.test.ts | 2 +- .../ui/src/__tests__/markdown-body.test.ts | 2 +- packages/ui/src/__tests__/materialize.test.ts | 4 +- .../tool-activity-presentation.test.ts | 6 +- .../__tests__/transcript-projection.test.ts | 2 +- packages/ui/src/artifact-preview-registry.ts | 2 +- packages/ui/src/assistant-stream.ts | 4 +- packages/ui/src/astryx-copy.ts | 72 +++ packages/ui/src/astryx-i18n.tsx | 6 +- packages/ui/src/chat-model-helpers.ts | 4 +- packages/ui/src/chat-turn.tsx | 3 +- packages/ui/src/chat-view.tsx | 2 +- packages/ui/src/conversation-copy.ts | 130 ++++- packages/ui/src/daily-review-copy.ts | 37 +- packages/ui/src/daily-review-helpers.ts | 2 +- packages/ui/src/live-turn-projection.ts | 2 +- packages/ui/src/locale-helpers.ts | 10 +- packages/ui/src/scheduled-task-copy.ts | 37 +- packages/ui/src/scheduled-task-panel.tsx | 2 +- packages/ui/src/search-modal.tsx | 2 +- .../ui/src/session-status-presentation.ts | 4 +- packages/ui/src/shared-ui-copy.ts | 91 ++- packages/ui/src/shell-controls-copy.ts | 34 +- packages/ui/src/skills-copy.ts | 17 +- packages/ui/src/thinking-stream.ts | 4 +- packages/ui/src/tool-activity.tsx | 2 +- .../ui/src/tool-activity/agent-preview.tsx | 2 +- packages/ui/src/tool-activity/copy.ts | 112 +++- .../ui/src/tool-activity/preview-utils.ts | 2 +- .../ui/src/tool-activity/result-projection.ts | 2 +- packages/ui/src/tool-output-stream.ts | 2 +- 146 files changed, 3122 insertions(+), 363 deletions(-) create mode 100644 apps/desktop/src/main/__tests__/settings-preferences-copy.test.ts diff --git a/apps/desktop/.storybook/preview.tsx b/apps/desktop/.storybook/preview.tsx index f460a84495..ef0d8d8fe5 100644 --- a/apps/desktop/.storybook/preview.tsx +++ b/apps/desktop/.storybook/preview.tsx @@ -38,7 +38,7 @@ const withMakaRoot: Decorator = (Story, context) => { // lines are ~1.8× the width of the Chinese copy, so a row that fits in zh // overflows, truncates, or clips in en. Stories were locked to `zh`, which // is exactly why those breakages only ever showed up in the shipped app. - const locale = context.globals.locale === 'en' ? 'en' : 'zh'; + const locale = context.globals.locale === 'en' ? 'en' : 'zh-CN'; root.classList.toggle('dark', colorScheme === 'dark'); root.style.colorScheme = colorScheme; @@ -96,7 +96,7 @@ const preview: Preview = { toolbar: { icon: 'globe', items: [ - { title: '中文', value: 'zh' }, + { title: '中文', value: 'zh-CN' }, { title: 'English', value: 'en' }, ], }, @@ -114,7 +114,7 @@ const preview: Preview = { }, initialGlobals: { colorScheme: 'light', - locale: 'zh', + locale: 'zh-CN', palette: 'default', }, parameters: { diff --git a/apps/desktop/e2e/accessibility-coverage.spec.ts b/apps/desktop/e2e/accessibility-coverage.spec.ts index fe4e97fe80..146a18297b 100644 --- a/apps/desktop/e2e/accessibility-coverage.spec.ts +++ b/apps/desktop/e2e/accessibility-coverage.spec.ts @@ -48,7 +48,7 @@ test('every settings page exposes named actionable controls', async ({ window: p const sectionLabels = (await navigation.getByRole('button').allTextContents()) .map((label) => label.trim().replace(/\s*Beta$/, '')) .filter((label) => label.length > 0 && label !== '返回应用'); - const expectedSectionLabels = groupedNav('zh') + const expectedSectionLabels = groupedNav('zh-CN') .flatMap(({ items }) => items) .filter(({ enabled }) => enabled) .map(({ label }) => label); diff --git a/apps/desktop/e2e/fixtures.ts b/apps/desktop/e2e/fixtures.ts index 1684647b1b..fbc6de9ed4 100644 --- a/apps/desktop/e2e/fixtures.ts +++ b/apps/desktop/e2e/fixtures.ts @@ -141,7 +141,7 @@ async function seedE2eConnection(userDataDir: string): Promise { } } -async function seedE2eLocale(userDataDir: string, locale: 'zh' | 'en'): Promise { +async function seedE2eLocale(userDataDir: string, locale: 'zh-CN' | 'zh-TW' | 'en'): Promise { const workspaceRoot = path.join(userDataDir, 'workspaces', 'default'); await createSettingsStore(workspaceRoot).update({ personalization: { uiLocale: locale }, @@ -342,7 +342,7 @@ async function withE2eWindow( seed: boolean; readinessSelector: string; e2eFixtureScenario?: string; - locale?: 'zh' | 'en'; + locale?: 'zh-CN' | 'zh-TW' | 'en'; /** Opt this window back into animated scrolling; see `scroll-motion-policy`. */ scrollMotion?: 'auto' | 'smooth'; /** #1312: force app:info's platform so the window boots natively into that platform's `data-os` cascade. */ @@ -445,13 +445,13 @@ export const test = base.extend<{ }>({ // Seeded: a pre-staged connection clears onboarding so the composer is ready. window: async ({}, use) => { - await withE2eWindow({ seed: true, readinessSelector: COMPOSER_INPUT, locale: 'zh' }, use); + await withE2eWindow({ seed: true, readinessSelector: COMPOSER_INPUT, locale: 'zh-CN' }, use); }, onboardingWindow: async ({}, use) => { await withE2eWindow({ seed: false, readinessSelector: '[data-maka-contract="onboarding-card"]', - locale: 'zh', + locale: 'zh-CN', showWindow: true, }, use); }, @@ -460,7 +460,7 @@ export const test = base.extend<{ { seed: true, readinessSelector: COMPOSER_INPUT, - locale: 'zh', + locale: 'zh-CN', gitReviewExtraFiles: 0, }, async (page, context) => { @@ -476,7 +476,7 @@ export const test = base.extend<{ await withE2eWindow({ seed: true, readinessSelector: COMPOSER_INPUT, - locale: 'zh', + locale: 'zh-CN', invocableSkills: true, }, use); }, @@ -495,7 +495,7 @@ export const test = base.extend<{ await withE2eWindow({ seed: true, readinessSelector: COMPOSER_INPUT, - locale: 'zh', + locale: 'zh-CN', newTaskProject: true, showWindow: true, }, use); @@ -505,7 +505,7 @@ export const test = base.extend<{ seed: false, readinessSelector: '[data-maka-contract="search-modal"][open]', e2eFixtureScenario: 'sidebar-search-modal-open', - locale: 'zh', + locale: 'zh-CN', showWindow: true, }, use); }, @@ -514,7 +514,7 @@ export const test = base.extend<{ { seed: true, readinessSelector: COMPOSER_INPUT, - locale: 'zh', + locale: 'zh-CN', parentRemovalSessions: true, }, use, @@ -556,7 +556,7 @@ export const test = base.extend<{ seed: false, readinessSelector: '.settingsSurface', e2eFixtureScenario: 'settings-models', - locale: 'zh', + locale: 'zh-CN', showWindow: true, }, use); }, diff --git a/apps/desktop/e2e/request-header-row-contract.spec.ts b/apps/desktop/e2e/request-header-row-contract.spec.ts index 743afe201d..2ff42e67fd 100644 --- a/apps/desktop/e2e/request-header-row-contract.spec.ts +++ b/apps/desktop/e2e/request-header-row-contract.spec.ts @@ -37,7 +37,7 @@ import { getProviderSettingsCopy } from '../src/renderer/locales/settings-provid * surfaces as they first render, and this editor is three clicks deep. */ -const copy = getProviderSettingsCopy('zh').detail; +const copy = getProviderSettingsCopy('zh-CN').detail; test('the request header remove button centres on its field', async ({ requestHeaderRowWindow: page, diff --git a/apps/desktop/src/main/__tests__/active-execution-boundary-read-model.test.ts b/apps/desktop/src/main/__tests__/active-execution-boundary-read-model.test.ts index f00a507cd6..7c914a5af1 100644 --- a/apps/desktop/src/main/__tests__/active-execution-boundary-read-model.test.ts +++ b/apps/desktop/src/main/__tests__/active-execution-boundary-read-model.test.ts @@ -286,7 +286,7 @@ describe('Boundary decisions notify the read model', () => { function handlersWithRecorder() { const boundaryChanges: string[] = []; const handlers = createAppShellSessionEventHandlers({ - uiLocale: 'zh', + uiLocale: 'zh-CN', activeIdRef: { current: 'session-a' }, liveTurnBySessionRef: { current: {} }, refreshMessages: async () => true, diff --git a/apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts b/apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts index 3b980403cf..6327d284e1 100644 --- a/apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts +++ b/apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts @@ -102,7 +102,7 @@ function createHarness(options: { }); const actions = createAppShellSessionSettingsActions({ - uiLocale: 'zh', + uiLocale: 'zh-CN', activeIdRef, connections: options.connections ?? ([{ slug: 'e2e', name: 'E2E' }] as LlmConnection[]), messages: options.messages ?? [], diff --git a/apps/desktop/src/main/__tests__/computer-use-status-item-locale.test.ts b/apps/desktop/src/main/__tests__/computer-use-status-item-locale.test.ts index cf3912e50e..3b2827c882 100644 --- a/apps/desktop/src/main/__tests__/computer-use-status-item-locale.test.ts +++ b/apps/desktop/src/main/__tests__/computer-use-status-item-locale.test.ts @@ -50,7 +50,7 @@ test('rebuilds an active Computer Use menu when its resolved locale changes', () assert.deepEqual(menus.at(-1)?.map((row) => row.label), ['Stop Using Safari']); locale.observe(mergeSettings(createDefaultSettings(), { - personalization: { uiLocale: 'zh' }, + personalization: { uiLocale: 'zh-CN' }, })); assert.deepEqual(menus.at(-1)?.map((row) => row.label), ['停止操作 Safari']); diff --git a/apps/desktop/src/main/__tests__/desktop-locale-authority.test.ts b/apps/desktop/src/main/__tests__/desktop-locale-authority.test.ts index 1b6ae08f01..60aa26c14a 100644 --- a/apps/desktop/src/main/__tests__/desktop-locale-authority.test.ts +++ b/apps/desktop/src/main/__tests__/desktop-locale-authority.test.ts @@ -24,15 +24,15 @@ import { createDesktopLocaleAuthority } from '../desktop-locale-authority.js'; test('resolves explicit preferences and observes a mid-session settings change', async () => { let settings = mergeSettings(createDefaultSettings(), { - personalization: { uiLocale: 'zh' }, + personalization: { uiLocale: 'zh-CN' }, }); const authority = createDesktopLocaleAuthority({ readSettings: async () => settings, preferredSystemLanguages: () => ['en-US'], }); - assert.equal(await authority.resolve(), 'zh'); - assert.equal(authority.current(), 'zh'); + assert.equal(await authority.resolve(), 'zh-CN'); + assert.equal(authority.current(), 'zh-CN'); settings = mergeSettings(settings, { personalization: { uiLocale: 'en' } }); authority.observe(settings); assert.equal(authority.current(), 'en'); @@ -50,14 +50,14 @@ test('publishes only resolved-locale changes and allows listeners to detach', () personalization: { uiLocale: 'en' }, })); authority.observe(mergeSettings(createDefaultSettings(), { - personalization: { uiLocale: 'zh' }, + personalization: { uiLocale: 'zh-CN' }, })); unsubscribe(); authority.observe(mergeSettings(createDefaultSettings(), { personalization: { uiLocale: 'en' }, })); - assert.deepEqual(observed, ['zh']); + assert.deepEqual(observed, ['zh-CN']); }); test('keeps automatic preference live against the current system fallback', async () => { @@ -68,8 +68,10 @@ test('keeps automatic preference live against the current system fallback', asyn }); assert.equal(await authority.resolve(), 'en'); + languages = ['zh-TW']; + assert.equal(authority.current(), 'zh-TW'); languages = ['zh-CN']; - assert.equal(authority.current(), 'zh'); + assert.equal(authority.current(), 'zh-CN'); }); test('does not let a stale read replace a newer observed preference', async () => { @@ -86,13 +88,13 @@ test('does not let a stale read replace a newer observed preference', async () = const read = authority.resolve(); authority.observe(mergeSettings(createDefaultSettings(), { - personalization: { uiLocale: 'zh' }, + personalization: { uiLocale: 'zh-CN' }, })); release(createDefaultSettings()); - assert.equal(await read, 'zh'); - assert.equal(authority.current(), 'zh'); - assert.deepEqual(observed, ['zh']); + assert.equal(await read, 'zh-CN'); + assert.equal(authority.current(), 'zh-CN'); + assert.deepEqual(observed, ['zh-CN']); }); test('falls back to its current projection when settings cannot be read', async () => { @@ -100,5 +102,5 @@ test('falls back to its current projection when settings cannot be read', async readSettings: async () => { throw new Error('unreadable'); }, preferredSystemLanguages: () => ['zh-CN'], }); - assert.equal(await authority.resolve(), 'zh'); + assert.equal(await authority.resolve(), 'zh-CN'); }); diff --git a/apps/desktop/src/main/__tests__/desktop-native-copy.test.ts b/apps/desktop/src/main/__tests__/desktop-native-copy.test.ts index f5678fc671..41e1504db0 100644 --- a/apps/desktop/src/main/__tests__/desktop-native-copy.test.ts +++ b/apps/desktop/src/main/__tests__/desktop-native-copy.test.ts @@ -25,11 +25,11 @@ import { import { projectPickerTitle } from '../project-picker-copy.js'; test('localizes native picker and client settings confirmation copy', () => { - assert.equal(projectPickerTitle('zh'), '添加项目'); + assert.equal(projectPickerTitle('zh-CN'), '添加项目'); assert.deepEqual( clientSettingsConfirmation( [{ key: 'keepSystemAwake', current: false, next: true }], - 'zh', + 'zh-CN', ), { message: '允许 Maka 更新此客户端的设置吗?', diff --git a/apps/desktop/src/main/__tests__/goal-arm-outcome.test.ts b/apps/desktop/src/main/__tests__/goal-arm-outcome.test.ts index 09a2b13c11..da86114d6d 100644 --- a/apps/desktop/src/main/__tests__/goal-arm-outcome.test.ts +++ b/apps/desktop/src/main/__tests__/goal-arm-outcome.test.ts @@ -83,7 +83,7 @@ test('successful Goal arming closes while every reconciliation result locks the }); test('Goal reconciliation copy explains authoritative state in Chinese and English', () => { - const zh = getShellCopy('zh').goalDialog; + const zh = getShellCopy('zh-CN').goalDialog; assert.match( zh.reconciledMatching('所有测试通过', zh.statusLabels.active), /所有测试通过.*进行中.*无法确认.*提交/, diff --git a/apps/desktop/src/main/__tests__/health-center-copy.test.ts b/apps/desktop/src/main/__tests__/health-center-copy.test.ts index e8f7a6aa5f..1fe77c4599 100644 --- a/apps/desktop/src/main/__tests__/health-center-copy.test.ts +++ b/apps/desktop/src/main/__tests__/health-center-copy.test.ts @@ -23,7 +23,7 @@ import { getHealthCenterCopy } from '../../renderer/locales/settings-health-copy test('labels blocker counts as global across filtered health views', () => { assert.equal( - getHealthCenterCopy('zh').blockers.send(1, 6), + getHealthCenterCopy('zh-CN').blockers.send(1, 6), '全部健康信号中,1/6 条会阻塞发送', ); assert.equal( diff --git a/apps/desktop/src/main/__tests__/import-tasks-settings-page.test.ts b/apps/desktop/src/main/__tests__/import-tasks-settings-page.test.ts index 6b4cb62aeb..6c928b9fa6 100644 --- a/apps/desktop/src/main/__tests__/import-tasks-settings-page.test.ts +++ b/apps/desktop/src/main/__tests__/import-tasks-settings-page.test.ts @@ -136,7 +136,7 @@ describe('ImportTasksSettingsPage durable import state', () => { it('renders durable repeat-import state in Chinese', async () => { const harness = await renderPage({ - locale: 'zh', + locale: 'zh-CN', catalog: catalog( externalSession({ importState: { @@ -630,7 +630,7 @@ async function renderPage(options: { | { ok: false; reason: 'commit_outcome_unknown' } | Promise<{ ok: false; reason: 'commit_outcome_unknown' }>; onOpenImported?: (sessionId: string) => void; - locale?: 'en' | 'zh'; + locale?: 'en' | 'zh-CN'; }): Promise<{ container: HTMLElement; root: Root; diff --git a/apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts b/apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts index d9db2f1602..f25da0c9bd 100644 --- a/apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts +++ b/apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts @@ -27,7 +27,7 @@ test('queue_update events drive the independent desktop queue projection', () => const transientMessages: unknown[] = []; const removedTransientMessageIds: string[] = []; const handlers = createAppShellSessionEventHandlers({ - uiLocale: 'zh', + uiLocale: 'zh-CN', activeIdRef: { current: 'session-1' }, liveTurnBySessionRef: controller.liveTurnBySessionRef, refreshMessages: async () => true, diff --git a/apps/desktop/src/main/__tests__/notifications-policy.test.ts b/apps/desktop/src/main/__tests__/notifications-policy.test.ts index fde041c2b9..546a6474e6 100644 --- a/apps/desktop/src/main/__tests__/notifications-policy.test.ts +++ b/apps/desktop/src/main/__tests__/notifications-policy.test.ts @@ -48,8 +48,8 @@ it('recognizes terminal kinds and keeps distinct localized fallback copy', () => assert.equal(isRunNotificationKind(value), false); } - const completed = runNotificationCopy('completed', 'zh'); - const errored = runNotificationCopy('errored', 'zh'); + const completed = runNotificationCopy('completed', 'zh-CN'); + const errored = runNotificationCopy('errored', 'zh-CN'); assert.ok(completed.title && completed.body); assert.ok(errored.title && errored.body); assert.notEqual(completed.title, errored.title); @@ -58,28 +58,28 @@ it('recognizes terminal kinds and keeps distinct localized fallback copy', () => it('sanitizes renderer content, caps it, and falls back per field', () => { const clean = resolveNotificationContent( { kind: 'completed', title: ' 会话 A ', body: 'line one\n\nline two\tindented' }, - 'zh', + 'zh-CN', ); assert.deepEqual(clean, { title: '会话 A', body: 'line one line two indented' }); - const completedFallback = runNotificationCopy('completed', 'zh'); + const completedFallback = runNotificationCopy('completed', 'zh-CN'); for (const value of ['', ' ', undefined, null, 42, {}]) { assert.deepEqual( - resolveNotificationContent({ kind: 'completed', title: value, body: value }, 'zh'), + resolveNotificationContent({ kind: 'completed', title: value, body: value }, 'zh-CN'), completedFallback, ); } const capped = resolveNotificationContent( { kind: 'completed', title: 'S', body: 'x'.repeat(500) }, - 'zh', + 'zh-CN', ); assert.equal(capped.body.length, 160); assert.ok(capped.body.endsWith('…')); - const erroredFallback = runNotificationCopy('errored', 'zh'); + const erroredFallback = runNotificationCopy('errored', 'zh-CN'); assert.deepEqual( - resolveNotificationContent({ kind: 'errored', title: '出错的会话', body: '' }, 'zh'), + resolveNotificationContent({ kind: 'errored', title: '出错的会话', body: '' }, 'zh-CN'), { title: '出错的会话', body: erroredFallback.body }, ); }); diff --git a/apps/desktop/src/main/__tests__/permission-center-copy.test.ts b/apps/desktop/src/main/__tests__/permission-center-copy.test.ts index 6333051056..318b15a877 100644 --- a/apps/desktop/src/main/__tests__/permission-center-copy.test.ts +++ b/apps/desktop/src/main/__tests__/permission-center-copy.test.ts @@ -22,6 +22,6 @@ import { test } from 'node:test'; import { getPermissionCenterCopy } from '../../renderer/locales/permission-center-copy.js'; test('presents a granted OS permission as a verified success', () => { - assert.equal(getPermissionCenterCopy('zh').osStates.granted.tone, 'success'); + assert.equal(getPermissionCenterCopy('zh-CN').osStates.granted.tone, 'success'); assert.equal(getPermissionCenterCopy('en').osStates.granted.tone, 'success'); }); diff --git a/apps/desktop/src/main/__tests__/plan-mode-copy.test.ts b/apps/desktop/src/main/__tests__/plan-mode-copy.test.ts index fbe73bb661..af8cc19329 100644 --- a/apps/desktop/src/main/__tests__/plan-mode-copy.test.ts +++ b/apps/desktop/src/main/__tests__/plan-mode-copy.test.ts @@ -22,7 +22,7 @@ import { test } from 'node:test'; import { getPlanModeCopy } from '../../renderer/locales/plan-mode-copy.js'; test('localizes Plan Mode chrome and abandon confirmation without rewriting plan content', () => { - const zh = getPlanModeCopy('zh'); + const zh = getPlanModeCopy('zh-CN'); const en = getPlanModeCopy('en'); assert.equal(zh.proposal.statuses.approved, '已批准'); diff --git a/apps/desktop/src/main/__tests__/provider-connection-status.test.ts b/apps/desktop/src/main/__tests__/provider-connection-status.test.ts index a0429cb72f..f6d1e41658 100644 --- a/apps/desktop/src/main/__tests__/provider-connection-status.test.ts +++ b/apps/desktop/src/main/__tests__/provider-connection-status.test.ts @@ -49,7 +49,7 @@ test('a retired connection reads as broken rather than repairable', () => { // Nothing else in the list marks this row, so without a status the only // signal that it has to go is on the detail page the user has no reason to // open. - assert.deepEqual(connectionChipStatus(retired, 'zh'), { + assert.deepEqual(connectionChipStatus(retired, 'zh-CN'), { label: '已停用 · 请删除', tone: 'error', }); @@ -69,7 +69,7 @@ test('retirement outranks every repairable state', () => { { enabled: false }, ]) { assert.deepEqual( - connectionChipStatus({ ...retired, ...overrides }, 'zh'), + connectionChipStatus({ ...retired, ...overrides }, 'zh-CN'), { label: '已停用 · 请删除', tone: 'error' }, `retirement must win over ${JSON.stringify(overrides)}`, ); @@ -77,12 +77,12 @@ test('retirement outranks every repairable state', () => { }); test('a live connection keeps its existing statuses', () => { - assert.equal(connectionChipStatus(connection({ lastTestStatus: 'verified' }), 'zh'), null); - assert.deepEqual(connectionChipStatus(connection({ lastTestStatus: 'needs_reauth' }), 'zh'), { + assert.equal(connectionChipStatus(connection({ lastTestStatus: 'verified' }), 'zh-CN'), null); + assert.deepEqual(connectionChipStatus(connection({ lastTestStatus: 'needs_reauth' }), 'zh-CN'), { label: '需要重新登录', tone: 'attention', }); - assert.deepEqual(connectionChipStatus(connection({ enabled: false }), 'zh'), { + assert.deepEqual(connectionChipStatus(connection({ enabled: false }), 'zh-CN'), { label: '暂不可用', tone: 'neutral', }); diff --git a/apps/desktop/src/main/__tests__/runtime-host-quit-copy.test.ts b/apps/desktop/src/main/__tests__/runtime-host-quit-copy.test.ts index bf1aab2f04..defbaa8e27 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-quit-copy.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-quit-copy.test.ts @@ -33,7 +33,7 @@ const failure = new DesktopLocalHostRetirementError( { cause: new Error('writer release timed out') }, ); -for (const locale of ['en', 'zh'] as const) { +for (const locale of ['en', 'zh-CN'] as const) { test(`quit failure copy exposes actionable Host facts in ${locale}`, () => { const dialog = buildRuntimeHostQuitFailureDialog(failure, locale); @@ -46,7 +46,7 @@ for (const locale of ['en', 'zh'] as const) { test('manual recovery copy names a cross-platform process-management concept', () => { const english = buildRuntimeHostQuitFailureDialog(failure, 'en').detail ?? ''; - const chinese = buildRuntimeHostQuitFailureDialog(failure, 'zh').detail ?? ''; + const chinese = buildRuntimeHostQuitFailureDialog(failure, 'zh-CN').detail ?? ''; assert.match(english, /operating system's process-management tool/); assert.match(chinese, /操作系统的进程管理工具/); diff --git a/apps/desktop/src/main/__tests__/runtime-host-upgrade-dialog.test.ts b/apps/desktop/src/main/__tests__/runtime-host-upgrade-dialog.test.ts index 3f515e2c9b..6342e65dea 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-upgrade-dialog.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-upgrade-dialog.test.ts @@ -40,7 +40,7 @@ const conflict = { test('localizes upgrade activity without changing decision indexes', () => { const en = buildRuntimeHostUpgradeDialogOptions(conflict, true, 'en'); - const zh = buildRuntimeHostUpgradeDialogOptions(conflict, true, 'zh'); + const zh = buildRuntimeHostUpgradeDialogOptions(conflict, true, 'zh-CN'); assert.deepEqual(en.buttons, ['Restart Runtime Host', 'Wait', 'Cancel Startup']); assert.deepEqual(zh.buttons, ['重启 Runtime Host', '等待', '取消启动']); assert.equal(en.defaultId, 1); diff --git a/apps/desktop/src/main/__tests__/session-status-presentation.test.ts b/apps/desktop/src/main/__tests__/session-status-presentation.test.ts index 0107b6ebc8..3e317b8423 100644 --- a/apps/desktop/src/main/__tests__/session-status-presentation.test.ts +++ b/apps/desktop/src/main/__tests__/session-status-presentation.test.ts @@ -32,13 +32,13 @@ const outputFreeFailure = { describe('failed turn recovery presentation', () => { it('presents persisted provider server errors as provider failures', () => { - assert.equal(describeTurnErrorClass('server_error', 'zh'), '模型服务返回错误'); + assert.equal(describeTurnErrorClass('server_error', 'zh-CN'), '模型服务返回错误'); assert.equal(describeTurnErrorClass('server_error', 'en'), 'Model service error'); }); it('does not recommend a byte-identical retry after context overflow', () => { assert.deepEqual( - deriveFailedTurnRecovery({ ...outputFreeFailure, errorClass: 'context_overflow' }, 'zh'), + deriveFailedTurnRecovery({ ...outputFreeFailure, errorClass: 'context_overflow' }, 'zh-CN'), { action: 'continue', label: '上下文仍超出限制,请减少附件或开启新任务' }, ); assert.deepEqual( diff --git a/apps/desktop/src/main/__tests__/settings-preferences-copy.test.ts b/apps/desktop/src/main/__tests__/settings-preferences-copy.test.ts new file mode 100644 index 0000000000..c04fe555cc --- /dev/null +++ b/apps/desktop/src/main/__tests__/settings-preferences-copy.test.ts @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { getSettingsPreferencesCopy } from '../../renderer/locales/settings-preferences-copy.js'; + +test('language selector offers every preference with locale-appropriate labels', () => { + assert.deepEqual(getSettingsPreferencesCopy('zh-CN').personalization.localeOptions, [ + ['auto', '跟随系统'], + ['zh-CN', '简体中文'], + ['zh-TW', '繁體中文'], + ['en', 'English'], + ]); + assert.deepEqual(getSettingsPreferencesCopy('zh-TW').personalization.localeOptions, [ + ['auto', '自動(跟隨系統)'], + ['zh-CN', '简体中文'], + ['zh-TW', '繁體中文'], + ['en', 'English'], + ]); + assert.deepEqual(getSettingsPreferencesCopy('en').personalization.localeOptions, [ + ['auto', 'Follow system'], + ['zh-CN', 'Simplified Chinese'], + ['zh-TW', 'Traditional Chinese'], + ['en', 'English'], + ]); +}); + +test('Traditional Chinese settings copy uses Taiwan terminology', () => { + const copy = getSettingsPreferencesCopy('zh-TW'); + assert.equal(copy.sections.network, '網路'); + assert.equal(copy.appearance.paletteLabels.default, '預設'); + assert.equal(copy.appearance.appIconImport, '匯入圖示…'); + assert.equal(copy.about.clipboardUnavailable, '剪貼簿不可用或被系統拒絕。'); +}); diff --git a/apps/desktop/src/main/__tests__/streaming-handoff.test.ts b/apps/desktop/src/main/__tests__/streaming-handoff.test.ts index 53fb370497..691ab9a5eb 100644 --- a/apps/desktop/src/main/__tests__/streaming-handoff.test.ts +++ b/apps/desktop/src/main/__tests__/streaming-handoff.test.ts @@ -38,7 +38,7 @@ import { function renderWithLocale(child: ReactNode): string { return renderToStaticMarkup( createElement(LocaleProvider, { - locale: 'zh', + locale: 'zh-CN', children: createElement(ChatSurfaceLayout, { composer: null, children: child }), }), ); @@ -283,7 +283,7 @@ describe('single live-turn handoff', () => { liveTurnBySessionRef.current = liveTurns.get(); }; const handlers = createAppShellSessionEventHandlers({ - uiLocale: 'zh', + uiLocale: 'zh-CN', activeIdRef: { current: 'session-1' }, liveTurnBySessionRef, refreshMessages: async (sessionId, options) => { @@ -336,7 +336,7 @@ describe('single live-turn handoff', () => { const frames: Array<() => void> = []; let publications = 0; const handlers = createAppShellSessionEventHandlers({ - uiLocale: 'zh', + uiLocale: 'zh-CN', activeIdRef: { current: 'session-1' }, liveTurnBySessionRef, refreshMessages: async () => true, @@ -389,7 +389,7 @@ describe('single live-turn handoff', () => { const frames: Array<() => void> = []; let publications = 0; const handlers = createAppShellSessionEventHandlers({ - uiLocale: 'zh', + uiLocale: 'zh-CN', activeIdRef: { current: 'session-1' }, liveTurnBySessionRef, refreshMessages: async () => true, @@ -452,7 +452,7 @@ describe('single live-turn handoff', () => { const displayBatch = createAppShellSessionDisplayBatch(); let publications = 0; const deps = { - uiLocale: 'zh' as const, + uiLocale: 'zh-CN' as const, activeIdRef: { current: 'session-1' }, liveTurnBySessionRef, refreshMessages: async () => true, @@ -499,7 +499,7 @@ describe('single live-turn handoff', () => { ref.current = liveTurns.get(); }; const handlers = createAppShellSessionEventHandlers({ - uiLocale: 'zh', + uiLocale: 'zh-CN', activeIdRef: { current: 'session-1' }, liveTurnBySessionRef: ref, refreshMessages: async () => true, @@ -556,7 +556,7 @@ describe('single live-turn handoff', () => { resolveRefresh = resolve; }); const handlers = createAppShellSessionEventHandlers({ - uiLocale: 'zh', + uiLocale: 'zh-CN', activeIdRef: { current: 'session-1' }, liveTurnBySessionRef: ref, refreshMessages: async () => refresh, @@ -605,7 +605,7 @@ describe('single live-turn handoff', () => { | { sessionId: string; turnId: string; eventId: string } | undefined; const handlers = createAppShellSessionEventHandlers({ - uiLocale: 'zh', + uiLocale: 'zh-CN', activeIdRef: { current: 'session-1' }, liveTurnBySessionRef: ref, refreshMessages: async () => false, @@ -678,7 +678,7 @@ describe('single live-turn handoff', () => { const ref = { current: liveTurns.get() }; const interactions = createStateSetter({}); const handlers = createAppShellSessionEventHandlers({ - uiLocale: 'zh', + uiLocale: 'zh-CN', activeIdRef: { current: 'session-1' }, liveTurnBySessionRef: ref, refreshMessages: async () => true, @@ -718,7 +718,7 @@ describe('single live-turn handoff', () => { resolveRefresh = resolve; }); const handlers = createAppShellSessionEventHandlers({ - uiLocale: 'zh', + uiLocale: 'zh-CN', activeIdRef: { current: 'session-1' }, liveTurnBySessionRef: ref, refreshMessages: async () => refresh, diff --git a/apps/desktop/src/main/__tests__/task-readiness-notice.test.ts b/apps/desktop/src/main/__tests__/task-readiness-notice.test.ts index 43d1f59cd9..88321f4e00 100644 --- a/apps/desktop/src/main/__tests__/task-readiness-notice.test.ts +++ b/apps/desktop/src/main/__tests__/task-readiness-notice.test.ts @@ -63,7 +63,7 @@ test('confirmed repair and unavailable states block, while loading uncertainty d test('model blockers stay owned by existing connection recovery surfaces', () => { assert.equal( - deriveTaskReadinessNotice(snapshot('repair_required', 'model_target'), 'zh'), + deriveTaskReadinessNotice(snapshot('repair_required', 'model_target'), 'zh-CN'), undefined, ); }); diff --git a/apps/desktop/src/main/__tests__/ui-locale-update-gate.test.ts b/apps/desktop/src/main/__tests__/ui-locale-update-gate.test.ts index 87ff3c2f8f..edb1b7acfd 100644 --- a/apps/desktop/src/main/__tests__/ui-locale-update-gate.test.ts +++ b/apps/desktop/src/main/__tests__/ui-locale-update-gate.test.ts @@ -48,8 +48,8 @@ describe('UI locale settings update gate', () => { const savedLocales: string[] = []; assert.equal(gate.commit(firstLocaleTicket, 'en', (next) => savedLocales.push(next)), false); - assert.equal(gate.commit(latestLocaleTicket, 'zh', (next) => savedLocales.push(next)), true); - assert.deepEqual(savedLocales, ['zh']); + assert.equal(gate.commit(latestLocaleTicket, 'zh-CN', (next) => savedLocales.push(next)), true); + assert.deepEqual(savedLocales, ['zh-CN']); }); it('delivers the persisted auto preference without resolving it locally', () => { @@ -71,7 +71,7 @@ describe('UI locale settings update gate', () => { assert.equal(gate.commit(saveTicket, 'en', (next) => applied.push(next)), true); assert.equal( - gate.commitHydration(hydration, 'zh', (next) => applied.push(next)), + gate.commitHydration(hydration, 'zh-CN', (next) => applied.push(next)), false, ); assert.deepEqual(applied, ['en']); @@ -84,7 +84,7 @@ describe('UI locale settings update gate', () => { const applied: string[] = []; assert.equal( - gate.commitHydration(hydration, 'zh', (next) => applied.push(next)), + gate.commitHydration(hydration, 'zh-CN', (next) => applied.push(next)), false, ); assert.equal(gate.commit(saveTicket, 'en', (next) => applied.push(next)), true); @@ -102,7 +102,7 @@ describe('UI locale settings update gate', () => { true, ); assert.equal( - appShellGate.commit(firstSurfaceTicket, 'zh', (next) => applied.push(next)), + appShellGate.commit(firstSurfaceTicket, 'zh-CN', (next) => applied.push(next)), false, ); assert.deepEqual(applied, ['en']); @@ -116,10 +116,10 @@ describe('UI locale settings update gate', () => { gate.cancel(failedSaveTicket); assert.equal( - gate.commitHydration(blockedHydration, 'zh', (next) => applied.push(next)), + gate.commitHydration(blockedHydration, 'zh-CN', (next) => applied.push(next)), true, ); - assert.deepEqual(applied, ['zh']); + assert.deepEqual(applied, ['zh-CN']); }); it('applies the latest blocked hydration when an intervening locale save fails', () => { @@ -129,13 +129,13 @@ describe('UI locale settings update gate', () => { const applied: string[] = []; assert.equal( - gate.commitHydration(hydration, 'zh', (next) => applied.push(next)), + gate.commitHydration(hydration, 'zh-CN', (next) => applied.push(next)), false, ); assert.deepEqual(applied, []); gate.cancel(failedSaveTicket); - assert.deepEqual(applied, ['zh']); + assert.deepEqual(applied, ['zh-CN']); }); it('accepts an older pending save after the newer save fails', () => { diff --git a/apps/desktop/src/main/__tests__/use-onboarding-snapshot.test.ts b/apps/desktop/src/main/__tests__/use-onboarding-snapshot.test.ts index 16a9822e9c..eb6c5c3227 100644 --- a/apps/desktop/src/main/__tests__/use-onboarding-snapshot.test.ts +++ b/apps/desktop/src/main/__tests__/use-onboarding-snapshot.test.ts @@ -104,7 +104,7 @@ describe('createOnboardingSnapshotPoller', () => { onSnapshot: (s) => events.push({ type: 'snap', payload: s }), onError: (m) => events.push({ type: 'err', payload: m }), }, - () => 'zh', + () => 'zh-CN', ); await poller.pull(); assert.deepEqual(events, [{ type: 'err', payload: '鉴权失败' }]); @@ -132,7 +132,7 @@ describe('createOnboardingSnapshotPoller', () => { /* not expected */ }, }, - () => 'zh', + () => 'zh-CN', ); // Fire two overlapping pulls. const pull1 = poller.pull(); @@ -170,7 +170,7 @@ describe('createOnboardingSnapshotPoller', () => { onSnapshot: (s) => snaps.push(s), onError: (m) => errs.push(m), }, - () => 'zh', + () => 'zh-CN', ); const pull1 = poller.pull(); const pull2 = poller.pull(); @@ -196,7 +196,7 @@ describe('createOnboardingSnapshotPoller', () => { onSnapshot: (s) => events.push({ type: 'snap', payload: s }), onError: (m) => events.push({ type: 'err', payload: m }), }, - () => 'zh', + () => 'zh-CN', ); const pull = poller.pull(); @@ -221,7 +221,7 @@ describe('createOnboardingSnapshotPoller', () => { onSnapshot: (s) => events.push({ type: 'snap', payload: s }), onError: (m) => events.push({ type: 'err', payload: m }), }, - () => 'zh', + () => 'zh-CN', ); const pull = poller.pull(); @@ -240,7 +240,7 @@ describe('createOnboardingSnapshotPoller', () => { onSnapshot: (s) => events.push({ type: 'snap', payload: s }), onError: (m) => events.push({ type: 'err', payload: m }), }, - () => 'zh', + () => 'zh-CN', ); poller.dispose(); diff --git a/apps/desktop/src/main/client-settings-confirmation-copy.ts b/apps/desktop/src/main/client-settings-confirmation-copy.ts index 4bec4fd2b9..bc940ebee1 100644 --- a/apps/desktop/src/main/client-settings-confirmation-copy.ts +++ b/apps/desktop/src/main/client-settings-confirmation-copy.ts @@ -24,7 +24,7 @@ export function clientSettingsConfirmation( changes: readonly ClientSettingsChange[], locale: UiLocale, ): { message: string; detail: string; buttons: [string, string] } { - const zh = locale === 'zh'; + const zh = locale !== 'en'; const labels: Record = { theme: ['Theme', '主题'], palette: ['Palette', '配色'], diff --git a/apps/desktop/src/main/computer-use/status-item.ts b/apps/desktop/src/main/computer-use/status-item.ts index 0506a4d5a9..1dba08cb10 100644 --- a/apps/desktop/src/main/computer-use/status-item.ts +++ b/apps/desktop/src/main/computer-use/status-item.ts @@ -132,11 +132,16 @@ interface StatusItemCopy { } const COPY: UiCatalog = { - zh: { + 'zh-CN': { stopUsing: (appName) => `停止操作 ${appName}`, stopUnnamed: '停止 Computer Use', empty: '没有正在进行的任务', }, + 'zh-TW': { + stopUsing: (appName) => `停止操作 ${appName}`, + stopUnnamed: '停止 Computer Use', + empty: '沒有正在進行的任務', + }, en: { stopUsing: (appName) => `Stop Using ${appName}`, stopUnnamed: 'Stop Computer Use', diff --git a/apps/desktop/src/main/e2e-fixture.ts b/apps/desktop/src/main/e2e-fixture.ts index b8643cab17..7f1b9ca0cf 100644 --- a/apps/desktop/src/main/e2e-fixture.ts +++ b/apps/desktop/src/main/e2e-fixture.ts @@ -124,7 +124,9 @@ function parseThemeFlag(raw: string | undefined): 'light' | 'dark' | 'auto' | nu function parseLocaleFlag(raw: string | undefined): UiLocale | null { const normalized = raw?.trim().toLowerCase(); - return normalized === 'zh' || normalized === 'en' ? normalized : null; + return normalized === 'zh-CN' || normalized === 'zh-TW' || normalized === 'en' + ? normalized + : null; } function parseTimezoneFlag(raw: string | undefined): string | null { diff --git a/apps/desktop/src/main/native-diagnostic-dialog.ts b/apps/desktop/src/main/native-diagnostic-dialog.ts index d7d2fb4677..de8b16917e 100644 --- a/apps/desktop/src/main/native-diagnostic-dialog.ts +++ b/apps/desktop/src/main/native-diagnostic-dialog.ts @@ -156,12 +156,18 @@ const DIALOG_COPY = { copied: 'Diagnostics copied. You can paste them into an issue report.', copyFailed: 'Could not copy diagnostics.', }, - zh: { + 'zh-CN': { copy: '复制诊断信息', copyAgain: '再次复制', copied: '诊断信息已复制,可直接粘贴到问题报告中。', copyFailed: '无法复制诊断信息。', }, + 'zh-TW': { + copy: '複製診斷資訊', + copyAgain: '再次複製', + copied: '診斷資訊已複製,可直接貼上到問題報告中。', + copyFailed: '無法複製診斷資訊。', + }, } as const; const FATAL_STARTUP_COPY = { @@ -171,12 +177,18 @@ const FATAL_STARTUP_COPY = { unknownError: 'An unknown startup error occurred.', exit: 'Exit', }, - zh: { + 'zh-CN': { title: 'Maka 启动失败', message: 'Maka 无法完成启动。', unknownError: '启动时发生未知错误。', exit: '退出', }, + 'zh-TW': { + title: 'Maka 啟動失敗', + message: 'Maka 無法完成啟動。', + unknownError: '啟動時發生未知錯誤。', + exit: '退出', + }, } as const; const MAIN_RENDERER_GONE_COPY = { @@ -187,11 +199,18 @@ const MAIN_RENDERER_GONE_COPY = { relaunch: 'Relaunch', exit: 'Exit', }, - zh: { + 'zh-CN': { title: 'Maka 需要恢复', message: 'Maka 界面意外停止运行。', detail: '重新启动 Maka 以继续,或退出后稍后再打开。', relaunch: '重新启动', exit: '退出', }, + 'zh-TW': { + title: 'Maka 需要恢復', + message: 'Maka 介面意外停止執行。', + detail: '重新啟動 Maka 以繼續,或退出後稍後再開啟。', + relaunch: '重新啟動', + exit: '退出', + }, } as const; diff --git a/apps/desktop/src/main/permission-overlay/permission-overlay-copy.ts b/apps/desktop/src/main/permission-overlay/permission-overlay-copy.ts index afb7814138..5311ff0469 100644 --- a/apps/desktop/src/main/permission-overlay/permission-overlay-copy.ts +++ b/apps/desktop/src/main/permission-overlay/permission-overlay-copy.ts @@ -50,7 +50,7 @@ export interface PermissionOverlayCopy { type Catalog = UiCatalog>; const COPY: Catalog = { - zh: { + 'zh-CN': { accessibility: { headline: (appName) => `把 ${appName} 拖到上面的列表里,即可开启「辅助功能」`, fallback: '也可以在系统设置里点 + 号,从「应用程序」中选择本 App。', @@ -69,6 +69,25 @@ const COPY: Catalog = { noBundle: '当前不是以 .app 方式运行,无法拖拽。请在系统设置里手动添加。', }, }, + 'zh-TW': { + accessibility: { + headline: (appName) => `把 ${appName} 拖到上面的列表裡,即可開啟「輔助功能」`, + fallback: '也可以在系統設定裡點 + 號,從「應用程式」中選擇本 App。', + granted: '輔助功能已開啟', + dismiss: '關閉', + dragHint: '拖我', + noBundle: '目前不是以 .app 方式執行,無法拖拽。請在系統設定裡手動新增。', + }, + screen_recording: { + headline: (appName) => `把 ${appName} 拖到上面的列表裡,即可開啟「螢幕錄製」`, + fallback: '也可以在系統設定裡點 + 號,從「應用程式」中選擇本 App。', + granted: '螢幕錄製已開啟', + dismiss: '關閉', + dragHint: '拖我', + restartHint: '若仍顯示未授權,需要重啟 App —— macOS 會快取上一次的拒絕結果。', + noBundle: '目前不是以 .app 方式執行,無法拖拽。請在系統設定裡手動新增。', + }, + }, en: { accessibility: { headline: (appName) => `Drag ${appName} into the list above to allow Accessibility`, diff --git a/apps/desktop/src/main/project-picker-copy.ts b/apps/desktop/src/main/project-picker-copy.ts index 80e2a347be..053768d3a4 100644 --- a/apps/desktop/src/main/project-picker-copy.ts +++ b/apps/desktop/src/main/project-picker-copy.ts @@ -20,5 +20,5 @@ import type { UiLocale } from '@maka/core/ui-locale'; export function projectPickerTitle(locale: UiLocale): string { - return locale === 'zh' ? '添加项目' : 'Add project'; + return locale !== 'en' ? '添加项目' : 'Add project'; } diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index bd0c692f66..91480b91b1 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -1637,7 +1637,7 @@ async function confirmDesktopStorageRootRepair( "[storage-root] root-identity conflict; parking at repair dialog", ); const isChinese = - resolveSystemUiLocale(app.getPreferredSystemLanguages()) === "zh"; + resolveSystemUiLocale(app.getPreferredSystemLanguages()) !== "en"; const { response } = await showStartupDiagnosticDialog( { type: "warning", @@ -1655,7 +1655,7 @@ async function confirmDesktopStorageRootRepair( cancelId: 1, noLink: true, }, - isChinese ? "zh" : "en", + isChinese ? "zh-CN" : "en", ); return response === 0; } @@ -1665,7 +1665,7 @@ async function promptForDefaultRuntimeHostRecovery(input: { readonly error: Error; }): Promise<"retry" | "use_local" | "keep_offline"> { const isChinese = - resolveSystemUiLocale(app.getPreferredSystemLanguages()) === "zh"; + resolveSystemUiLocale(app.getPreferredSystemLanguages()) !== "en"; const { response } = await showStartupDiagnosticDialog( { type: "warning", @@ -1685,7 +1685,7 @@ async function promptForDefaultRuntimeHostRecovery(input: { cancelId: 2, noLink: true, }, - isChinese ? "zh" : "en", + isChinese ? "zh-CN" : "en", ); return response === 0 ? "retry" : response === 1 ? "use_local" : "keep_offline"; } diff --git a/apps/desktop/src/main/runtime-host-quit-copy.ts b/apps/desktop/src/main/runtime-host-quit-copy.ts index f9bfff703d..2a99ca822a 100644 --- a/apps/desktop/src/main/runtime-host-quit-copy.ts +++ b/apps/desktop/src/main/runtime-host-quit-copy.ts @@ -63,7 +63,7 @@ const COPY = { cause: 'Cause', button: 'OK', }, - zh: { + 'zh-CN': { title: '无法安全退出 Maka', message: '本地 Runtime Host 未能安全停止,Maka 仍在运行。', detail: '退出已取消。请重试;如果问题持续存在,请查看诊断信息。', @@ -72,4 +72,13 @@ const COPY = { cause: '原因', button: '好', }, + 'zh-TW': { + title: '無法安全退出 Maka', + message: '本地 Runtime Host 未能安全停止,Maka 仍在執行。', + detail: '退出已取消。請重試;如果問題持續存在,請檢視診斷資訊。', + process: (pid: number) => `Runtime Host 程序 PID:${pid}`, + manual: '如果重試仍然失敗,請先確認沒有需要保留的執行,再透過作業系統的程序管理工具停止該 PID。', + cause: '原因', + button: '好', + }, } as const; diff --git a/apps/desktop/src/main/runtime-host-upgrade-copy.ts b/apps/desktop/src/main/runtime-host-upgrade-copy.ts index 4a4a46c99a..de291e76bc 100644 --- a/apps/desktop/src/main/runtime-host-upgrade-copy.ts +++ b/apps/desktop/src/main/runtime-host-upgrade-copy.ts @@ -109,7 +109,7 @@ const UPGRADE_COPY = { other: 'Other background activity', }, }, - zh: { + 'zh-CN': { title: '旧版 Runtime Host 正在运行', message: '另一个 Runtime Host 进程仍占用此工作区。', restart: '重启 Runtime Host', @@ -127,4 +127,22 @@ const UPGRADE_COPY = { resource: 'Runtime 资源', graph: 'Agent Graph', other: '其他后台活动', }, }, + 'zh-TW': { + title: '舊版 Runtime Host 正在執行', + message: '另一個 Runtime Host 程序仍佔用此工作區。', + restart: '重啟 Runtime Host', + wait: '等待', + cancel: '取消啟動', + uptime: (n: number) => `已執行約 ${n} 分鐘`, + connections: (n: number) => `仍有 ${n} 個其他客戶端連線`, + operations: (n: number) => `有 ${n} 個操作正在執行`, + unknownActivity: '此 Host 版本無法報告後臺活動。', + restartWarning: '重啟會保留持久化狀態,但可能中斷正在進行的外部工作。', + exitOwner: '請退出目前佔用此 Host 的程序,以便安全替換。', + waitExplanation: '若選擇等待,目前 Host 退出後 Maka 將自動繼續。', + activity: { + goal: '目標', scheduledTask: '計劃任務', dailyReview: '每日回顧', execution: '活動執行', + resource: 'Runtime 資源', graph: 'Agent Graph', other: '其他後臺活動', + }, + }, } as const; diff --git a/apps/desktop/src/renderer/agent-graph-panel.tsx b/apps/desktop/src/renderer/agent-graph-panel.tsx index 32e8a38810..fc2de62583 100644 --- a/apps/desktop/src/renderer/agent-graph-panel.tsx +++ b/apps/desktop/src/renderer/agent-graph-panel.tsx @@ -77,7 +77,7 @@ type GraphPanelCopy = { }; export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy { - if (locale === 'zh') { + if (locale !== 'en') { return { title: 'Agent Graph', loading: '正在读取 Graph 状态…', diff --git a/apps/desktop/src/renderer/app-shell-copy.ts b/apps/desktop/src/renderer/app-shell-copy.ts index 2f15529095..6dccb3c07a 100644 --- a/apps/desktop/src/renderer/app-shell-copy.ts +++ b/apps/desktop/src/renderer/app-shell-copy.ts @@ -42,7 +42,7 @@ function sessionMessageErrorMessage(error: unknown, fallback: string, locale: Ui } function localizedErrorMessage(error: unknown, fallback: string, locale: UiLocale): string { - return locale === 'zh' ? generalizedErrorMessageChinese(error, fallback) : generalizedErrorMessage(error, fallback); + return locale !== 'en' ? generalizedErrorMessageChinese(error, fallback) : generalizedErrorMessage(error, fallback); } export function commandPaletteActionErrorMessage(error: unknown, fallback: string, locale: UiLocale): string { diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx index 7a8fc2e9db..4af5d16602 100644 --- a/apps/desktop/src/renderer/app-shell.tsx +++ b/apps/desktop/src/renderer/app-shell.tsx @@ -2856,7 +2856,7 @@ function AppShellContent({ {workHubEnabled && navSelection.section === 'sessions' && activeId ? (