diff --git a/.github/workflows/zh-tw-locale-screenshots.yml b/.github/workflows/zh-tw-locale-screenshots.yml new file mode 100644 index 0000000000..4a1122f1ca --- /dev/null +++ b/.github/workflows/zh-tw-locale-screenshots.yml @@ -0,0 +1,37 @@ +name: Traditional Chinese locale screenshots + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + screenshots: + runs-on: macos-15 + timeout-minutes: 45 + steps: + - name: Check out pull request + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.19.0 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Capture locale screenshots + run: npm --workspace @maka/desktop run e2e -- e2e/zh-tw-locale-screenshots.spec.ts --workers=1 + + - name: Upload locale screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: zh-tw-locale-screenshots + path: apps/desktop/test-results/**/zh-tw-locale/*.png + if-no-files-found: warn diff --git a/apps/desktop/.storybook/preview.tsx b/apps/desktop/.storybook/preview.tsx index f460a84495..52b022704d 100644 --- a/apps/desktop/.storybook/preview.tsx +++ b/apps/desktop/.storybook/preview.tsx @@ -38,7 +38,12 @@ 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' + : context.globals.locale === 'zh-TW' + ? 'zh-TW' + : 'zh-CN'; root.classList.toggle('dark', colorScheme === 'dark'); root.style.colorScheme = colorScheme; @@ -96,7 +101,8 @@ const preview: Preview = { toolbar: { icon: 'globe', items: [ - { title: '中文', value: 'zh' }, + { title: '简体中文', value: 'zh-CN' }, + { title: '繁體中文', value: 'zh-TW' }, { title: 'English', value: 'en' }, ], }, @@ -114,7 +120,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/e2e/zh-tw-locale-screenshots.spec.ts b/apps/desktop/e2e/zh-tw-locale-screenshots.spec.ts new file mode 100644 index 0000000000..3f3c9ddcf9 --- /dev/null +++ b/apps/desktop/e2e/zh-tw-locale-screenshots.spec.ts @@ -0,0 +1,47 @@ +/* + * 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 { mkdir } from 'node:fs/promises'; +import path from 'node:path'; +import { test, expect } from './fixtures'; + +test('captures General Settings in Simplified and Traditional Chinese', async ({ + window: page, +}, testInfo) => { + const outputDirectory = path.join(testInfo.outputDir, 'zh-tw-locale'); + await mkdir(outputDirectory, { recursive: true }); + + await page.getByRole('button', { name: '设置', exact: true }).click(); + await expect(page.getByRole('main', { name: '设置内容' })).toBeVisible(); + await page.getByRole('button', { name: '通用', exact: true }).click(); + await expect(page.getByText('界面语言', { exact: true }).first()).toBeVisible(); + await page.screenshot({ path: path.join(outputDirectory, 'settings-zh-CN.png'), fullPage: true }); + await page.keyboard.press('Escape'); + + await page.evaluate(async () => { + await window.maka.settings.update({ personalization: { uiLocale: 'zh-TW' } }); + }); + await page.reload(); + await page.waitForSelector('.maka-composer-editor'); + await page.getByRole('button', { name: '設定', exact: true }).click(); + await expect(page.getByRole('main', { name: '設定內容' })).toBeVisible(); + await page.getByRole('button', { name: '通用', exact: true }).click(); + await expect(page.getByText('介面語言', { exact: true }).first()).toBeVisible(); + await page.screenshot({ path: path.join(outputDirectory, 'settings-zh-TW.png'), fullPage: true }); +}); 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__/agent-graph-panel-copy.test.ts b/apps/desktop/src/main/__tests__/agent-graph-panel-copy.test.ts new file mode 100644 index 0000000000..0ef254444c --- /dev/null +++ b/apps/desktop/src/main/__tests__/agent-graph-panel-copy.test.ts @@ -0,0 +1,30 @@ +/* + * 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 { getAgentGraphPanelCopy } from '../../renderer/agent-graph-panel.js'; + +test('Traditional Chinese Agent Graph copy does not use Simplified fallbacks', () => { + const copy = getAgentGraphPanelCopy('zh-TW'); + assert.equal(copy.loading, '正在讀取 Graph 狀態…'); + assert.equal(copy.openSession, '開啟子任務'); + assert.equal(copy.currentEpoch, '目前'); + assert.equal(copy.status('active'), '執行中'); +}); 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__/config-transfer-service.test.ts b/apps/desktop/src/main/__tests__/config-transfer-service.test.ts index a904296104..8d5f8098a6 100644 --- a/apps/desktop/src/main/__tests__/config-transfer-service.test.ts +++ b/apps/desktop/src/main/__tests__/config-transfer-service.test.ts @@ -100,6 +100,27 @@ describe('config-transfer-service', () => { assert.deepEqual(writtenMemory, ['# imported memory']); }); + it('canonicalizes a legacy zh preference before the imported settings reach observers', async () => { + const { deps, updatedSettings } = makeDeps(); + const bundle = { + schemaVersion: 1, + exportedAt: '', + appVersion: '0.1.0', + includedData: ['settings'] as const, + data: { + settings: { + personalization: { uiLocale: 'zh', displayName: 'Maka user' }, + }, + }, + }; + + await applyConfigImport(bundle as any, 'skip', deps); + + assert.deepEqual(updatedSettings, [ + { personalization: { uiLocale: 'zh-CN', displayName: 'Maka user' } }, + ]); + }); + it('restores the selection a backup states instead of re-enabling its default', async () => { // A backup can hold a connection whose default model the user had disabled. // `save()` cannot tell a stated selection from one a sync echoed back, so it 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..77b6e4965a 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,20 @@ 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'); +}); + +test('never projects the legacy zh preference into native copy lookups', async () => { + const legacy = mergeSettings(createDefaultSettings(), { + personalization: { uiLocale: 'zh-CN' }, + }); + (legacy.personalization as { uiLocale: string }).uiLocale = 'zh'; + const authority = createDesktopLocaleAuthority({ + readSettings: async () => legacy, + preferredSystemLanguages: () => ['en-US'], + }); + + assert.equal(authority.observe(legacy), 'zh-CN'); + assert.equal(authority.current(), 'zh-CN'); + 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..821ecb27e7 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,12 @@ 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.equal(projectPickerTitle('zh-TW'), '新增專案'); assert.deepEqual( clientSettingsConfirmation( [{ key: 'keepSystemAwake', current: false, next: true }], - 'zh', + 'zh-CN', ), { message: '允许 Maka 更新此客户端的设置吗?', @@ -37,4 +38,15 @@ test('localizes native picker and client settings confirmation copy', () => { buttons: ['应用更改', '取消'], }, ); + assert.deepEqual( + clientSettingsConfirmation( + [{ key: 'theme', current: 'light', next: 'dark' }], + 'zh-TW', + ), + { + message: '允許 Maka 更新此用戶端的設定嗎?', + detail: '主題: light → dark', + buttons: ['套用變更', '取消'], + }, + ); }); diff --git a/apps/desktop/src/main/__tests__/e2e-fixture-locale.test.ts b/apps/desktop/src/main/__tests__/e2e-fixture-locale.test.ts new file mode 100644 index 0000000000..cec94f10f1 --- /dev/null +++ b/apps/desktop/src/main/__tests__/e2e-fixture-locale.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { resolveE2eFixture } from '../e2e-fixture.js'; + +test('preserves both canonical Chinese locale fixture flags', () => { + for (const locale of ['zh-CN', 'zh-TW', 'en'] as const) { + const fixture = resolveE2eFixture( + 'settings-general', + false, + undefined, + undefined, + locale, + ); + assert.equal(fixture?.locale, locale); + } +}); + +test('normalizes locale fixture flag casing without widening the contract', () => { + assert.equal( + resolveE2eFixture('settings-general', false, undefined, undefined, 'ZH-tw')?.locale, + 'zh-TW', + ); + assert.equal( + resolveE2eFixture('settings-general', false, undefined, undefined, 'zh-Hant')?.locale, + null, + ); +}); 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..6ab57b2855 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( @@ -31,3 +31,23 @@ test('labels blocker counts as global across filtered health views', () => { 'Across all health signals, 1 of 6 blocks sending', ); }); + +test('Traditional Chinese health copy localizes structured signal text', () => { + const copy = getHealthCenterCopy('zh-TW'); + const signal = { + id: 'connection:test', + label: '測試 运行态', + scope: 'llm_connection' as const, + layer: 'validation' as const, + status: 'ok' as const, + source: 'connection_test' as const, + checkedAt: 1, + message: '凭据与端点验证已通过。', + detail: '这是连接验证结果,不代表发送、流式输出或中断通路已经运行通过。', + blocksSend: false, + }; + assert.equal(copy.layers.configuration.label, '設定'); + assert.equal(copy.signalLabel(signal), '測試 執行狀態'); + assert.equal(copy.signalMessage(signal), '憑證與端點驗證已通過。'); + assert.match(copy.signalDetail(signal) ?? '', /串流輸出/); +}); 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..a20d0c1300 100644 --- a/apps/desktop/src/main/__tests__/notifications-policy.test.ts +++ b/apps/desktop/src/main/__tests__/notifications-policy.test.ts @@ -48,38 +48,43 @@ 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); + + assert.deepEqual(runNotificationCopy('completed', 'zh-TW'), { + title: '回答已產生', + body: 'Maka 已完成本次回答,按一下以檢視。', + }); }); 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-boot-copy.test.ts b/apps/desktop/src/main/__tests__/runtime-host-boot-copy.test.ts new file mode 100644 index 0000000000..22aa914054 --- /dev/null +++ b/apps/desktop/src/main/__tests__/runtime-host-boot-copy.test.ts @@ -0,0 +1,30 @@ +/* + * 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 { getStartupRecoveryCopy } from '../runtime-host-boot-copy.js'; + +test('startup recovery copy preserves Traditional Chinese', () => { + const copy = getStartupRecoveryCopy('zh-TW'); + assert.equal(copy.storageRoot.title, 'Maka 工作區需要修復'); + assert.match(copy.storageRoot.detail('C:\\Maka'), /磁碟識別碼/); + assert.equal(copy.runtimeHost.title, '預設 Runtime Host 無法連線'); + assert.match(copy.runtimeHost.detail('失敗'), /設定中處理/); +}); 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..c0c5a05203 --- /dev/null +++ b/apps/desktop/src/main/__tests__/settings-preferences-copy.test.ts @@ -0,0 +1,74 @@ +/* + * 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'; +import { getMcpCatalog } from '../../renderer/mcp-catalog.js'; +import { providerDisplay } from '../../renderer/settings/provider-display-copy.js'; +import { getBotSettingsCopy } from '../../renderer/locales/settings-bot-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, '剪貼簿不可用或被系統拒絕。'); +}); + +test('Traditional Chinese MCP catalog does not fall back to Simplified Chinese', () => { + const catalog = getMcpCatalog('zh-TW'); + assert.equal(catalog.find((entry) => entry.id === 'filesystem')?.name, '本機檔案'); + assert.equal(catalog.find((entry) => entry.id === 'google-calendar')?.name, 'Google 日曆'); + assert.equal(catalog.find((entry) => entry.id === 'playwright')?.category, '設計與開發'); +}); + +test('Traditional Chinese provider cards use Taiwan connection terminology', () => { + assert.equal(providerDisplay('deepseek', 'zh-TW').description, 'DeepSeek 官方 API 連線'); + assert.match(providerDisplay('github-copilot', 'zh-TW').description, /訂閱連線/); +}); + +test('Traditional Chinese bot settings use Taiwan integration terminology', () => { + const copy = getBotSettingsCopy('zh-TW'); + assert.match(copy.providers.wecom.help, /串接/); + assert.doesNotMatch(copy.providers.wecom.help, /接入/); + assert.match(copy.overview.more, /串接更多/); + assert.doesNotMatch(copy.overview.more, /渠道|訪問|匹配/); +}); 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__/subscription-result-message.test.ts b/apps/desktop/src/main/__tests__/subscription-result-message.test.ts new file mode 100644 index 0000000000..14dd03b785 --- /dev/null +++ b/apps/desktop/src/main/__tests__/subscription-result-message.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { subscriptionResultMessage } from '../../renderer/settings/subscription-result-message.js'; + +test('Traditional Chinese subscription errors reject raw Simplified backend copy', () => { + assert.equal( + subscriptionResultMessage('GitHub Copilot 登录已过期。', '請重新登入。', 'zh-TW'), + '請重新登入。', + ); + assert.equal( + subscriptionResultMessage('network unreachable', '請重新登入。', 'zh-TW'), + '網路錯誤', + ); +}); 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..5b5444c6be 100644 --- a/apps/desktop/src/main/__tests__/task-readiness-notice.test.ts +++ b/apps/desktop/src/main/__tests__/task-readiness-notice.test.ts @@ -63,11 +63,25 @@ 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, ); }); +test('Traditional Chinese readiness notices use Taiwan terminology', () => { + assert.deepEqual(deriveTaskReadinessNotice(snapshot('unavailable', 'runtime'), 'zh-TW'), { + tone: 'destructive', + title: 'Maka 執行服務暫時無法使用。', + description: '任務尚未送出。重新檢查執行服務後再試。', + actionLabel: '重新檢查', + action: 'retry', + }); + assert.equal( + deriveTaskReadinessNotice(snapshot('repair_required', 'workspace'), 'zh-TW')?.actionLabel, + '選擇工作區', + ); +}); + function snapshot( state: TaskSubmissionReadinessSnapshot['state'], id: 'runtime' | 'model_target' | 'workspace', 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..422e9aef92 100644 --- a/apps/desktop/src/main/client-settings-confirmation-copy.ts +++ b/apps/desktop/src/main/client-settings-confirmation-copy.ts @@ -24,30 +24,38 @@ export function clientSettingsConfirmation( changes: readonly ClientSettingsChange[], locale: UiLocale, ): { message: string; detail: string; buttons: [string, string] } { - const zh = locale === 'zh'; - const labels: Record = { - theme: ['Theme', '主题'], - palette: ['Palette', '配色'], - uiLocale: ['UI language', '界面语言'], - runComplete: ['Run-complete notifications', '回答完成通知'], - keepSystemAwake: ['Keep system awake', '保持系统唤醒'], + const labels: Record = { + theme: ['Theme', '主题', '主題'], + palette: ['Palette', '配色', '色彩配置'], + uiLocale: ['UI language', '界面语言', '介面語言'], + runComplete: ['Run-complete notifications', '回答完成通知', '回答完成通知'], + keepSystemAwake: ['Keep system awake', '保持系统唤醒', '保持系統喚醒'], }; + const localeIndex = locale === 'en' ? 0 : locale === 'zh-CN' ? 1 : 2; const value = (input: string | boolean | undefined): string => { - if (!zh) return String(input); - if (input === true) return '开启'; - if (input === false) return '关闭'; + if (locale === 'en') return String(input); + if (input === true) return locale === 'zh-CN' ? '开启' : '開啟'; + if (input === false) return locale === 'zh-CN' ? '关闭' : '關閉'; return String(input); }; return { - message: zh - ? '允许 Maka 更新此客户端的设置吗?' - : "Allow Maka to update this client's settings?", + message: + locale === 'en' + ? "Allow Maka to update this client's settings?" + : locale === 'zh-CN' + ? '允许 Maka 更新此客户端的设置吗?' + : '允許 Maka 更新此用戶端的設定嗎?', detail: changes .map( (change) => - `${labels[change.key][zh ? 1 : 0]}: ${value(change.current)} → ${value(change.next)}`, + `${labels[change.key][localeIndex]}: ${value(change.current)} → ${value(change.next)}`, ) .join('\n'), - buttons: zh ? ['应用更改', '取消'] : ['Apply changes', 'Cancel'], + buttons: + locale === 'en' + ? ['Apply changes', 'Cancel'] + : locale === 'zh-CN' + ? ['应用更改', '取消'] + : ['套用變更', '取消'], }; } 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/config-transfer-service.ts b/apps/desktop/src/main/config-transfer-service.ts index a66cc53701..326ddc1d60 100644 --- a/apps/desktop/src/main/config-transfer-service.ts +++ b/apps/desktop/src/main/config-transfer-service.ts @@ -18,6 +18,7 @@ */ import type { AppSettings, UpdateAppSettingsInput } from '@maka/core/settings'; +import { normalizeUiLocalePreference } from '@maka/core/ui-locale'; import { reconcileConnectionAfterEnabledModelsChange, type LlmConnection, @@ -101,7 +102,21 @@ export async function applyConfigImport( } if (bundle.data.settings && typeof bundle.data.settings === 'object') { - await deps.settingsStore.update(bundle.data.settings as unknown as UpdateAppSettingsInput); + const patch = bundle.data.settings as unknown as UpdateAppSettingsInput; + const importedPersonalization = patch.personalization as + | (NonNullable & { uiLocale?: unknown }) + | undefined; + await deps.settingsStore.update( + importedPersonalization && Object.hasOwn(importedPersonalization, 'uiLocale') + ? { + ...patch, + personalization: { + ...importedPersonalization, + uiLocale: normalizeUiLocalePreference(importedPersonalization.uiLocale), + }, + } + : patch, + ); result.settings = { applied: true }; } diff --git a/apps/desktop/src/main/desktop-locale-authority.ts b/apps/desktop/src/main/desktop-locale-authority.ts index 8690d2ce73..2e2f423b55 100644 --- a/apps/desktop/src/main/desktop-locale-authority.ts +++ b/apps/desktop/src/main/desktop-locale-authority.ts @@ -18,6 +18,7 @@ */ import { + normalizeUiLocalePreference, resolveSystemUiLocale, resolveUiLocale, type UiLocale, @@ -65,7 +66,7 @@ export function createDesktopLocaleAuthority(input: { }; const observe = (settings: LocaleSettings): UiLocale => { observation += 1; - preference = settings.personalization.uiLocale; + preference = normalizeUiLocalePreference(settings.personalization.uiLocale); return publish(); }; return Object.freeze({ @@ -79,7 +80,9 @@ export function createDesktopLocaleAuthority(input: { const request = ++observation; try { const settings = await input.readSettings(); - if (request === observation) preference = settings.personalization.uiLocale; + if (request === observation) { + preference = normalizeUiLocalePreference(settings.personalization.uiLocale); + } } catch { // Locale presentation must not block startup or an unrelated native action. } diff --git a/apps/desktop/src/main/e2e-fixture.ts b/apps/desktop/src/main/e2e-fixture.ts index b8643cab17..af617d1c6f 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; + if (normalized === 'zh-cn') return 'zh-CN'; + if (normalized === 'zh-tw') return 'zh-TW'; + return normalized === 'en' ? 'en' : 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/notifications-policy.ts b/apps/desktop/src/main/notifications-policy.ts index efd72ba96b..8795aa00d9 100644 --- a/apps/desktop/src/main/notifications-policy.ts +++ b/apps/desktop/src/main/notifications-policy.ts @@ -86,6 +86,11 @@ export function runNotificationCopy( ? { title: 'Conversation error', body: 'This response did not finish. Click to view details.' } : { title: 'Response ready', body: 'Maka finished this response. Click to view it.' }; } + if (locale === 'zh-TW') { + return kind === 'errored' + ? { title: '任務發生錯誤', body: '本次回答未能完成,按一下以檢視詳細資料。' } + : { title: '回答已產生', body: 'Maka 已完成本次回答,按一下以檢視。' }; + } if (kind === 'errored') { return { title: '任务出错', body: '本轮回答未能完成,点击查看详情。' }; } 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..45a2598914 100644 --- a/apps/desktop/src/main/project-picker-copy.ts +++ b/apps/desktop/src/main/project-picker-copy.ts @@ -20,5 +20,7 @@ import type { UiLocale } from '@maka/core/ui-locale'; export function projectPickerTitle(locale: UiLocale): string { - return locale === 'zh' ? '添加项目' : 'Add project'; + if (locale === 'zh-CN') return '添加项目'; + if (locale === 'zh-TW') return '新增專案'; + return 'Add project'; } diff --git a/apps/desktop/src/main/runtime-host-boot-copy.ts b/apps/desktop/src/main/runtime-host-boot-copy.ts new file mode 100644 index 0000000000..fb1489d8c7 --- /dev/null +++ b/apps/desktop/src/main/runtime-host-boot-copy.ts @@ -0,0 +1,90 @@ +/* + * 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 type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; + +interface StartupRecoveryCopy { + storageRoot: { + title: string; + message: string; + detail(workspaceRoot: string): string; + buttons: [string, string]; + }; + runtimeHost: { + title: string; + message(profileName: string): string; + detail(message: string): string; + buttons: [string, string, string]; + }; +} + +const STARTUP_RECOVERY_COPY = { + 'zh-CN': { + storageRoot: { + title: 'Maka 工作区需要修复', + message: 'Maka 无法验证这个工作区。', + detail: (workspaceRoot) => + `系统中的磁盘标识可能发生了变化。仅当这是本机原来的 Maka 工作区、而不是复制出的工作区时,才选择修复。\n\n${workspaceRoot}`, + buttons: ['修复工作区', '退出'], + }, + runtimeHost: { + title: '默认 Runtime Host 无法连接', + message: (profileName) => `无法连接 ${profileName}`, + detail: (message) => + `${message}\n\n你可以重试、改用 Local 作为默认 Host,或保持当前选择并稍后在设置中处理。`, + buttons: ['重试', '改用 Local', '保持离线'], + }, + }, + 'zh-TW': { + storageRoot: { + title: 'Maka 工作區需要修復', + message: 'Maka 無法驗證這個工作區。', + detail: (workspaceRoot) => + `系統中的磁碟識別碼可能已變更。只有當這是本機原本的 Maka 工作區,而不是複製的工作區時,才選擇修復。\n\n${workspaceRoot}`, + buttons: ['修復工作區', '離開'], + }, + runtimeHost: { + title: '預設 Runtime Host 無法連線', + message: (profileName) => `無法連線 ${profileName}`, + detail: (message) => + `${message}\n\n你可以重試、改用 Local 作為預設 Host,或保留目前選擇並稍後在設定中處理。`, + buttons: ['重試', '改用 Local', '保持離線'], + }, + }, + en: { + storageRoot: { + title: 'Maka workspace needs repair', + message: 'Maka cannot verify this workspace.', + detail: (workspaceRoot) => + `The disk identity may have changed. Repair only if this is the original Maka workspace on this computer, not a copied workspace.\n\n${workspaceRoot}`, + buttons: ['Repair Workspace', 'Exit'], + }, + runtimeHost: { + title: 'Default Runtime Host is unavailable', + message: (profileName) => `Could not connect to ${profileName}`, + detail: (message) => + `${message}\n\nRetry, use Local as the default Host, or keep the current selection and resolve it later in Settings.`, + buttons: ['Retry', 'Use Local', 'Keep Offline'], + }, + }, +} satisfies UiCatalog; + +export function getStartupRecoveryCopy(locale: UiLocale): StartupRecoveryCopy { + return STARTUP_RECOVERY_COPY[locale]; +} diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index bd0c692f66..e2087a0a13 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -105,6 +105,7 @@ import { showMainRendererProcessGoneDialog, showMessageBoxWithDiagnostics, } from "./native-diagnostic-dialog.js"; +import { getStartupRecoveryCopy } from "./runtime-host-boot-copy.js"; import { resolveDesktopSessionWorkspace, } from "./new-session-project.js"; @@ -1636,26 +1637,20 @@ async function confirmDesktopStorageRootRepair( console.log( "[storage-root] root-identity conflict; parking at repair dialog", ); - const isChinese = - resolveSystemUiLocale(app.getPreferredSystemLanguages()) === "zh"; + const locale = resolveSystemUiLocale(app.getPreferredSystemLanguages()); + const copy = getStartupRecoveryCopy(locale).storageRoot; const { response } = await showStartupDiagnosticDialog( { type: "warning", - title: isChinese ? "Maka 工作区需要修复" : "Maka workspace needs repair", - message: isChinese - ? "Maka 无法验证这个工作区。" - : "Maka cannot verify this workspace.", - detail: isChinese - ? `系统中的磁盘标识可能发生了变化。仅当这是本机原来的 Maka 工作区、而不是复制出的工作区时,才选择修复。\n\n${workspaceRoot}` - : `The disk identity may have changed. Repair only if this is the original Maka workspace on this computer, not a copied workspace.\n\n${workspaceRoot}`, - buttons: isChinese - ? ["修复工作区", "退出"] - : ["Repair Workspace", "Exit"], + title: copy.title, + message: copy.message, + detail: copy.detail(workspaceRoot), + buttons: copy.buttons, defaultId: 1, cancelId: 1, noLink: true, }, - isChinese ? "zh" : "en", + locale, ); return response === 0; } @@ -1664,28 +1659,20 @@ async function promptForDefaultRuntimeHostRecovery(input: { readonly profileName: string; readonly error: Error; }): Promise<"retry" | "use_local" | "keep_offline"> { - const isChinese = - resolveSystemUiLocale(app.getPreferredSystemLanguages()) === "zh"; + const locale = await desktopLocale.resolve(); + const copy = getStartupRecoveryCopy(locale).runtimeHost; const { response } = await showStartupDiagnosticDialog( { type: "warning", - title: isChinese - ? "默认 Runtime Host 无法连接" - : "Default Runtime Host is unavailable", - message: isChinese - ? `无法连接 ${input.profileName}` - : `Could not connect to ${input.profileName}`, - detail: isChinese - ? `${input.error.message}\n\n你可以重试、改用 Local 作为默认 Host,或保持当前选择并稍后在设置中处理。` - : `${input.error.message}\n\nRetry, use Local as the default Host, or keep the current selection and resolve it later in Settings.`, - buttons: isChinese - ? ["重试", "改用 Local", "保持离线"] - : ["Retry", "Use Local", "Keep Offline"], + title: copy.title, + message: copy.message(input.profileName), + detail: copy.detail(input.error.message), + buttons: copy.buttons, defaultId: 0, cancelId: 2, noLink: true, }, - isChinese ? "zh" : "en", + locale, ); 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..48a3bbcda6 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 === 'zh-CN') { return { title: 'Agent Graph', loading: '正在读取 Graph 状态…', @@ -125,6 +125,54 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy { wait: waitReasonZh, }; } + if (locale === 'zh-TW') { + return { + title: 'Agent Graph', + loading: '正在讀取 Graph 狀態…', + retry: '重試', + collapse: '收起 Agent Graph', + expand: '展開 Agent Graph', + dismiss: '關閉 Agent Graph', + stop: '停止 Graph', + stopping: '停止中…', + stopFailed: '停止 Graph 失敗,請重試。', + loadFailed: 'Graph 狀態重新整理失敗。', + openSession: '開啟子任務', + operators: 'Operators', + selectedResults: '已選取結果', + epoch: 'Graph 執行輪次', + currentEpoch: '目前', + historicalEpoch: '歷史記錄(唯讀)', + cappedEpochs: (count) => `僅顯示最近 ${count} 次執行`, + noOperators: '等待主 Agent 建立 operator…', + hiddenOperators: (count) => `另有 ${count} 個 operator`, + progress: (settled, total, hasOmitted) => + hasOmitted ? `可見項目中 ${settled}/${total} 已結束` : `${settled}/${total} 已結束`, + status: (status) => + ({ + empty: '等待排程', + active: '執行中', + closing: '收尾中', + waiting: '等待中', + stopped: '已停止', + failed: '失敗', + completed: '已完成', + })[status], + operatorStatus: (status) => + ({ + not_started: '尚未啟動', + waiting: '等待', + runnable: '可執行', + running: '執行中', + blocked: '受阻', + completed: '完成', + failed: '失敗', + aborted: '已中止', + cancelled: '已取消', + })[status], + wait: waitReasonZhTw, + }; + } return { title: 'Agent Graph', loading: 'Loading graph state…', @@ -566,3 +614,15 @@ function waitReasonZh(operator: AgentGraphClientOperator): string | undefined { } return `等待 ${wait.operatorId} 结束`; } + +function waitReasonZhTw(operator: AgentGraphClientOperator): string | undefined { + const wait = firstWait(operator); + if (!wait) return undefined; + if (wait.kind === 'input_route') { + return `等待 ${wait.upstreamOperatorIds.join('、')} 的輸入`; + } + if (wait.kind === 'activation_missing') { + return `等待 ${wait.operatorId} 啟用`; + } + return `等待 ${wait.operatorId} 結束`; +} diff --git a/apps/desktop/src/renderer/app-shell-command-actions.ts b/apps/desktop/src/renderer/app-shell-command-actions.ts index bb42b79a18..9179b01425 100644 --- a/apps/desktop/src/renderer/app-shell-command-actions.ts +++ b/apps/desktop/src/renderer/app-shell-command-actions.ts @@ -299,7 +299,16 @@ export function buildAppShellCommandList( window.maka.memory.openFile(host), ); if (!result.ok) { - toastApi.error(copy.memoryOpenFailedTitle, result.message, undefined, diagnosticTarget); + toastApi.error( + copy.memoryOpenFailedTitle, + commandPaletteActionErrorMessage( + new Error(result.message), + copy.memoryOpenFallback, + options.uiLocale, + ), + undefined, + diagnosticTarget, + ); } } catch (err) { toastApi.error( diff --git a/apps/desktop/src/renderer/app-shell-copy.ts b/apps/desktop/src/renderer/app-shell-copy.ts index 2f15529095..8e5d7c82fb 100644 --- a/apps/desktop/src/renderer/app-shell-copy.ts +++ b/apps/desktop/src/renderer/app-shell-copy.ts @@ -20,7 +20,7 @@ import type { ConnectionTestResult } from '@maka/core/llm-connections'; import type { TextFileImportPreflightFailureReason } from '@maka/core/text-file-import'; import type { UiLocale } from '@maka/core/ui-locale'; -import { generalizedErrorMessage, generalizedErrorMessageChinese } from '@maka/core/redaction'; +import { generalizedErrorMessageForLocale } from '@maka/core/redaction'; import { getShellCopy } from './locales/shell-copy.js'; const SESSION_READ_MESSAGES_ERROR_MARKER = 'MAKA_SESSION_READ_MESSAGES_ERROR:'; @@ -36,13 +36,13 @@ export function messageRefreshErrorMessage(error: unknown, locale: UiLocale): st function sessionMessageErrorMessage(error: unknown, fallback: string, locale: UiLocale): string { const raw = error instanceof Error ? error.message : String(error); const markerIndex = raw.indexOf(SESSION_READ_MESSAGES_ERROR_MARKER); - if (markerIndex < 0 || locale === 'en') return localizedErrorMessage(error, fallback, locale); + if (markerIndex < 0 || locale !== 'zh-CN') return localizedErrorMessage(error, fallback, locale); const marked = raw.slice(markerIndex + SESSION_READ_MESSAGES_ERROR_MARKER.length).trim(); return marked.split(/\r?\n/, 1)[0]?.trim() || fallback; } function localizedErrorMessage(error: unknown, fallback: string, locale: UiLocale): string { - return locale === 'zh' ? generalizedErrorMessageChinese(error, fallback) : generalizedErrorMessage(error, fallback); + return generalizedErrorMessageForLocale(error, fallback, locale); } export function commandPaletteActionErrorMessage(error: unknown, fallback: string, locale: UiLocale): string { diff --git a/apps/desktop/src/renderer/app-shell-effects.ts b/apps/desktop/src/renderer/app-shell-effects.ts index 20c83217a8..3c1099ac2a 100644 --- a/apps/desktop/src/renderer/app-shell-effects.ts +++ b/apps/desktop/src/renderer/app-shell-effects.ts @@ -25,7 +25,6 @@ import type { SessionEvent } from '@maka/core/events'; import type { SessionEventStreamSnapshot } from '@maka/core/session-event-health'; import type { ThemePalette, ThemePreference } from '@maka/core/settings'; import type { UiLocale } from '@maka/core/ui-locale'; -import { generalizedErrorMessageChinese } from '@maka/core/redaction'; import { sessionExpectsEventStream } from '@maka/core/session-event-health'; import { type ShellRunUpdate } from '@maka/core/events'; import type { LiveTurnProjection, NavSelection } from '@maka/ui'; 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 ? (