You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by zh...@apache.org on 2026/09/04 13:29:58 UTC
(maka) branch main updated: feat(i18n): add Traditional Chinese locale (#3853)
This is an automated email from the ASF dual-hosted git repository.
me2seeks pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new b1369c8087 feat(i18n): add Traditional Chinese locale (#3853)
b1369c8087 is described below
commit b1369c80875c02b8d6335dd440d3deb76fe69e59
Author: AcerYue <33...@users.noreply.github.com>
AuthorDate: Fri Sep 4 21:29:52 2026 +0800
feat(i18n): add Traditional Chinese locale (#3853)
Add separate zh-CN and zh-TW resolved locales, including Taiwan-localized UI catalogs and system-locale aliases, while preserving the former persisted zh preference as zh-CN.
Replace the Desktop Settings language segmented control with the @maka/ui Selector, offering Follow system, Simplified Chinese, Traditional Chinese, and English.
Co-authored-by: AcerYue <33...@users.noreply.github.com>
Co-authored-by: JLee <pk...@photons.com.tw>
---
apps/desktop/.storybook/preview.tsx | 12 +-
apps/desktop/e2e/accessibility-coverage.spec.ts | 2 +-
apps/desktop/e2e/context-window-save.spec.ts | 2 +-
apps/desktop/e2e/fixtures.ts | 34 +-
.../e2e/request-header-row-contract.spec.ts | 2 +-
apps/desktop/e2e/zh-tw-locale.spec.ts | 42 ++
apps/desktop/renderer-architecture.json | 13 +-
.../src/main/__tests__/about-update-status.test.ts | 4 +-
.../active-execution-boundary-read-model.test.ts | 2 +-
...copy.test.ts => agent-graph-panel-copy.test.ts} | 17 +-
.../main/__tests__/command-palette-retired.test.ts | 4 +-
.../computer-use-status-item-locale.test.ts | 2 +-
.../main/__tests__/config-transfer-service.test.ts | 21 +
.../__tests__/desktop-locale-authority.test.ts | 57 ++-
.../src/main/__tests__/desktop-native-copy.test.ts | 16 +-
...ter-copy.test.ts => e2e-fixture-locale.test.ts} | 25 +-
.../src/main/__tests__/goal-arm-outcome.test.ts | 2 +-
.../src/main/__tests__/health-center-copy.test.ts | 22 +-
.../__tests__/import-tasks-settings-page.test.ts | 4 +-
.../main/__tests__/message-queue-ui-state.test.ts | 2 +-
.../main/__tests__/new-task-staged-content.test.ts | 10 +-
.../main/__tests__/notifications-policy.test.ts | 21 +-
.../main/__tests__/permission-center-copy.test.ts | 2 +-
.../src/main/__tests__/plan-mode-copy.test.ts | 2 +-
.../__tests__/provider-connection-status.test.ts | 10 +-
.../main/__tests__/renderer-error-report.test.ts | 2 +-
...copy.test.ts => runtime-host-boot-copy.test.ts} | 17 +-
.../main/__tests__/runtime-host-quit-copy.test.ts | 4 +-
.../__tests__/runtime-host-upgrade-dialog.test.ts | 12 +-
.../session-inspector-context-format.test.ts | 2 +-
.../__tests__/session-status-presentation.test.ts | 22 +-
.../__tests__/settings-preferences-copy.test.ts | 74 +++
.../__tests__/settings-test-result-copy.test.ts | 2 +-
.../src/main/__tests__/streaming-handoff.test.ts | 24 +-
...test.ts => subscription-result-message.test.ts} | 12 +-
.../main/__tests__/task-readiness-notice.test.ts | 16 +-
.../traditional-chinese-peer-mesh-copy.test.ts | 59 +++
.../main/__tests__/ui-locale-update-gate.test.ts | 18 +-
.../main/__tests__/use-onboarding-snapshot.test.ts | 12 +-
.../src/main/__tests__/use-session-todo.test.ts | 151 ++++++
.../main/__tests__/workhub-surface-flow.test.ts | 8 +-
apps/desktop/src/main/browser-message-box.ts | 6 +-
.../src/main/client-settings-confirmation-copy.ts | 38 +-
apps/desktop/src/main/computer-use/status-item.ts | 7 +-
apps/desktop/src/main/config-transfer-service.ts | 15 +-
apps/desktop/src/main/desktop-locale-authority.ts | 7 +-
apps/desktop/src/main/e2e-fixture.ts | 4 +-
apps/desktop/src/main/main.ts | 23 +-
.../src/main/native-diagnostic-dialog-copy.ts | 51 ++-
apps/desktop/src/main/notifications-policy.ts | 5 +
.../permission-overlay/permission-overlay-copy.ts | 21 +-
apps/desktop/src/main/project-picker-copy.ts | 4 +-
apps/desktop/src/main/runtime-host-boot-copy.ts | 90 ++++
apps/desktop/src/main/runtime-host-quit-copy.ts | 18 +-
apps/desktop/src/main/runtime-host-upgrade-copy.ts | 25 +-
.../src/preload/runtime-host-session-catalog.ts | 2 +-
apps/desktop/src/renderer/agent-graph-panel.tsx | 62 ++-
apps/desktop/src/renderer/app-shell-copy.ts | 6 +-
apps/desktop/src/renderer/app-shell-effects.ts | 1 -
apps/desktop/src/renderer/app-shell.tsx | 2 +-
apps/desktop/src/renderer/attachment-preflight.ts | 2 +-
apps/desktop/src/renderer/conversation-markdown.ts | 2 +-
apps/desktop/src/renderer/daily-review-actions.ts | 4 +-
.../src/renderer/derive-turn-lineage-badges.ts | 2 +-
.../renderer/features/connection-settings/index.ts | 1 +
.../connection-settings/provider-panel-shared.ts | 30 +-
.../connection-settings/settings-provider-copy.ts | 199 +++++++-
.../subscription-result-message.ts | 53 +++
.../conversation/model/task-readiness-notice.ts | 78 ++--
.../features/runtime-host-management/index.ts | 23 +-
.../ui/runtime-host-peer-mesh-dialog.tsx | 197 +++++++-
.../model/session-project-grouping.ts | 2 +-
.../src/renderer/features/workbar/testing.ts | 1 +
.../workbar/tools/artifacts/artifact-pane.tsx | 8 +-
.../workbar/tools/inspector/use-session-trace.ts | 10 +-
.../workbar/tools/review/session-review-panel.tsx | 6 +-
.../tools/side-chat/quote-companion-panel.tsx | 9 +-
.../workbar/tools/tasks/use-session-todo.ts | 7 +-
.../tools/terminal/session-terminal-panel.tsx | 13 +-
apps/desktop/src/renderer/locales/artifact-copy.ts | 38 +-
apps/desktop/src/renderer/locales/browser-copy.ts | 26 +-
.../src/renderer/locales/conversation-copy.ts | 247 +++++++++-
.../locales/external-session-import-copy.ts | 58 ++-
apps/desktop/src/renderer/locales/mcp-copy.ts | 61 ++-
.../src/renderer/locales/onboarding-copy.ts | 51 ++-
.../src/renderer/locales/permission-center-copy.ts | 49 +-
.../desktop/src/renderer/locales/plan-mode-copy.ts | 21 +-
.../renderer/locales/session-collaboration-copy.ts | 105 ++++-
.../src/renderer/locales/settings-bot-copy.ts | 92 +++-
.../renderer/locales/settings-daily-review-copy.ts | 21 +-
.../src/renderer/locales/settings-data-copy.ts | 29 +-
.../src/renderer/locales/settings-health-copy.ts | 86 +++-
.../src/renderer/locales/settings-memory-copy.ts | 47 +-
.../renderer/locales/settings-navigation-copy.ts | 28 +-
.../renderer/locales/settings-preferences-copy.ts | 97 +++-
.../src/renderer/locales/settings-projects-copy.ts | 305 ++++++++++++-
.../src/renderer/locales/settings-shared-copy.ts | 42 +-
.../renderer/locales/settings-subagents-copy.ts | 84 +++-
.../src/renderer/locales/settings-tasks-copy.ts | 29 +-
.../renderer/locales/settings-test-result-copy.ts | 27 +-
.../src/renderer/locales/settings-usage-copy.ts | 24 +-
.../renderer/locales/settings-web-search-copy.ts | 19 +-
apps/desktop/src/renderer/locales/shell-copy.ts | 508 ++++++++++++++++++++-
.../src/renderer/locales/shell-remaining-copy.ts | 78 +++-
apps/desktop/src/renderer/mcp-catalog.ts | 22 +-
apps/desktop/src/renderer/model-catalog-choices.ts | 2 +-
.../src/renderer/model-connection-errors.ts | 6 +-
.../src/renderer/session-error-presentation.ts | 2 +-
.../src/renderer/session-status-presentation.ts | 4 +-
.../src/renderer/settings/about-settings-page.tsx | 10 +-
.../src/renderer/settings/about-update-status.ts | 8 +-
.../src/renderer/settings/bot-chat-detail.tsx | 8 +-
.../src/renderer/settings/bot-chat-overview.tsx | 4 +-
.../src/renderer/settings/bot-chat-shared.tsx | 4 +-
.../src/renderer/settings/bot-onboarding-modal.tsx | 6 +-
.../settings/daily-review-settings-page.tsx | 2 +-
.../src/renderer/settings/data-settings-page.tsx | 2 +-
.../settings/memory-settings-view-model.ts | 2 +-
.../renderer/settings/permission-center-page.tsx | 4 +-
.../settings/personalization-settings-section.tsx | 23 +-
.../src/renderer/settings/provider-add-form.tsx | 2 +-
.../renderer/settings/provider-catalog-page.tsx | 5 +-
.../settings/provider-connection-status.ts | 2 +-
.../src/renderer/settings/provider-display-copy.ts | 183 +++++---
.../renderer/settings/provider-oauth-section.tsx | 2 +-
.../src/renderer/settings/providers-panel.tsx | 3 +-
.../settings/runtime-host-management-dialog.tsx | 28 +-
.../settings/runtime-host-profiles-section.tsx | 42 +-
.../src/renderer/settings/settings-error-copy.ts | 9 +-
.../settings/use-memory-settings-controller.ts | 2 +-
.../src/renderer/settings/use-oauth-login-flow.ts | 37 +-
apps/desktop/src/renderer/turn-footer-actions.ts | 2 +-
.../src/renderer/use-onboarding-snapshot.ts | 4 +-
apps/desktop/src/renderer/workhub-surface.tsx | 74 ++-
apps/desktop/stories/agent-graph-panel.stories.tsx | 2 +-
apps/desktop/stories/app-shell.stories.tsx | 2 +-
apps/desktop/stories/error-boundary.stories.tsx | 3 +-
.../stories/settings/settings-pages.stories.tsx | 14 +-
docs/astryx-surface-file-inventory.md | 2 +-
packages/cli/src/__tests__/cli-ui-locale.test.ts | 42 +-
packages/cli/src/__tests__/cli.test.ts | 2 +-
packages/cli/src/__tests__/pi-transcript.test.ts | 10 +-
.../cli/src/__tests__/pi-tui-mcp-status.test.ts | 270 +++++++----
packages/cli/src/__tests__/pi-tui-runner.test.ts | 16 +-
.../cli/src/__tests__/tui-copy-catalog.test.ts | 91 ++--
.../cli/src/__tests__/tui-copy-command.test.ts | 2 +-
.../cli/src/__tests__/tui-primary-guidance.test.ts | 10 +-
.../cli/src/__tests__/tui-session-status.test.ts | 4 +-
packages/cli/src/cli-ui-locale.ts | 10 +-
packages/cli/src/pi-tui-mcp-status.ts | 16 +-
packages/cli/src/tui-copy-catalog.ts | 361 ++++++++++++++-
packages/core/src/__tests__/redaction.test.ts | 37 ++
packages/core/src/__tests__/relative-time.test.ts | 10 +-
packages/core/src/__tests__/settings.test.ts | 26 ++
.../core/src/__tests__/tool-quiet-preview.test.ts | 16 +-
packages/core/src/__tests__/ui-locale.test.ts | 64 ++-
packages/core/src/redaction.ts | 87 +++-
packages/core/src/relative-time.ts | 11 +-
packages/core/src/settings.ts | 10 +-
packages/core/src/tool-quiet-preview.ts | 20 +-
packages/core/src/ui-locale.ts | 24 +-
.../ui/src/__tests__/conversation-copy.test.ts | 15 +-
.../ui/src/__tests__/daily-review-copy.test.ts | 7 +-
.../ui/src/__tests__/live-turn-projection.test.ts | 2 +-
packages/ui/src/__tests__/markdown-body.test.ts | 2 +-
packages/ui/src/__tests__/materialize.test.ts | 4 +-
.../ui/src/__tests__/runtime-resume-copy.test.ts | 8 +-
.../src/__tests__/session-hover-card-copy.test.ts | 2 +-
.../__tests__/tool-activity-presentation.test.ts | 6 +-
.../ui/src/__tests__/tool-format.test.ts | 20 +-
.../ui/src/__tests__/transcript-projection.test.ts | 2 +-
.../ui/src/__tests__/turn-running-spinner.test.tsx | 4 +-
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 | 160 ++++++-
packages/ui/src/daily-review-copy.ts | 37 +-
packages/ui/src/daily-review-helpers.ts | 6 +-
packages/ui/src/live-turn-projection.ts | 2 +-
packages/ui/src/locale-helpers.ts | 10 +-
packages/ui/src/runtime-resume-copy.ts | 39 +-
packages/ui/src/scheduled-task-copy.ts | 37 +-
packages/ui/src/scheduled-task-panel.tsx | 12 +-
packages/ui/src/search-modal.tsx | 6 +-
packages/ui/src/session-hover-card-copy.ts | 14 +-
packages/ui/src/session-status-presentation.ts | 4 +-
packages/ui/src/shared-ui-copy.ts | 86 +++-
packages/ui/src/shell-controls-copy.ts | 33 +-
packages/ui/src/skills-copy.ts | 17 +-
packages/ui/src/thinking-stream.ts | 4 +-
packages/ui/src/tool-activity.tsx | 2 +-
packages/ui/src/tool-activity/copy.ts | 106 ++++-
packages/ui/src/tool-activity/preview-utils.ts | 2 +-
packages/ui/src/tool-activity/result-projection.ts | 2 +-
packages/ui/src/tool-format.ts | 6 +-
packages/ui/src/tool-output-stream.ts | 2 +-
scripts/fixture-env.mjs | 2 +-
201 files changed, 5525 insertions(+), 866 deletions(-)
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 8d55ab9c85..e04e7ddd66 100644
--- a/apps/desktop/e2e/accessibility-coverage.spec.ts
+++ b/apps/desktop/e2e/accessibility-coverage.spec.ts
@@ -72,7 +72,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/context-window-save.spec.ts b/apps/desktop/e2e/context-window-save.spec.ts
index 41b42b1cdf..a6094fda4b 100644
--- a/apps/desktop/e2e/context-window-save.spec.ts
+++ b/apps/desktop/e2e/context-window-save.spec.ts
@@ -20,7 +20,7 @@
import { expect, test } from './fixtures';
import { getProviderSettingsCopy } from '../src/renderer/features/connection-settings';
-const copy = getProviderSettingsCopy('zh').detail;
+const copy = getProviderSettingsCopy('zh-CN').detail;
const MODEL_ID = 'custom-reasoner';
test('one save persists a context window that is still focused', async ({
diff --git a/apps/desktop/e2e/fixtures.ts b/apps/desktop/e2e/fixtures.ts
index 688c6379ea..45a3b91bb1 100644
--- a/apps/desktop/e2e/fixtures.ts
+++ b/apps/desktop/e2e/fixtures.ts
@@ -202,7 +202,7 @@ async function seedE2eConnection(userDataDir: string): Promise<void> {
}
}
-async function seedE2eLocale(userDataDir: string, locale: 'zh' | 'en'): Promise<void> {
+async function seedE2eLocale(userDataDir: string, locale: 'zh-CN' | 'zh-TW' | 'en'): Promise<void> {
const workspaceRoot = path.join(userDataDir, 'workspaces', 'default');
await createSettingsStore(workspaceRoot).update({
personalization: { uiLocale: locale },
@@ -426,7 +426,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. */
@@ -603,7 +603,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
}, { scope: 'worker', auto: true }],
directoryReferenceWindow: async ({}, use) => {
await withE2eWindow(
- { seed: true, readinessSelector: COMPOSER_INPUT, locale: 'zh', showWindow: true },
+ { seed: true, readinessSelector: COMPOSER_INPUT, locale: 'zh-CN', showWindow: true },
async (page, { userDataDir, app }) => {
const folder = path.join(userDataDir, 'referenced-source');
await mkdir(path.join(folder, 'nested'), { recursive: true });
@@ -620,7 +620,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
},
// 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);
},
agentGraphWindow: async ({}, use) => {
await withE2eWindow(
@@ -628,7 +628,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
seed: false,
readinessSelector: '.maka-agent-graph-panel',
e2eFixtureScenario: 'agent-graph-layout',
- locale: 'zh',
+ locale: 'zh-CN',
showWindow: true,
},
use,
@@ -638,7 +638,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
await withE2eWindow({
seed: false,
readinessSelector: '[data-maka-contract="onboarding-card"]',
- locale: 'zh',
+ locale: 'zh-CN',
showWindow: true,
}, use);
},
@@ -647,7 +647,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
{
seed: true,
readinessSelector: COMPOSER_INPUT,
- locale: 'zh',
+ locale: 'zh-CN',
gitReviewExtraFiles: 0,
},
async (page, context) => {
@@ -663,7 +663,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
await withE2eWindow({
seed: true,
readinessSelector: COMPOSER_INPUT,
- locale: 'zh',
+ locale: 'zh-CN',
invocableSkills: true,
}, use);
},
@@ -680,7 +680,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
await withE2eWindow({
seed: true,
readinessSelector: COMPOSER_INPUT,
- locale: 'zh',
+ locale: 'zh-CN',
newTaskProject: true,
showWindow: true,
}, use);
@@ -694,7 +694,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
seed: false,
readinessSelector: '[data-maka-contract="search-modal"][open]',
e2eFixtureScenario: 'sidebar-search-modal-open',
- locale: 'zh',
+ locale: 'zh-CN',
}, use);
},
parentRemovalWindow: async ({}, use) => {
@@ -702,7 +702,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
{
seed: true,
readinessSelector: COMPOSER_INPUT,
- locale: 'zh',
+ locale: 'zh-CN',
parentRemovalSessions: true,
},
use,
@@ -713,7 +713,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
{
seed: true,
readinessSelector: COMPOSER_INPUT,
- locale: 'zh',
+ locale: 'zh-CN',
railRenderSessions: true,
},
use,
@@ -734,7 +734,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
// Every other fixture window names its locale; without one the renderer
// takes the host's, so any test that reaches a control by its label
// passes on a Chinese desktop and cannot find it on an English CI runner.
- locale: 'zh',
+ locale: 'zh-CN',
showWindow: true,
}, async (page, { app }) => {
const viewport = await page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
@@ -760,7 +760,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
seed: false,
readinessSelector: '[data-turn-id]',
e2eFixtureScenario: 'chat-partial-history',
- locale: 'zh',
+ locale: 'zh-CN',
showWindow: true,
}, use);
},
@@ -773,7 +773,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
seed: false,
readinessSelector: '.settingsSurface',
e2eFixtureScenario: 'settings-models',
- locale: 'zh',
+ locale: 'zh-CN',
showWindow: true,
}, use);
},
@@ -782,7 +782,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
seed: false,
readinessSelector: '.settingsCapabilityGroup',
e2eFixtureScenario: 'settings-permissions',
- locale: 'zh',
+ locale: 'zh-CN',
showWindow: true,
}, use);
},
@@ -794,7 +794,7 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
seed: false,
readinessSelector: '[data-turn-id]',
e2eFixtureScenario: 'turn-narrative',
- 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 c820b17743..772d5f5ae9 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/features/connection-set
* 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.spec.ts b/apps/desktop/e2e/zh-tw-locale.spec.ts
new file mode 100644
index 0000000000..801542b2b2
--- /dev/null
+++ b/apps/desktop/e2e/zh-tw-locale.spec.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { ensureSidebarExpanded, expect, test } from './fixtures';
+
+test('switches General Settings from Simplified to Traditional Chinese', async ({
+ window: page,
+}) => {
+ await ensureSidebarExpanded(page);
+ 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.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 ensureSidebarExpanded(page);
+ 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();
+});
diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json
index 8067c02959..74a02cf6df 100644
--- a/apps/desktop/renderer-architecture.json
+++ b/apps/desktop/renderer-architecture.json
@@ -408,8 +408,8 @@
"./locales/shell-copy.js": 1,
"@maka/core/redaction": 1
},
- "importSpecifiers": 3,
- "nonTriviaTokens": 515
+ "importSpecifiers": 2,
+ "nonTriviaTokens": 504
},
"src/renderer/app-shell-detail-panel.tsx": {
"importDeclarations": 0,
@@ -444,7 +444,7 @@
"nonTriviaTokens": 672
},
"src/renderer/app-shell-effects.ts": {
- "importDeclarations": 13,
+ "importDeclarations": 12,
"bridgePaths": {
"window.maka.app.info": 1,
"window.maka.appWindow.subscribeCommand": 1,
@@ -490,12 +490,11 @@
"./theme": 1,
"./titlebar-modal-sync": 1,
"@astryxdesign/core/hooks": 1,
- "@maka/core/redaction": 1,
"@maka/core/session-event-health": 1,
"react": 1
},
- "importSpecifiers": 21,
- "nonTriviaTokens": 3823
+ "importSpecifiers": 20,
+ "nonTriviaTokens": 3816
},
"src/renderer/app-shell-overlays.tsx": {
"importDeclarations": 8,
@@ -2909,7 +2908,6 @@
"./settings-expandable-row": 1,
"./settings-section": 1,
"./settings-skeleton.js": 1,
- "@astryxdesign/core": 1,
"@maka/ui": 1,
"react": 1
}
@@ -3894,7 +3892,6 @@
"../features/connection-settings": 1,
"./oauth-login-flow-guard": 1,
"./runtime-host-settings-target.js": 1,
- "@maka/core/redaction": 1,
"@maka/ui": 1,
"react": 1
}
diff --git a/apps/desktop/src/main/__tests__/about-update-status.test.ts b/apps/desktop/src/main/__tests__/about-update-status.test.ts
index c3a33fae15..a3e7e70ac2 100644
--- a/apps/desktop/src/main/__tests__/about-update-status.test.ts
+++ b/apps/desktop/src/main/__tests__/about-update-status.test.ts
@@ -25,7 +25,7 @@ import {
} from '../../renderer/settings/about-update-status.js';
import { getSettingsPreferencesCopy } from '../../renderer/locales/settings-preferences-copy.js';
-const copy = getSettingsPreferencesCopy('zh').about;
+const copy = getSettingsPreferencesCopy('zh-CN').about;
test('a packaged nightly is tokened Nightly, never 正式版', () => {
const facts = aboutChannelFacts({ buildMode: 'packaged', updateChannel: 'nightly' }, copy);
@@ -47,7 +47,7 @@ test('buildMode decides before updateChannel, whose dev value is a placeholder',
test('the nightly steady states each read as themselves', () => {
const detail = (status: Parameters<typeof aboutUpdateStatusDetail>[0]) =>
- aboutUpdateStatusDetail(status, copy);
+ aboutUpdateStatusDetail(status, copy, { isDevBuild: false });
assert.equal(
detail({
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 c2fbe9867d..bdf9b42987 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
@@ -278,7 +278,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__/health-center-copy.test.ts b/apps/desktop/src/main/__tests__/agent-graph-panel-copy.test.ts
similarity index 67%
copy from apps/desktop/src/main/__tests__/health-center-copy.test.ts
copy to apps/desktop/src/main/__tests__/agent-graph-panel-copy.test.ts
index e8f7a6aa5f..0ef254444c 100644
--- a/apps/desktop/src/main/__tests__/health-center-copy.test.ts
+++ b/apps/desktop/src/main/__tests__/agent-graph-panel-copy.test.ts
@@ -19,15 +19,12 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
-import { getHealthCenterCopy } from '../../renderer/locales/settings-health-copy.js';
+import { getAgentGraphPanelCopy } from '../../renderer/agent-graph-panel.js';
-test('labels blocker counts as global across filtered health views', () => {
- assert.equal(
- getHealthCenterCopy('zh').blockers.send(1, 6),
- '全部健康信号中,1/6 条会阻塞发送',
- );
- assert.equal(
- getHealthCenterCopy('en').blockers.send(1, 6),
- 'Across all health signals, 1 of 6 blocks sending',
- );
+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__/command-palette-retired.test.ts b/apps/desktop/src/main/__tests__/command-palette-retired.test.ts
index 9df42247d3..46b8fff553 100644
--- a/apps/desktop/src/main/__tests__/command-palette-retired.test.ts
+++ b/apps/desktop/src/main/__tests__/command-palette-retired.test.ts
@@ -85,7 +85,7 @@ test('a stale in-memory default pointing at a retired connection is not testable
assert.ok(!ids.includes('diag:test-default'));
});
-for (const locale of ['en', 'zh'] as const) {
+for (const locale of ['en', 'zh-CN'] as const) {
test(`${locale} static shortcut hints preserve both platform variants`, () => {
const commands = buildCommandList({
locale,
@@ -111,6 +111,6 @@ for (const locale of ['en', 'zh'] as const) {
other: 'Ctrl+,',
});
assert.equal(byId.get('diag:copy-diagnostics')?.platformHint?.other, 'Ctrl+Shift+D · ' +
- (locale === 'zh' ? '脱敏日志 · 仅写入剪贴板' : 'Redacted logs · clipboard only'));
+ (locale === 'zh-CN' ? '脱敏日志 · 仅写入剪贴板' : 'Redacted logs · clipboard only'));
});
}
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 2d15fb0eab..ebee2a46c0 100644
--- a/apps/desktop/src/main/__tests__/config-transfer-service.test.ts
+++ b/apps/desktop/src/main/__tests__/config-transfer-service.test.ts
@@ -104,6 +104,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('reports a settings-carried proxy credential skipped by Host target binding', async () => {
const { deps } = makeDeps({
settingsStore: {
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..184049a523 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,38 @@ 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('resolves a delayed persisted preference before startup native copy is selected', async () => {
+ let release!: (settings: ReturnType<typeof createDefaultSettings>) => void;
+ const pending = new Promise<ReturnType<typeof createDefaultSettings>>((resolve) => {
+ release = resolve;
+ });
+ const authority = createDesktopLocaleAuthority({
+ readSettings: () => pending,
+ preferredSystemLanguages: () => ['en-US'],
+ });
+
+ const startupLocale = authority.resolve();
+ release(mergeSettings(createDefaultSettings(), {
+ personalization: { uiLocale: 'zh-TW' },
+ }));
+
+ assert.equal(await startupLocale, 'zh-TW');
+});
+
+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__/health-center-copy.test.ts b/apps/desktop/src/main/__tests__/e2e-fixture-locale.test.ts
similarity index 58%
copy from apps/desktop/src/main/__tests__/health-center-copy.test.ts
copy to apps/desktop/src/main/__tests__/e2e-fixture-locale.test.ts
index e8f7a6aa5f..cec94f10f1 100644
--- a/apps/desktop/src/main/__tests__/health-center-copy.test.ts
+++ b/apps/desktop/src/main/__tests__/e2e-fixture-locale.test.ts
@@ -19,15 +19,28 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
-import { getHealthCenterCopy } from '../../renderer/locales/settings-health-copy.js';
+import { resolveE2eFixture } from '../e2e-fixture.js';
-test('labels blocker counts as global across filtered health views', () => {
+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(
- getHealthCenterCopy('zh').blockers.send(1, 6),
- '全部健康信号中,1/6 条会阻塞发送',
+ resolveE2eFixture('settings-general', false, undefined, undefined, 'ZH-tw')?.locale,
+ 'zh-TW',
);
assert.equal(
- getHealthCenterCopy('en').blockers.send(1, 6),
- 'Across all health signals, 1 of 6 blocks sending',
+ 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 a62994ecaf..885cdcea20 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: {
@@ -1211,7 +1211,7 @@ async function renderPage(options: {
*/
importBySource?: Record<string, 'ok' | 'unknown' | 'throw'>;
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 ece5f859fc..1eddb50f65 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__/new-task-staged-content.test.ts b/apps/desktop/src/main/__tests__/new-task-staged-content.test.ts
index ee1ca35b77..b0fa5ee43e 100644
--- a/apps/desktop/src/main/__tests__/new-task-staged-content.test.ts
+++ b/apps/desktop/src/main/__tests__/new-task-staged-content.test.ts
@@ -66,7 +66,7 @@ afterEach(async () => {
async function mountProbe<T>(useHook: (options: { draftKey: string }) => T): Promise<{
latest(): T;
- render(draftKey: string, locale?: 'en' | 'zh'): Promise<void>;
+ render(draftKey: string, locale?: 'en' | 'zh-CN'): Promise<void>;
}> {
const { document, window } = parseHTML('<div id="root"></div>');
Object.assign(globalThis, {
@@ -88,7 +88,7 @@ async function mountProbe<T>(useHook: (options: { draftKey: string }) => T): Pro
return null;
}
- const render = async (draftKey: string, locale: 'en' | 'zh' = 'en') => {
+ const render = async (draftKey: string, locale: 'en' | 'zh-CN' = 'en') => {
await act(async () => {
root.render(
createElement(LocaleProvider, {
@@ -275,16 +275,16 @@ test('AppShell composition shows the localized non-vision image notice once per
description: en.imageAttachmentNotDirectDescription,
}]);
- await probe.render('session-zh', 'zh');
+ await probe.render('session-zh', 'zh-CN');
await act(() => probe.latest().attachFilePaths([image]));
- const zh = getDesktopConversationCopy('zh').actions;
+ const zh = getDesktopConversationCopy('zh-CN').actions;
assert.deepEqual(calls[1], {
title: zh.imageAttachmentNotDirectTitle,
description: zh.imageAttachmentNotDirectDescription,
});
probe.latest().imageNoticeLifecycle.transfer('session-zh', 'session-transferred');
- await probe.render('session-transferred', 'zh');
+ await probe.render('session-transferred', 'zh-CN');
await act(() => probe.latest().attachFilePaths([image]));
assert.equal(calls.length, 2);
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__/renderer-error-report.test.ts b/apps/desktop/src/main/__tests__/renderer-error-report.test.ts
index 367e118023..530f26b45c 100644
--- a/apps/desktop/src/main/__tests__/renderer-error-report.test.ts
+++ b/apps/desktop/src/main/__tests__/renderer-error-report.test.ts
@@ -64,7 +64,7 @@ test('bounds and redacts the browser-only renderer error report', () => {
test('keeps raw exceptions out of the localized crash surface', () => {
for (const [locale, title] of [
- ['zh', 'Maka 渲染层崩溃了'],
+ ['zh-CN', 'Maka 渲染层崩溃了'],
['en', 'The Maka renderer crashed'],
] as const) {
const markup = renderToStaticMarkup(
diff --git a/apps/desktop/src/main/__tests__/health-center-copy.test.ts b/apps/desktop/src/main/__tests__/runtime-host-boot-copy.test.ts
similarity index 65%
copy from apps/desktop/src/main/__tests__/health-center-copy.test.ts
copy to apps/desktop/src/main/__tests__/runtime-host-boot-copy.test.ts
index e8f7a6aa5f..22aa914054 100644
--- a/apps/desktop/src/main/__tests__/health-center-copy.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-boot-copy.test.ts
@@ -19,15 +19,12 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
-import { getHealthCenterCopy } from '../../renderer/locales/settings-health-copy.js';
+import { getStartupRecoveryCopy } from '../runtime-host-boot-copy.js';
-test('labels blocker counts as global across filtered health views', () => {
- assert.equal(
- getHealthCenterCopy('zh').blockers.send(1, 6),
- '全部健康信号中,1/6 条会阻塞发送',
- );
- assert.equal(
- getHealthCenterCopy('en').blockers.send(1, 6),
- 'Across all health signals, 1 of 6 blocks sending',
- );
+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 0b03b5a540..1c03be70d7 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
@@ -41,7 +41,7 @@ const manualFailure = new DesktopLocalHostRetirementError(
{ cause: failure.cause },
);
-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(manualFailure, locale);
@@ -54,7 +54,7 @@ for (const locale of ['en', 'zh'] as const) {
test('manual recovery copy names a cross-platform process-management concept', () => {
const english = buildRuntimeHostQuitFailureDialog(manualFailure, 'en').options.detail ?? '';
- const chinese = buildRuntimeHostQuitFailureDialog(manualFailure, 'zh').options.detail ?? '';
+ const chinese = buildRuntimeHostQuitFailureDialog(manualFailure, 'zh-CN').options.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 fa1da71958..2c4abd86c1 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
@@ -48,7 +48,12 @@ test('localizes upgrade activity without changing decision indexes', () => {
const zh = buildRuntimeHostUpgradeDialog(
conflict,
'restart',
- 'zh',
+ 'zh-CN',
+ ).options;
+ const zhTw = buildRuntimeHostUpgradeDialog(
+ conflict,
+ 'restart',
+ 'zh-TW',
).options;
assert.deepEqual(en.buttons, ['Restart Runtime Host', 'Wait', 'Cancel Startup']);
assert.deepEqual(zh.buttons, ['重启 Runtime Host', '等待', '取消启动']);
@@ -58,6 +63,9 @@ test('localizes upgrade activity without changing decision indexes', () => {
assert.match(zh.detail ?? '', /每日回顾: 1/);
assert.match(en.detail ?? '', /Scheduled Task: 2/);
assert.match(zh.detail ?? '', /计划任务: 2/);
+ assert.deepEqual(zhTw.buttons, ['重啟 Runtime Host', '等待', '取消啟動']);
+ assert.match(zhTw.detail ?? '', /仍有 2 個其他客戶端連線/);
+ assert.match(zhTw.detail ?? '', /每日回顧: 1/);
assert.match(en.detail ?? '', /Process ID \(PID\):/);
});
@@ -148,7 +156,7 @@ test('explains when the safe replacement check could not verify idle state', ()
const dialog = buildRuntimeHostUpgradeDialog(
conflict,
'replace_may_interrupt_work',
- 'zh',
+ 'zh-CN',
);
assert.match(dialog.options.detail ?? '', /无法确认此 Host 是否处于空闲状态/u);
diff --git a/apps/desktop/src/main/__tests__/session-inspector-context-format.test.ts b/apps/desktop/src/main/__tests__/session-inspector-context-format.test.ts
index b8fd943106..410f265169 100644
--- a/apps/desktop/src/main/__tests__/session-inspector-context-format.test.ts
+++ b/apps/desktop/src/main/__tests__/session-inspector-context-format.test.ts
@@ -22,7 +22,7 @@ import { test } from 'node:test';
import { compactNumberFormatter } from '../../renderer/features/workbar/testing.js';
test('formats context-window capacity with stable K/M units in every UI locale', () => {
- for (const locale of ['en', 'zh'] as const) {
+ for (const locale of ['en', 'zh-CN', 'zh-TW'] as const) {
const format = compactNumberFormatter(locale);
assert.equal(format(256_000), '256K');
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 7f2473c70f..133ad3e7da 100644
--- a/apps/desktop/src/main/__tests__/session-status-presentation.test.ts
+++ b/apps/desktop/src/main/__tests__/session-status-presentation.test.ts
@@ -33,7 +33,8 @@ const NOTHING_RAN = {
describe('failed turn presentation', () => {
it('presents persisted provider server errors as provider failures', () => {
- assert.match(describeTurnErrorClass('server_error', 'zh'), /模型服务返回错误/);
+ assert.match(describeTurnErrorClass('server_error', 'zh-CN'), /模型服务返回错误/);
+ assert.match(describeTurnErrorClass('server_error', 'zh-TW'), /模型服務回傳錯誤/);
assert.match(describeTurnErrorClass('server_error', 'en'), /model service returned an error/i);
});
@@ -41,7 +42,7 @@ describe('failed turn presentation', () => {
// The banner offers a button only for `app_restarted`; every other class
// has to point at the one action that always exists — send a message.
for (const errorClass of ['rate_limit', 'network', 'timeout', 'unknown_failure']) {
- assert.match(describeTurnErrorClass(errorClass, 'zh'), /重新发消息|再发消息|发消息/);
+ assert.match(describeTurnErrorClass(errorClass, 'zh-CN'), /重新发消息|再发消息|发消息/);
}
});
@@ -59,7 +60,7 @@ describe('failed turn execution state', () => {
it('warns that a completed tool may already have taken effect', () => {
// A blind resend after a side-effecting tool can repeat that effect, so
// this has to survive alongside a transport failure like `timeout`.
- const zh = describeFailedTurnExecutionState({ ...NOTHING_RAN, toolActivityCount: 1 }, 'zh');
+ const zh = describeFailedTurnExecutionState({ ...NOTHING_RAN, toolActivityCount: 1 }, 'zh-CN');
assert.match(zh ?? '', /执行过工具|实际改动/);
const en = describeFailedTurnExecutionState({ ...NOTHING_RAN, toolActivityCount: 1 }, 'en');
assert.match(en ?? '', /tools already ran/i);
@@ -70,26 +71,27 @@ describe('failed turn execution state', () => {
// the tool branch win, so `auth` plus an errored tool advised "inspect the
// tool result" and dropped the sign-in step. They are separate slots now.
const state = { ...NOTHING_RAN, toolActivityCount: 1, erroredToolCount: 1 };
- assert.match(describeTurnErrorClass('auth', 'zh'), /重新连接或登录/);
- assert.match(describeFailedTurnExecutionState(state, 'zh') ?? '', /工具执行出错/);
- assert.match(describeTurnErrorClass('context_overflow', 'zh'), /减少附件|开启新任务/);
+ assert.match(describeTurnErrorClass('auth', 'zh-CN'), /重新连接或登录/);
+ assert.match(describeFailedTurnExecutionState(state, 'zh-CN') ?? '', /工具执行出错/);
+ assert.match(describeTurnErrorClass('context_overflow', 'zh-CN'), /减少附件|开启新任务/);
+ assert.match(describeFailedTurnExecutionState(state, 'zh-TW') ?? '', /工具執行出錯/);
});
it('reports nothing when the turn left nothing to re-read', () => {
- assert.equal(describeFailedTurnExecutionState(NOTHING_RAN, 'zh'), undefined);
+ assert.equal(describeFailedTurnExecutionState(NOTHING_RAN, 'zh-CN'), undefined);
});
it('prefers the most specific state the turn reached', () => {
const all = { partialOutputRetained: true, toolActivityCount: 2, erroredToolCount: 1 };
- assert.match(describeFailedTurnExecutionState(all, 'zh') ?? '', /工具执行出错/);
+ assert.match(describeFailedTurnExecutionState(all, 'zh-CN') ?? '', /工具执行出错/);
assert.match(
- describeFailedTurnExecutionState({ ...all, erroredToolCount: 0 }, 'zh') ?? '',
+ describeFailedTurnExecutionState({ ...all, erroredToolCount: 0 }, 'zh-CN') ?? '',
/执行过工具/,
);
assert.match(
describeFailedTurnExecutionState(
{ ...NOTHING_RAN, partialOutputRetained: true },
- 'zh',
+ 'zh-CN',
) ?? '',
/部分回答/,
);
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__/settings-test-result-copy.test.ts b/apps/desktop/src/main/__tests__/settings-test-result-copy.test.ts
index 757ced374c..9f86548727 100644
--- a/apps/desktop/src/main/__tests__/settings-test-result-copy.test.ts
+++ b/apps/desktop/src/main/__tests__/settings-test-result-copy.test.ts
@@ -29,7 +29,7 @@ test("missing proxy credentials have actionable bilingual copy", () => {
} as never;
assert.equal(
- settingsTestResultMessage(result, "zh"),
+ settingsTestResultMessage(result, "zh-CN"),
"代理认证已开启,请输入代理密码后再测试。",
);
assert.equal(
diff --git a/apps/desktop/src/main/__tests__/streaming-handoff.test.ts b/apps/desktop/src/main/__tests__/streaming-handoff.test.ts
index 16e8f1de69..1f910c38f5 100644
--- a/apps/desktop/src/main/__tests__/streaming-handoff.test.ts
+++ b/apps/desktop/src/main/__tests__/streaming-handoff.test.ts
@@ -39,7 +39,7 @@ import { waitFor as pollFor } from '@maka/core/test-only/async-primitives';
function renderWithLocale(child: ReactNode): string {
return renderToStaticMarkup(
createElement(LocaleProvider, {
- locale: 'zh',
+ locale: 'zh-CN',
children: createElement(ChatSurfaceLayout, { composer: null, children: child }),
}),
);
@@ -303,7 +303,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) => {
@@ -356,7 +356,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,
@@ -410,7 +410,7 @@ describe('single live-turn handoff', () => {
const displayBatch = createAppShellSessionDisplayBatch();
let publications = 0;
const handlers = createAppShellSessionEventHandlers({
- uiLocale: 'zh',
+ uiLocale: 'zh-CN',
activeIdRef: { current: 'session-1' },
liveTurnBySessionRef,
refreshMessages: async () => true,
@@ -477,7 +477,7 @@ describe('single live-turn handoff', () => {
const interactions = createStateSetter<InteractionQueues>({});
const frames: Array<() => void> = [];
const handlers = createAppShellSessionEventHandlers({
- uiLocale: 'zh',
+ uiLocale: 'zh-CN',
activeIdRef: { current: 'session-1' },
liveTurnBySessionRef,
refreshMessages: async () => true,
@@ -515,7 +515,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,
@@ -578,7 +578,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,
@@ -625,7 +625,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,
@@ -724,7 +724,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,
@@ -773,7 +773,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,
@@ -846,7 +846,7 @@ describe('single live-turn handoff', () => {
const ref = { current: liveTurns.get() };
const interactions = createStateSetter<InteractionQueues>({});
const handlers = createAppShellSessionEventHandlers({
- uiLocale: 'zh',
+ uiLocale: 'zh-CN',
activeIdRef: { current: 'session-1' },
liveTurnBySessionRef: ref,
refreshMessages: async () => true,
@@ -886,7 +886,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__/health-center-copy.test.ts b/apps/desktop/src/main/__tests__/subscription-result-message.test.ts
similarity index 69%
copy from apps/desktop/src/main/__tests__/health-center-copy.test.ts
copy to apps/desktop/src/main/__tests__/subscription-result-message.test.ts
index e8f7a6aa5f..843717e9f0 100644
--- a/apps/desktop/src/main/__tests__/health-center-copy.test.ts
+++ b/apps/desktop/src/main/__tests__/subscription-result-message.test.ts
@@ -19,15 +19,15 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
-import { getHealthCenterCopy } from '../../renderer/locales/settings-health-copy.js';
+import { subscriptionResultMessage } from '../../renderer/features/connection-settings/index.js';
-test('labels blocker counts as global across filtered health views', () => {
+test('Traditional Chinese subscription errors reject raw Simplified backend copy', () => {
assert.equal(
- getHealthCenterCopy('zh').blockers.send(1, 6),
- '全部健康信号中,1/6 条会阻塞发送',
+ subscriptionResultMessage('GitHub Copilot 登录已过期。', '請重新登入。', 'zh-TW'),
+ '請重新登入。',
);
assert.equal(
- getHealthCenterCopy('en').blockers.send(1, 6),
- 'Across all health signals, 1 of 6 blocks sending',
+ 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 5fb9c4dd3f..5937431acd 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__/traditional-chinese-peer-mesh-copy.test.ts b/apps/desktop/src/main/__tests__/traditional-chinese-peer-mesh-copy.test.ts
new file mode 100644
index 0000000000..67af56d965
--- /dev/null
+++ b/apps/desktop/src/main/__tests__/traditional-chinese-peer-mesh-copy.test.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { getRuntimeHostPeerMeshCopy } from '../../renderer/features/runtime-host-management/index.js';
+import { getDesktopConversationCopy } from '../../renderer/locales/conversation-copy.js';
+import { getProviderSettingsCopy } from '../../renderer/features/connection-settings/index.js';
+import { settingsTestResultMessage } from '../../renderer/locales/settings-test-result-copy.js';
+
+test('Traditional Chinese Peer Mesh copy does not use the Simplified Chinese branch', () => {
+ const copy = getRuntimeHostPeerMeshCopy('zh-TW');
+
+ assert.equal(copy.experimental, '實驗性');
+ assert.equal(copy.invalidResult, 'Peer Mesh 回傳了無效結果');
+ assert.equal(copy.copyPeerId('peer-1'), '複製完整 Peer ID:peer-1');
+ assert.equal(copy.peerIdCopyFailed, '無法複製 Peer ID');
+ assert.equal(copy.peerPathDirect, '直接連線');
+ assert.equal(copy.peerPathTransit, '成員轉送');
+ assert.equal(copy.peerPathOther, '其他');
+ assert.equal(copy.joinHint, '貼上另一個 Peer 產生的一次性邀請碼。');
+});
+
+test('Traditional Chinese connection copy uses 回傳, 回應, and 發送', () => {
+ const conversation = getDesktopConversationCopy('zh-TW');
+ assert.equal(conversation.quoteCompanion.errors.respondFailed, '回應失敗,請稍後重試。');
+ assert.match(conversation.health.reauth.tooltip, /回傳鑑權失敗/);
+ assert.match(conversation.health.reauth.tooltip, /攔截發送/);
+ assert.match(conversation.health.testError.tooltip, /攔截發送/);
+ assert.match(conversation.turnError.provider, /模型服務回傳錯誤/);
+
+ const provider = getProviderSettingsCopy('zh-TW');
+ assert.equal(provider.shared.lastTest['模型服务返回错误'], '模型服務回傳錯誤');
+ assert.equal(provider.shared.lastTest['provider returned an error'], '模型服務回傳錯誤');
+
+ assert.equal(
+ settingsTestResultMessage(
+ { ok: false, code: 'proxy_http_error', message: '', details: { status: 502 } },
+ 'zh-TW',
+ ),
+ '代理測試回傳 HTTP 502,請檢查代理服務或測試地址。',
+ );
+});
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 d60c58c767..35084cadae 100644
--- a/apps/desktop/src/main/__tests__/use-onboarding-snapshot.test.ts
+++ b/apps/desktop/src/main/__tests__/use-onboarding-snapshot.test.ts
@@ -102,7 +102,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: '鉴权失败' }]);
@@ -130,7 +130,7 @@ describe('createOnboardingSnapshotPoller', () => {
/* not expected */
},
},
- () => 'zh',
+ () => 'zh-CN',
);
// Fire two overlapping pulls.
const pull1 = poller.pull();
@@ -168,7 +168,7 @@ describe('createOnboardingSnapshotPoller', () => {
onSnapshot: (s) => snaps.push(s),
onError: (m) => errs.push(m),
},
- () => 'zh',
+ () => 'zh-CN',
);
const pull1 = poller.pull();
const pull2 = poller.pull();
@@ -194,7 +194,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();
@@ -219,7 +219,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();
@@ -238,7 +238,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/__tests__/use-session-todo.test.ts b/apps/desktop/src/main/__tests__/use-session-todo.test.ts
new file mode 100644
index 0000000000..ca05257f6a
--- /dev/null
+++ b/apps/desktop/src/main/__tests__/use-session-todo.test.ts
@@ -0,0 +1,151 @@
+/*
+ * 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 { strict as assert } from 'node:assert';
+import { afterEach, describe, it } from 'node:test';
+import { act, createElement } from 'react';
+import type { SessionTodoItem } from '@maka/core/session-todo';
+import type { UiLocale } from '@maka/ui';
+import { cleanupFakeDom, installReactRenderer } from './fake-dom.js';
+import {
+ createFakeWorkbarServices,
+ useSessionTodo,
+ WorkbarServicesProvider,
+ type WorkbarServices,
+} from '../../renderer/features/workbar/testing.js';
+
+/**
+ * Regression coverage for the Traditional Chinese error-copy leak
+ * (2026-09-02 review): this hook used to route zh-CN and zh-TW through the
+ * same hard-coded Simplified helper, so a zh-TW reader saw 请求超时 instead
+ * of 請求逾時. It now goes through the shared, locale-aware classifier like
+ * every sibling surface (use-session-trace.ts, session-review-panel.tsx,
+ * session-terminal-panel.tsx, artifact-pane.tsx).
+ */
+
+function Probe(props: {
+ services: WorkbarServices;
+ sessionId?: string;
+ locale: UiLocale;
+ onSnapshot: (snapshot: ReturnType<typeof useSessionTodo>) => void;
+}) {
+ return createElement(
+ WorkbarServicesProvider,
+ { services: props.services },
+ createElement(TodoProbe, props),
+ );
+}
+
+function TodoProbe(props: {
+ sessionId?: string;
+ locale: UiLocale;
+ onSnapshot: (snapshot: ReturnType<typeof useSessionTodo>) => void;
+}) {
+ const snapshot = useSessionTodo(props.sessionId, {
+ locale: props.locale,
+ loadFailed: 'load failed',
+ });
+ props.onSnapshot(snapshot);
+ return null;
+}
+
+function createHarness(read: () => Promise<SessionTodoItem[]>): WorkbarServices {
+ return createFakeWorkbarServices({
+ todo: {
+ read,
+ subscribeChanges: () => () => undefined,
+ },
+ });
+}
+
+describe('useSessionTodo', () => {
+ afterEach(() => {
+ cleanupFakeDom();
+ delete (globalThis as { window?: unknown }).window;
+ });
+
+ it('renders the Traditional Chinese category copy for a zh-TW reader, not Simplified', async () => {
+ const { root } = installReactRenderer();
+ const services = createHarness(async () => {
+ throw new Error('request timeout');
+ });
+ let snapshot: ReturnType<typeof useSessionTodo> | undefined;
+
+ await act(async () => {
+ root.render(
+ createElement(Probe, {
+ services,
+ sessionId: 'session-1',
+ locale: 'zh-TW',
+ onSnapshot: (value) => {
+ snapshot = value;
+ },
+ }),
+ );
+ });
+
+ assert.equal(snapshot?.error, '請求逾時', 'a zh-TW reader must not see Simplified 请求超时');
+ });
+
+ it('still renders the Simplified Chinese category copy for a zh-CN reader', async () => {
+ const { root } = installReactRenderer();
+ const services = createHarness(async () => {
+ throw new Error('request timeout');
+ });
+ let snapshot: ReturnType<typeof useSessionTodo> | undefined;
+
+ await act(async () => {
+ root.render(
+ createElement(Probe, {
+ services,
+ sessionId: 'session-1',
+ locale: 'zh-CN',
+ onSnapshot: (value) => {
+ snapshot = value;
+ },
+ }),
+ );
+ });
+
+ assert.equal(snapshot?.error, '请求超时');
+ });
+
+ it('falls back to the caller-supplied copy for an unclassified error in English', async () => {
+ const { root } = installReactRenderer();
+ const services = createHarness(async () => {
+ throw new Error('unexpected boom');
+ });
+ let snapshot: ReturnType<typeof useSessionTodo> | undefined;
+
+ await act(async () => {
+ root.render(
+ createElement(Probe, {
+ services,
+ sessionId: 'session-1',
+ locale: 'en',
+ onSnapshot: (value) => {
+ snapshot = value;
+ },
+ }),
+ );
+ });
+
+ assert.equal(snapshot?.error, 'load failed');
+ });
+});
diff --git a/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts b/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
index 076040c109..8d9166794e 100644
--- a/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
@@ -186,7 +186,7 @@ test('durable delegation renders terminal link state instead of stale execution
},
updatedAt: 10,
};
- const render = (locale: 'en' | 'zh') => renderToStaticMarkup(
+ const render = (locale: 'en' | 'zh-CN') => renderToStaticMarkup(
createElement(LocaleProvider, {
locale,
children: createElement(AstryxLocaleProvider, {
@@ -200,7 +200,7 @@ test('durable delegation renders terminal link state instead of stale execution
}),
);
const englishMarkup = render('en');
- const chineseMarkup = render('zh');
+ const chineseMarkup = render('zh-CN');
assert.match(englishMarkup, new RegExp(english, 'u'));
assert.doesNotMatch(englishMarkup, />Running</u);
@@ -229,7 +229,7 @@ test('durable creation explicitly announces the new work', () => {
},
updatedAt: 10,
};
- const render = (locale: 'en' | 'zh') => renderToStaticMarkup(
+ const render = (locale: 'en' | 'zh-CN') => renderToStaticMarkup(
createElement(LocaleProvider, {
locale,
children: createElement(AstryxLocaleProvider, {
@@ -244,7 +244,7 @@ test('durable creation explicitly announces the new work', () => {
);
assert.match(render('en'), /Created new work:/u);
- assert.match(render('zh'), /已创建新工作:/u);
+ assert.match(render('zh-CN'), /已创建新工作:/u);
});
test('surface projection refresh gate rejects older reads after a newer refresh starts', () => {
diff --git a/apps/desktop/src/main/browser-message-box.ts b/apps/desktop/src/main/browser-message-box.ts
index 755ecfb8d4..7ab4302cd7 100644
--- a/apps/desktop/src/main/browser-message-box.ts
+++ b/apps/desktop/src/main/browser-message-box.ts
@@ -266,7 +266,7 @@ function normalizeBrowserMessageBoxPresentation(
defaultId,
cancelId,
dark: appearance.dark,
- locale: appearance.locale === 'zh' ? 'zh' : 'en',
+ locale: appearance.locale,
palette: isThemePalette(appearance.palette) ? appearance.palette : 'default',
};
}
@@ -348,7 +348,7 @@ export function buildBrowserMessageBoxHtml(
function renderBrowserMessageBoxHtml(input: BrowserMessageBoxPresentation): string {
const nonce = randomUUID().replaceAll('-', '');
- const closeLabel = input.locale === 'zh' ? '关闭' : 'Close';
+ const closeLabel = input.locale === 'zh-CN' ? '关闭' : input.locale === 'zh-TW' ? '關閉' : 'Close';
const closeButton = `<button class="window-close" type="button" data-response="${input.cancelId}" aria-label="${closeLabel}">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>
</button>`;
@@ -386,7 +386,7 @@ function renderBrowserMessageBoxHtml(input: BrowserMessageBoxPresentation): stri
: '<path d="M12 8v5M12 17h.01" />';
return `<!doctype html>
-<html lang="${input.locale === 'zh' ? 'zh-CN' : 'en'}" data-theme="${input.dark ? 'dark' : 'light'}" data-maka-theme="${input.palette}" data-astryx-theme="maka" class="${input.dark ? 'dark' : ''}">
+<html lang="${input.locale}" data-theme="${input.dark ? 'dark' : 'light'}" data-maka-theme="${input.palette}" data-astryx-theme="maka" class="${input.dark ? 'dark' : ''}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
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<ClientSettingsChange['key'], readonly [string, string]> = {
- theme: ['Theme', '主题'],
- palette: ['Palette', '配色'],
- uiLocale: ['UI language', '界面语言'],
- runComplete: ['Run-complete notifications', '回答完成通知'],
- keepSystemAwake: ['Keep system awake', '保持系统唤醒'],
+ const labels: Record<ClientSettingsChange['key'], readonly [string, string, string]> = {
+ 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<StatusItemCopy> = {
- 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 13c6972208..a412a6c2a2 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 { UpdateAppSettingsInput } from '@maka/core/settings';
+import { normalizeUiLocalePreference } from '@maka/core/ui-locale';
import {
reconcileConnectionAfterEnabledModelsChange,
type LlmConnection,
@@ -123,8 +124,20 @@ export async function applyConfigImport(
let settingsCredentialSkips = 0;
if (bundle.data.settings && typeof bundle.data.settings === 'object') {
+ const patch = bundle.data.settings as unknown as UpdateAppSettingsInput;
+ const importedPersonalization = patch.personalization as
+ | (NonNullable<UpdateAppSettingsInput['personalization']> & { uiLocale?: unknown })
+ | undefined;
const applied = await deps.settingsStore.update(
- bundle.data.settings as unknown as UpdateAppSettingsInput,
+ importedPersonalization && Object.hasOwn(importedPersonalization, 'uiLocale')
+ ? {
+ ...patch,
+ personalization: {
+ ...importedPersonalization,
+ uiLocale: normalizeUiLocalePreference(importedPersonalization.uiLocale),
+ },
+ }
+ : patch,
);
settingsCredentialSkips = applied.skippedCredentials;
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 08dc5b226d..6c741c5248 100644
--- a/apps/desktop/src/main/e2e-fixture.ts
+++ b/apps/desktop/src/main/e2e-fixture.ts
@@ -137,7 +137,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/main.ts b/apps/desktop/src/main/main.ts
index 16133b4515..565583503f 100644
--- a/apps/desktop/src/main/main.ts
+++ b/apps/desktop/src/main/main.ts
@@ -105,18 +105,27 @@ if (!app.requestSingleInstanceLock()) {
.whenReady()
.then(() => {
const locale = resolveSystemUiLocale(app.getPreferredSystemLanguages());
- const isChinese = locale === 'zh';
+ const isSimplifiedChinese = locale === 'zh-CN';
+ const isTraditionalChinese = locale === 'zh-TW';
return showBrowserMessageBox(
{
type: 'warning',
- title: isChinese ? 'Maka Dev 已在运行' : 'Maka Dev is already running',
- message: isChinese
+ title: isSimplifiedChinese
+ ? 'Maka Dev 已在运行'
+ : isTraditionalChinese
+ ? 'Maka Dev 已在執行'
+ : 'Maka Dev is already running',
+ message: isSimplifiedChinese
? '另一个 Maka Dev 实例正在使用此开发配置。'
- : 'Another Maka Dev instance is using this development profile.',
- detail: isChinese
+ : isTraditionalChinese
+ ? '另一個 Maka Dev 執行個體正在使用此開發設定。'
+ : 'Another Maka Dev instance is using this development profile.',
+ detail: isSimplifiedChinese
? `开发配置:${profilePath}\n\n请先退出正在运行的实例,然后重试。`
- : `Development profile: ${profilePath}\n\nQuit the running instance, then retry.`,
- buttons: [isChinese ? '退出' : 'Exit'],
+ : isTraditionalChinese
+ ? `開發設定:${profilePath}\n\n請先退出正在執行的執行個體,然後重試。`
+ : `Development profile: ${profilePath}\n\nQuit the running instance, then retry.`,
+ buttons: [isSimplifiedChinese ? '退出' : isTraditionalChinese ? '退出' : 'Exit'],
defaultId: 0,
cancelId: 0,
},
diff --git a/apps/desktop/src/main/native-diagnostic-dialog-copy.ts b/apps/desktop/src/main/native-diagnostic-dialog-copy.ts
index c671249561..bd9ccee1eb 100644
--- a/apps/desktop/src/main/native-diagnostic-dialog-copy.ts
+++ b/apps/desktop/src/main/native-diagnostic-dialog-copy.ts
@@ -118,7 +118,7 @@ const COPY = {
exit: 'Exit',
},
},
- zh: {
+ 'zh-CN': {
dialog: {
copy: '复制诊断信息',
copyAgain: '再次复制',
@@ -167,6 +167,55 @@ const COPY = {
exit: '退出',
},
},
+ 'zh-TW': {
+ dialog: {
+ copy: '複製診斷資訊',
+ copyAgain: '再次複製',
+ copied: '診斷資訊已複製,可直接貼上到問題報告中。',
+ copyFailed: '無法複製診斷資訊。',
+ },
+ fatalStartup: {
+ title: 'Maka 啟動失敗',
+ message: 'Maka 無法完成啟動。',
+ detail: '啟動時發生未預期的錯誤。複製診斷資訊可檢視詳細資料。',
+ exit: '退出',
+ },
+ rendererGone: {
+ title: 'Maka 需要復原',
+ message: 'Maka 介面意外停止執行。',
+ detail: '只復原介面,不重新啟動 Maka。Runtime Host、正在執行的工作和背景服務都會保留。',
+ recover: '復原介面',
+ exit: '退出',
+ },
+ runtimeHostRecovery: {
+ title: 'Maka 需要修復 Runtime Host',
+ message: '管理此工作區的 Runtime Host 無法啟動。',
+ detail:
+ 'Maka 可以修復此 Desktop 選擇的受管理 Runtime Host。工作區、Host 身分、認證資料和設定都會保留。即使無法確認自動更新相容性,修復也可能使用此 Desktop 選擇的版本取代目前 Host。',
+ activeTasks: 'Host 可能仍有正在執行的任務。繼續會先中斷這些任務,再重新啟動 Host。',
+ repairFailed: '上一次修復未能完成。複製診斷資訊可檢視詳細資料。',
+ repair: '修復 Runtime Host',
+ repairAndRestart: '修復並重新啟動 Host',
+ exit: '退出',
+ },
+ defaultRuntimeHostRecovery: {
+ title: '預設 Runtime Host 無法連線',
+ connectFailed: (profileName) => `無法連線至 ${profileName}`,
+ detail:
+ '你可以重試、改用 Local 作為預設 Host,或保留目前選擇並稍後在設定中處理。複製診斷資訊可檢視連線失敗的詳細資料。',
+ retry: '重試',
+ useLocal: '改用 Local',
+ keepOffline: '保持離線',
+ },
+ storageRootRepair: {
+ title: 'Maka 工作區需要修復',
+ message: 'Maka 無法驗證這個工作區。',
+ detail: (workspaceRoot) =>
+ `系統中的磁碟識別資訊可能已變更。僅當這是本機原本的 Maka 工作區,而不是複製的工作區時,才選擇修復。\n\n${workspaceRoot}`,
+ repair: '修復工作區',
+ exit: '退出',
+ },
+ },
} satisfies UiCatalog<NativeDiagnosticDialogCopy>;
export function getNativeDiagnosticDialogCopy(locale: UiLocale): NativeDiagnosticDialogCopy {
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<Record<DragGrantPermissionId, PermissionOverlayCopy>>;
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<StartupRecoveryCopy>;
+
+export function getStartupRecoveryCopy(locale: UiLocale): StartupRecoveryCopy {
+ return STARTUP_RECOVERY_COPY[locale];
+}
diff --git a/apps/desktop/src/main/runtime-host-quit-copy.ts b/apps/desktop/src/main/runtime-host-quit-copy.ts
index 26db6a37e4..e53cf84b89 100644
--- a/apps/desktop/src/main/runtime-host-quit-copy.ts
+++ b/apps/desktop/src/main/runtime-host-quit-copy.ts
@@ -109,7 +109,7 @@ const COPY = {
retry: 'Retry Quit',
forceQuit: 'Force Quit Maka',
},
- zh: {
+ 'zh-CN': {
activeTitle: 'Maka 正在后台工作',
activeMessage: '仍有后台工作正在运行。',
activeDetail: '现在退出会停止 Runtime Host,并可能中断正在执行或等待运行的后台任务。',
@@ -125,4 +125,20 @@ const COPY = {
retry: '重试退出',
forceQuit: '强制退出 Maka',
},
+ 'zh-TW': {
+ activeTitle: 'Maka 正在背景工作',
+ activeMessage: '仍有背景工作正在執行。',
+ activeDetail: '現在退出會停止 Runtime Host,並可能中斷正在執行或等待執行的背景工作。',
+ stopAndQuit: '停止工作並退出',
+ keepRunning: '繼續執行 Maka',
+ title: '無法安全退出 Maka',
+ message: '本地 Runtime Host 未能安全停止,Maka 仍在執行。',
+ detail: '退出已取消。請重試;如果問題持續存在,請檢視診斷資訊。',
+ process: (pid: number) => `Runtime Host 程序 PID:${pid}`,
+ manual: '如果重試仍然失敗,請先確認沒有需要保留的執行,再透過作業系統的程序管理工具停止該 PID。',
+ forceWarning: '強制退出可能丟棄尚未完成的外部工作。',
+ cause: '原因',
+ retry: '重試退出',
+ forceQuit: '強制退出 Maka',
+ },
} 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 53fd212d68..3f03574171 100644
--- a/apps/desktop/src/main/runtime-host-upgrade-copy.ts
+++ b/apps/desktop/src/main/runtime-host-upgrade-copy.ts
@@ -160,7 +160,7 @@ const UPGRADE_COPY = {
other: 'Other background activity',
},
},
- zh: {
+ 'zh-CN': {
title: '旧版 Runtime Host 正在运行',
message: '另一个 Runtime Host 进程仍占用此工作区。',
restart: '重启 Runtime Host',
@@ -184,4 +184,27 @@ const UPGRADE_COPY = {
resource: 'Runtime 资源', graph: 'Agent Graph', other: '其他后台活动',
},
},
+ 'zh-TW': {
+ title: '舊版 Runtime Host 正在執行',
+ message: '另一個 Runtime Host 程序仍佔用此工作區。',
+ restart: '重啟 Runtime Host',
+ replace: '停止 Host 並繼續',
+ wait: '等待',
+ cancel: '取消啟動',
+ uptime: (n: number) => `已執行約 ${n} 分鐘`,
+ connections: (n: number) => `仍有 ${n} 個其他客戶端連線`,
+ operations: (n: number) => `有 ${n} 個操作正在執行`,
+ idleNotVerified: '安全替換檢查無法確認此 Host 是否處於閒置狀態。',
+ unknownActivity: '此 Host 版本無法報告後臺活動。',
+ processId: (pid: number) => `程序 ID (PID):${pid}`,
+ restartWarning: '重啟會保留持久化狀態,但可能中斷正在進行的外部工作。',
+ replaceWarning: '停止 Host 會保留持久化狀態,但可能中斷正在進行的外部工作。',
+ replaceExplanation: 'Maka 將停止並安全替換此 Host,然後繼續啟動。',
+ exitOwner: (pid: number) => `請使用系統程序管理工具結束程序 ${pid},以便安全替換此 Host。`,
+ waitExplanation: '若選擇等待,目前 Host 退出後 Maka 將自動繼續。',
+ activity: {
+ goal: '目標', scheduledTask: '計劃任務', dailyReview: '每日回顧', execution: '活動執行',
+ resource: 'Runtime 資源', graph: 'Agent Graph', other: '其他後臺活動',
+ },
+ },
} as const;
diff --git a/apps/desktop/src/preload/runtime-host-session-catalog.ts b/apps/desktop/src/preload/runtime-host-session-catalog.ts
index 08b506ea97..532c771192 100644
--- a/apps/desktop/src/preload/runtime-host-session-catalog.ts
+++ b/apps/desktop/src/preload/runtime-host-session-catalog.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import type { DesktopSessionSummary } from './bridge-contract.js';
+import type { DesktopSessionSummary } from '../shared/desktop-session-projection.js';
export interface RuntimeHostSessionCatalogRequest {
readonly hostId: string;
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-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 00d596e466..42afdd3708 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 8ecc4e7f93..040e9f4a25 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -2866,7 +2866,7 @@ function AppShellContent({
{workHubEnabled && navSelection.section === 'sessions' && activeId ? (
<Button
className="workhub-return"
- label={uiLocale === 'zh' ? '返回 WorkHub' : 'Return to WorkHub'}
+ label={uiLocale !== 'en' ? '返回 WorkHub' : 'Return to WorkHub'}
variant="secondary"
size="sm"
onClick={openWorkHub}
diff --git a/apps/desktop/src/renderer/attachment-preflight.ts b/apps/desktop/src/renderer/attachment-preflight.ts
index 0af114c7a9..b88cfd6ba0 100644
--- a/apps/desktop/src/renderer/attachment-preflight.ts
+++ b/apps/desktop/src/renderer/attachment-preflight.ts
@@ -38,7 +38,7 @@ type PreflightItem = {
* File blobs are sized by the browser File object; approval-token attachments
* are sized by the pending size stamped at pick time (main re-stats).
*/
-export function preflightAttachmentItems(items: readonly PreflightItem[], locale: UiLocale = 'zh'): void {
+export function preflightAttachmentItems(items: readonly PreflightItem[], locale: UiLocale = 'zh-CN'): void {
const copy = getDesktopConversationCopy(locale).attachments;
if (items.length > MAX_ATTACHMENT_COUNT) throw new Error(copy.tooMany);
const seen = new Set<string>();
diff --git a/apps/desktop/src/renderer/conversation-markdown.ts b/apps/desktop/src/renderer/conversation-markdown.ts
index 526f41d701..0c808af68a 100644
--- a/apps/desktop/src/renderer/conversation-markdown.ts
+++ b/apps/desktop/src/renderer/conversation-markdown.ts
@@ -43,7 +43,7 @@ import { getShellRemainingCopy } from './locales/shell-remaining-copy.js';
* export that the user is going to paste somewhere public.
* - **user text** left untouched (the user typed it, they own it).
*/
-export function renderConversationMarkdown(sessionName: string, messages: StoredMessage[], locale: UiLocale = 'zh'): string {
+export function renderConversationMarkdown(sessionName: string, messages: StoredMessage[], locale: UiLocale = 'zh-CN'): string {
const copy = getShellRemainingCopy(locale).conversationExport;
const lines: string[] = [];
lines.push(`# ${sessionName}`);
diff --git a/apps/desktop/src/renderer/daily-review-actions.ts b/apps/desktop/src/renderer/daily-review-actions.ts
index 02bdc38119..ae3f20f764 100644
--- a/apps/desktop/src/renderer/daily-review-actions.ts
+++ b/apps/desktop/src/renderer/daily-review-actions.ts
@@ -21,7 +21,7 @@ import type { DailyReviewArchive } from '@maka/core/daily-review';
import type { UiLocale } from '@maka/core/ui-locale';
import { dailyReviewArchiveId } from '@maka/core/daily-review';
-import { generalizedErrorMessage, generalizedErrorMessageChinese } from '@maka/core/redaction';
+import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
export function dailyReviewExportDefaultName(
input: Pick<DailyReviewArchive, 'day' | 'range'>,
@@ -30,5 +30,5 @@ export function dailyReviewExportDefaultName(
}
export function dailyReviewActionErrorMessage(error: unknown, fallback: string, locale: UiLocale): string {
- return locale === 'zh' ? generalizedErrorMessageChinese(error, fallback) : generalizedErrorMessage(error, fallback);
+ return generalizedErrorMessageForLocale(error, fallback, locale);
}
diff --git a/apps/desktop/src/renderer/derive-turn-lineage-badges.ts b/apps/desktop/src/renderer/derive-turn-lineage-badges.ts
index f6aa1d8373..5dd2c5791b 100644
--- a/apps/desktop/src/renderer/derive-turn-lineage-badges.ts
+++ b/apps/desktop/src/renderer/derive-turn-lineage-badges.ts
@@ -50,7 +50,7 @@ export interface TurnLineageBadgeInput {
}
export function deriveTurnLineageBadges(input: TurnLineageBadgeInput): TurnLineageBadge[] {
- const copy = getDesktopConversationCopy(input.locale ?? 'zh').lineage;
+ const copy = getDesktopConversationCopy(input.locale ?? 'zh-CN').lineage;
const badges: TurnLineageBadge[] = [];
const forwardFrom = input.regeneratedFromTurnId ?? input.retriedFromTurnId;
diff --git a/apps/desktop/src/renderer/features/connection-settings/index.ts b/apps/desktop/src/renderer/features/connection-settings/index.ts
index d9c5757d2d..cf1c5ddb5f 100644
--- a/apps/desktop/src/renderer/features/connection-settings/index.ts
+++ b/apps/desktop/src/renderer/features/connection-settings/index.ts
@@ -38,6 +38,7 @@ export {
} from './provider-panel-shared.js';
export { OnboardingStepForm } from './onboarding-step-form.js';
export { getProviderSettingsCopy } from './settings-provider-copy.js';
+export { subscriptionResultMessage } from './subscription-result-message.js';
export type { ProviderSettingsCopy } from './settings-provider-copy.js';
export type {
CredentialPresenceStatus,
diff --git a/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts b/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts
index 0a470b680d..b30f2d1a6f 100644
--- a/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts
+++ b/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { generalizedErrorMessage, generalizedErrorMessageChinese, redactSecrets } from '@maka/core/redaction';
+import { generalizedErrorMessageForLocale, redactSecrets } from '@maka/core/redaction';
import { type ConnectionTestResult } from '@maka/core/llm-connections';
import { type UiLocale } from '@maka/core/ui-locale';
import { getProviderSettingsCopy } from './settings-provider-copy.js';
@@ -25,7 +25,7 @@ import { cleanErrorMessage } from '../../application/contracts/connection-error-
export type CredentialPresenceStatus = boolean | 'loading' | 'error';
-export function providerPanelActionErrorMessage(error: unknown, locale: UiLocale = 'zh'): string {
+export function providerPanelActionErrorMessage(error: unknown, locale: UiLocale = 'zh-CN'): string {
const shared = getProviderSettingsCopy(locale).shared;
// Electron wraps ipcMain.handle rejections as "Error invoking remote method
// '<channel>': Error: <message>". Classify the original message, not the
@@ -36,15 +36,13 @@ export function providerPanelActionErrorMessage(error: unknown, locale: UiLocale
if (known) return known;
// Main-process handlers throw display-ready Chinese copy; keep it instead
// of flattening it into a coarser classification or the generic fallback.
- if (/[\u3400-\u9fff]/.test(cleaned)) return cleaned;
+ if (locale === 'zh-CN' && /[\u3400-\u9fff]/.test(cleaned)) return cleaned;
if (/connection_stale|Unable to delete Connection: connection_stale/i.test(cleaned)) {
- return locale === 'zh'
- ? '连接状态已更新,请刷新列表后再删除。'
- : 'The connection changed while deleting. Refresh the list and try again.';
+ if (locale === 'zh-CN') return '连接状态已更新,请刷新列表后再删除。';
+ if (locale === 'zh-TW') return '連線狀態已更新,請重新整理清單後再刪除。';
+ return 'The connection changed while deleting. Refresh the list and try again.';
}
- const classified = locale === 'zh'
- ? generalizedErrorMessageChinese(new Error(cleaned), '')
- : generalizedErrorMessage(new Error(cleaned), '');
+ const classified = generalizedErrorMessageForLocale(new Error(cleaned), '', locale);
return classified || shared.actionFallback;
}
@@ -61,7 +59,7 @@ export interface ConnectionTestTroubleshootingCopy {
export function connectionTestFailureFallback(
result: ConnectionTestResult,
copy: ConnectionTestTroubleshootingCopy,
- locale: UiLocale = 'zh',
+ locale: UiLocale = 'zh-CN',
): string {
const shared = getProviderSettingsCopy(locale).shared;
if (result.statusCode === 429) return shared.rateLimit;
@@ -79,16 +77,14 @@ export function connectionTestFailureFallback(
export function connectionTestFailureMessage(
result: ConnectionTestResult,
copy: ConnectionTestTroubleshootingCopy,
- locale: UiLocale = 'zh',
+ locale: UiLocale = 'zh-CN',
): string {
const fallback = connectionTestFailureFallback(result, copy, locale);
if (!result.errorMessage) return fallback;
- return locale === 'zh'
- ? generalizedErrorMessageChinese(new Error(result.errorMessage), fallback)
- : generalizedErrorMessage(new Error(result.errorMessage), fallback);
+ return generalizedErrorMessageForLocale(new Error(result.errorMessage), fallback, locale);
}
-export function connectionLastTestMessageDisplay(message: string | undefined, locale: UiLocale = 'zh'): string | undefined {
+export function connectionLastTestMessageDisplay(message: string | undefined, locale: UiLocale = 'zh-CN'): string | undefined {
if (!message) return undefined;
const trimmed = message.trim();
if (!trimmed) return undefined;
@@ -96,8 +92,6 @@ export function connectionLastTestMessageDisplay(message: string | undefined, lo
const copy = getProviderSettingsCopy(locale).shared;
const known = (copy.lastTest as Readonly<Record<string, string>>)[normalized];
if (known) return known;
- const classified = locale === 'zh'
- ? generalizedErrorMessageChinese(new Error(trimmed), '')
- : generalizedErrorMessage(new Error(trimmed), '');
+ const classified = generalizedErrorMessageForLocale(new Error(trimmed), '', locale);
return classified || copy.statusUnavailable;
}
diff --git a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts
index 28cfe0f7a9..da6a223f72 100644
--- a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts
+++ b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts
@@ -51,6 +51,30 @@ const zhCapabilitiesCopy = {
fastAuto: '自动',
fastEnabled: 'Fast',
};
+
+const zhTwCapabilitiesCopy = {
+ capabilities: '能力',
+ thinkingEffort: '思考檔位(reasoning_effort)',
+ thinkingEffortHelp: '勾選需要的思考強度檔位,不勾選即為不宣告。',
+ thinkingUndeclared: '未宣告',
+ thinkingSelectedCount: (count: number) => `已選擇 ${count} 個`,
+ thinkingBulk: '批次設定思考檔位',
+ thinkingBulkCoverage: (declared: number, total: number) =>
+ declared === 0 ? '全部未宣告' : `${declared}/${total} 個模型`,
+ visionInput: '視覺輸入(vision)',
+ visionInputHelp: '「自動」跟隨內建後設資料;「啟用/停用」是顯式宣告,覆蓋自動判斷。',
+ visionAuto: '自動',
+ visionEnabledOption: '啟用',
+ visionDisabledOption: '停用',
+ contextWindow: '上下文視窗(tokens)',
+ contextWindowHelp: '聲明後壓縮與預算按此值計算;留空跟隨內建後設資料。',
+ contextWindowHint: (tokens: number) => `該模型宣告的視窗為 ${tokens} tokens`,
+ contextWindowApplyHint: '填入',
+ fastMode: 'Fast 模式',
+ fastModeHelp: '使用 OpenAI 的 fast service tier;留空跟隨服務商預設值。',
+ fastAuto: '自動',
+ fastEnabled: 'Fast',
+};
const enCapabilitiesCopy = {
capabilities: 'Capabilities',
thinkingEffort: 'Thinking levels (reasoning_effort)',
@@ -245,6 +269,175 @@ const zhCopy = {
storageFailed: (name: string) => `${name} 本地凭据读取失败,请重新登录。`, providerUnavailable: (name: string) => `${name} 已登录,但当前 provider 状态不可用。`,
},
} as const;
+const zhTwCopy = {
+ detail: {
+ deviceCode: '登入碼:',
+ delete: '刪除', cancel: '取消', deleteUnused: '不再需要,刪除連線',
+ deleteFailed: '刪除模型連線失敗', credentialReadFailed: '讀取模型憑據狀態失敗', refreshFailed: '重新整理模型連線失敗',
+ saveFailed: '儲存模型連線失敗', saveModelsFailed: '儲存啟用模型失敗',
+ modelKey: '模型金鑰', pasteModelKey: '貼上模型金鑰', getModelKey: '取得模型金鑰', saving: '儲存中…',
+ advancedRequest: '高階請求設定', advancedRequestHelp: '為這個連線的 HTTP 請求新增請求頭和額外 JSON 請求體。請求頭值作為憑據儲存在本機。',
+ requestHeaders: '自訂請求頭', headerName: '請求頭名稱', headerValue: '請求頭值', retainedHeaderValue: '保留已儲存的值', addHeader: '新增請求頭', removeHeader: '移除', noRequestHeaders: '未設定自訂請求頭。',
+ extraRequestBody: '額外請求體(JSON)', extraRequestBodyHelp: '僅新增頂層欄位;若與 Maka 生成的欄位重名,請求會明確失敗。', requestCustomizationInvalid: '請求設定無效', requestHeadersInvalidDetail: '請檢查請求頭名稱和值。', requestBodyInvalidDetail: '請輸入符合要求的 JSON 物件。', saveAdvancedRequest: '儲存高階請求設定', configuredHeaders: (count: number) => `${count} 個請求頭`, noAdvancedRequest: '未設定',
+ oauthLoggedIn: 'OAuth 已登入', oauthLoading: 'OAuth 狀態讀取中', oauthUnknown: 'OAuth 狀態未知', oauthWaiting: '等待 OAuth 登入',
+ oauthLoggedInDetail: '該模型連線使用主程序儲存的 OAuth access token;若請求提示需要重新登入,請到帳號連線重新授權。',
+ oauthLoadingDetail: '正在讀取本機 OAuth 登入狀態,讀取完成前不會把未知狀態顯示成未登入。',
+ oauthUnknownDetail: '暫時無法讀取本機 OAuth 登入狀態;請重新整理頁面或重新開啟設定。',
+ oauthWaitingDetail: '請到帳號連線完成登入;登入成功後會自動出現在模型連線裡。',
+ oauthRetired: '此登入方式已停用',
+ oauthRetiredDetail:
+ '這條連線使用的登入方式已從 Maka 移除,無法再登入,也無法用於對話。改用 Anthropic API Key 連線即可繼續使用 Claude 模型;刪除這條連線會一併清除本機儲存的登入憑據。',
+ credentialLoadingDetail: '正在讀取模型憑據狀態,讀取完成前暫不測試連線或重新整理模型。',
+ credentialUnknownDetail: '模型憑據狀態暫時沒重新整理成功,已避免把未知狀態顯示成未登入或未設定。',
+ testConnection: '測試連線',
+ updateModels: '更新模型目錄', endpoint: '服務地址',
+ connectionName: '名稱',
+ connectionNamePlaceholder: '給這個連線起個名字',
+ addModel: '新增模型',
+ addModelConfirm: '新增',
+ addModelIdField: '模型 ID',
+ addModelIdFieldHelp: '需與服務商完全一致,區分大小寫。',
+ addModelIdPlaceholder: 'deepseek-v4-pro-beta',
+ addModelIdRequired: '請填寫模型 ID。',
+ addModelIdDuplicate: '該模型已在列表中。',
+ addModelContextWindow: '上下文視窗',
+ addModelContextWindowHelp: '服務商模型頁給出的最大 token 數。缺少它 Maka 只能按 32k 處理,長對話會被提前截斷。',
+ addModelContextWindowRequired: '請填寫上下文視窗。',
+ credentials: '連線', dangerZone: '刪除連線', deleteRowHelp: '此操作不可撤銷。',
+ credentialsHelp: '金鑰只儲存在本機。',
+ credentialsHelpAccount: '登入權杖只儲存在本機。',
+ modelManagementHelp: '這些模型會出現在任務的模型選擇器裡。',
+ ...zhTwCapabilitiesCopy,
+ capabilitiesHelp: '宣告每個已啟用模型的思考檔位、視覺與上下文視窗;儲存後生效。',
+ // Row affordances (settings-sidebar 的 InfoRow / ExpandableRow 語言):一行
+ // 只報狀態,改的時候才展開成輸入框。
+ change: '更換', set: '設定', edit: '編輯', save: '儲存',
+ endpointManaged: '由帳號登入或服務商管理',
+ endpointMissing: '尚未設定服務地址',
+ endpointModelOverridesNote: '部分模型會透過其他服務端點發送請求',
+ endpointCredentialsMasked: '已儲存的地址內嵌憑據,編輯時預設隱藏',
+ modelManagement: '模型',
+ disconnectAndDelete: '退出帳號並刪除',
+ login: '登入', loggingIn: '登入中…', relogin: '重新登入',
+ oauthReloginDetail: '若請求提示需要重新登入,點這裡重新走一遍授權。',
+ oauthStartDetail: '點下方按鈕開啟瀏覽器完成登入,授權成功後會自動重新整理這裡的狀態。',
+ status: '連線狀態', statusHealthy: '正常', statusUntested: '未測試',
+ modelsSummary: (enabled: number, total: number) => `已啟用 ${enabled} / ${total}`,
+ filterModels: '搜尋模型', noModelsMatch: '找不到符合的模型',
+ enableModelAria: (name: string) => `啟用模型 ${name}`,
+ declareCapabilities: '設定參數', declareCapabilitiesAria: (name: string) => `設定模型參數:${name}`,
+ modelUndescribed: '缺少該模型的參數資訊,請手動設定。',
+ visionToken: '视觉', thinkingToken: '思考', contextToken: (value: string) => `${value} 上下文`,
+ noModels: '暫無可選模型,請先更新模型目錄。',
+ keySet: '已設定', statusLoading: '正在讀取狀態', credentialUnknown: '憑據狀態未知', keyMissing: '尚未設定金鑰',
+ keyTroubleshooting: '模型金鑰 / 服務地址 / 代理設定', endpointTroubleshooting: '本地服務 / 服務地址 / 代理設定', oauthTroubleshooting: 'OAuth 登入 / 代理設定',
+ unknownDescription: (provider: string) => `該連線使用的 provider「${provider}」在目前版本未註冊。設定和憑據會保留,切回支援它的版本即可繼續使用。`,
+ deleteProviderTitle: (name: string) => `刪除供應商 ${name}?`,
+ deleteConnectionTitle: (name: string) => `刪除模型連線 ${name}?`,
+ deleteUnknownDescription: '刪除後,支援該 provider 的其他版本也無法恢復這條連線及其憑據。',
+ deleteDescription: (isDefault: boolean, disconnectsAccount: boolean) => [
+ disconnectsAccount
+ ? '這會退出本機帳號、刪除 OAuth 憑據和模型連線;重新整理後不會自動重建立立。'
+ : '這會刪除模型連線及其本機憑據;如需再次使用,需要重新新增。',
+ isDefault
+ ? '它目前是預設連線;刪除後預設模型會變成未設定,已有任務可能需要重新選擇模型。'
+ : '',
+ ].filter(Boolean).join(' '),
+ connectionSuccess: (name: string) => `連線成功 · ${name}`, connectionFailed: (name: string) => `連線失敗 · ${name}`,
+ connectionFallbackTitle: (name: string) => `連線可用 · ${name}`,
+ connectionFallbackDetail: (selected: readonly string[], tested: string) =>
+ `你選擇的 ${selected.join('、')} 目前沒有回應,已改用 ${tested} 驗證連線可用;任務中繼續使用你選擇的模型可能會失敗。`,
+ connectionTestError: (name: string) => `連線測試出錯 · ${name}`, modelsFetched: (count: number, name: string) => `已拉取 ${count} 個模型 · ${name}`,
+ modelsFetchFailed: (name: string) => `拉取模型失敗 · ${name}`,
+ modelsFetchFailedDetail: (message: string, troubleshooting: string) => `${message} · 目前繼續顯示靜態列表,請確認 ${troubleshooting} 後重試。`,
+ authTroubleshooting: (value: string) => `鑑權失敗,請確認 ${value} 後重試。`, recheckTroubleshooting: (value: string) => `檢查 ${value} 後重試。`,
+ modelKeyAria: (name: string) => `${name} 模型金鑰`,
+ },
+ shared: {
+ actionFallback: '模型連線服務暫時不可用,請稍後重試。', rateLimit: '目前帳號或模型服務觸發速率限制,請稍後重試。',
+ timeout: '請求超時,請檢查網路或代理後重試。', unavailable: '模型服務暫時不可用,請稍後重試。',
+ network: '網路錯誤,請檢查服務地址或代理設定後重試。', statusUnavailable: '連線測試狀態暫時無法顯示,請重新測試。',
+ filterMatches: (count: number) => (count === 0 ? '沒有符合的結果' : `${count} 個符合結果`),
+ connectionStatuses: { retired: '已停用 · 請刪除', reauth: '需要重新登入', disabledFailed: '暫不可用 · 上次連線失敗', disabled: '暫不可用', failed: '上次連線失敗' },
+ lastTest: {
+ '连接已验证': '連線已驗證', '鉴权失败': '鑑權失敗', '请求超时': '請求超時', '网络错误': '網路錯誤', '模型服务返回错误': '模型服務回傳錯誤', '连接测试失败': '連線測試失敗',
+ 'connection verified': '連線已驗證', 'authentication failed': '鑑權失敗', 'request timed out': '請求超時', 'network error': '網路錯誤', 'provider returned an error': '模型服務回傳錯誤', 'connection test failed': '連線測試失敗',
+ 'claude oauth 未登录。': 'Claude OAuth 未登入。', 'claude oauth 本地凭据读取失败。': 'Claude OAuth 本地憑據讀取失敗。', 'claude oauth 需要重新登录。': 'Claude OAuth 需要重新登入。', 'claude oauth 已登录。': 'Claude OAuth 已登入。', 'claude oauth 已退出登录。': 'Claude OAuth 已退出登入。',
+ 'codex oauth 未登录。': 'Codex OAuth 未登入。', 'codex oauth 本地凭据读取失败。': 'Codex OAuth 本地憑據讀取失敗。', 'codex oauth 需要重新登录。': 'Codex OAuth 需要重新登入。', 'codex oauth 已登录。': 'Codex OAuth 已登入。', 'codex oauth 已退出登录。': 'Codex OAuth 已退出登入。',
+ '当前账号无可用 codex 模型。': '目前帳號無可用 Codex 模型。', 'codex 模型列表获取失败。': 'Codex 模型列表取得失敗。',
+ 'github copilot 需要重新导入 github cli 登录。': 'GitHub Copilot 需要重新匯入 GitHub CLI 登入。', 'github copilot 无法读取当前账号可用模型,请重新验证登录。': 'GitHub Copilot 無法讀取目前帳號可用模型,請重新驗證登入。', 'github copilot 登录已导入。': 'GitHub Copilot 登入已匯入。', 'github copilot 连接未能保存,请重新导入登录。': 'GitHub Copilot 連線未能儲存,請重新匯入登入。', 'github copilot 已移除本地登录。': 'GitHub Copilot 已移除本地登入。',
+ },
+ },
+ panel: {
+ groups: { recommended: '推薦', plans: '訂閱計劃', api: 'API', aggregators: '聚合服務', local: '本地' },
+ loadFailed: '載入模型連線失敗', loadingAria: '正在載入模型供應商', connections: '模型連線',
+ connectionsHelp: '已啟用的模型會顯示在任務的模型選擇器中。',
+ retry: '點選重試。', empty: '還沒有模型連線', connectionRemoved: '原連線已刪除或移除,已返回模型連線列表。',
+ connectedLoading: '帳號已連線,正在載入新的模型連線…', connectedLoadFailed: '帳號已連線,但暫時無法載入新的模型連線。',
+ connectionIdentityChanged: '新連線的身分與登入結果不一致,請返回連線列表後重試。',
+ emptyHelp: '從下方的常用服務商開始,或瀏覽全部服務商。', recommended: '推薦服務商', browseAll: '查看全部服務商',
+ default: '預設', setDefault: '設為預設', setDefaultTitle: '讓新任務預設使用這個連線', setDefaultPending: '設定中…', setDefaultFailed: '設為預設失敗', addHelp: '選擇帳號登入、模型計劃、API、聚合服務或本地執行時。',
+ searchPlaceholder: '搜尋服務商', searchAria: '搜尋模型服務商', noMatch: '沒有符合的服務商', clearSearch: '清除搜尋',
+ createSubtitle: '完成必要設定後,連線會出現在模型頁上方。', connection: '模型連線',
+ count: (value: number) => `· ${value}`, modelCount: (value: number) => `${value} 個模型`, connectTitle: (name: string) => `連線 ${name}`,
+ chipAria: (name: string, provider: string, isDefault: boolean, status?: string) => `模型連線:${name},供應商:${provider}${isDefault ? ',預設連線' : ''}${status ? `,${status}` : ''}`,
+ addConnection: '新增連線', backToList: '返回模型連線', backToCatalog: '返回服務商列表',
+ },
+ catalog: {
+ unavailable: '未開放',
+ cardAria: (name: string, description: string) => `新增模型供應商:${name},${description}`,
+ },
+ add: {
+ invalidSlug: '連線標識格式不正確', duplicateSlug: '連線標識已存在', cloudflareAccount: '請填寫 Cloudflare Account ID', endpointRequired: '這個供應商需要填寫服務地址',
+ accountLogin: '請到帳號連線完成登入;登入成功後會自動建立模型連線。',
+ apiKeyPlaceholder: '輸入或貼上 API Key', cancel: '取消', accountTitle: '使用帳號連線登入',
+ advancedRequest: '高階請求設定', expandAdvancedRequest: '展開高階請求設定', collapseAdvancedRequest: '收起高階請求設定',
+ requestHeaders: '自訂請求頭', headerName: '請求頭名稱', headerValue: '請求頭值', retainedHeaderValue: '保留已儲存的值', addHeader: '新增請求頭', removeHeader: '移除', noRequestHeaders: '未設定自訂請求頭。',
+ extraRequestBody: '額外請求體(JSON)', extraRequestBodyHelp: '僅新增頂層欄位;與 Maka 生成欄位重名時會明確失敗。請求頭值將作為憑據儲存在本機。', requestCustomizationInvalid: '請檢查高階請求設定。',
+ accountDetail: '不要在這裡手動新增;請回到模型連線頁的帳號連線完成登入,Maka 會自動建立並重新整理模型連線。',
+ slug: '連線標識', name: '顯示名稱',
+ accountIdPlaceholder: '填寫帳號 ID',
+ saving: '儲存中…', save: '儲存供應商', keyRequired: (name: string) => `請填寫 ${name} API Key`,
+ apiKeyLabel: 'API Key', accountIdLabel: 'Cloudflare Account ID', endpointLabel: '服務地址',
+ defaultModel: '預設模型', defaultModelPlaceholder: '留空即可,儲存後自動拉取', defaultModelHelp: '儲存後 Maka 會向該端點拉取模型目錄。只有當端點不提供目錄時,才需要在這裡手填一個模型 ID。',
+ stepsAria: '新增連線步驟', stepCredentials: '金鑰', stepModels: '選擇模型',
+ onboardingVerifyAndChoose: '驗證並選擇模型', onboardingVerifying: '正在驗證金鑰並取得模型…',
+ onboardingChooseModels: '選擇此連線使用的模型', onboardingChooseModelsHelp: '新增後仍可在連線詳細資料中啟用其他模型。', onboardingEnabledModels: '啟用的模型', onboardingSearchModels: '搜尋模型', onboardingAddConnection: '新增連線', onboardingBack: '返回修改',
+ onboardingSelectedCount: (selected: number, total: number) => `已選 ${selected} / ${total}`, onboardingSelectAll: '全選', onboardingClearAll: '取消全選', onboardingNoModelsMatch: '找不到符合的模型',
+ onboardingDefaultModel: '預設模型', onboardingDefaultModelHelp: '新任務將預設使用此模型,僅可在已勾選的模型中選擇。',
+ onboardingAuthFailed: '金鑰驗證失敗,請檢查後重試。', onboardingTimeout: '驗證逾時,請檢查網路或 Proxy 後重試。', onboardingNetwork: '無法連線至模型服務,請檢查網路、Proxy 或服務位址。', onboardingUnavailable: '模型連線服務暫時無法使用,請稍後重試。', onboardingInvalidResponse: '模型服務回傳無法辨識的結果,請稍後重試。', onboardingCatalogFull: '連線數量已達上限,請先刪除不再使用的連線。', onboardingModelsChanged: '可用模型已變更,請重新驗證後選擇。', onboardingNoModels: '未發現可用模型,目前未建立連線。', onboardingSelectModel: '請至少啟用一個模型。',
+ onboardingOutcomeUnknown: '暫時無法確認儲存結果', onboardingOutcomeUnknownDetail: '請勿再次新增,以免建立重複連線。重新載入連線列表並檢查該連線是否已出現;仍不確定時請先重新連線 Runtime Host。', onboardingReloadConnections: '重新載入連線列表', onboardingRestart: '仍要新增另一個連線',
+ ...zhTwCapabilitiesCopy,
+ },
+ oauthFlow: {
+ refreshFailed: '重新整理登入狀態失敗', accountActionFailed: (name: string) => `${name} 帳號操作失敗`, loginFailedRetry: '登入失敗,請稍後重試。',
+ retry: '請稍後再試。', startFailed: '無法開始登入', startFailedRetry: '無法開始登入,請稍後再試。', openFailed: '無法開啟瀏覽器',
+ openFailedRetry: '無法開啟瀏覽器,請稍後重試。', loginSuccess: '登入成功', bound: (name: string) => `${name} 已繫結本機。`,
+ incomplete: '登入未完成', incompleteRetry: '登入未完成,請重新開啟瀏覽器授權。', loginFailed: '登入失敗',
+ logoutDescription: '將刪除本機儲存的訂閱憑據,之後需要重新登入才能繼續使用這些 OAuth 模型。', logout: '退出登入', cancel: '取消',
+ loggedOut: '已退出登入', credentialsCleared: '本地憑據已清除。', logoutFailed: '退出失敗', logoutFailedRetry: '退出登入失敗,請稍後重試。',
+ serviceUnavailable: '登入服務暫時不可用,請檢查網路後重試。',
+ logoutTitle: (name: string) => `退出 ${name} 登入?`,
+ },
+ oauthSection: {
+ signedIn: '已登入', codexDescription: 'ChatGPT Plus / Pro 訂閱帳號登入。', xaiDescription: 'SuperGrok / X Premium 帳號登入。',
+ configuredConnections: (count: number) => `已有 ${count} 個連線 · 新增另一個帳號`,
+ copilotDescription: '匯入相容 GitHub 憑據連線 Copilot 訂閱。', serviceUnavailable: '登入服務暫時不可用,請檢查網路後重試。',
+ aria: 'OAuth 登入',
+ staleState: 'OAuth 登入狀態暫時沒重新整理成功,已保留上一次狀態。',
+ codexDetail: '點選下方按鈕開啟裝置授權頁,並在頁面中輸入這裡顯示的登入碼。', xaiDetail: '點選下方按鈕開啟瀏覽器登入,授權完成後會自動回寫。', deviceCode: '登入碼:',
+ openingBrowser: '開啟瀏覽器…', waitingAuthorization: '等待瀏覽器授權…', logout: '退出登入', loggingOut: '退出中…',
+ copilotSubtitle: '匯入相容的 GitHub 登入;token 不會暴露給渲染程序。', copilotImported: '已匯入 GitHub Copilot 訂閱帳號。',
+ copilotSetup: '請設定具有 Copilot Requests 權限的 fine-grained PAT;普通 gh auth login 可能不包含該權限。', importing: '匯入中…',
+ copilotSignIn: '使用 GitHub 登入', copilotActionFailed: 'GitHub Copilot 帳號操作失敗',
+ copilotSignInDisabledHint: '本機未啟用 GitHub 登入;請改用匯入相容憑據,或由管理員啟用後重試。',
+ importCredential: '匯入相容憑據',
+ loadingAccount: '正在載入帳號狀態…', authorizing: '請在彈出的瀏覽器視窗完成登入。', refreshing: '正在重新整理存取權杖…', refreshTokenFailed: '權杖重新整理失敗,請重新登入。',
+ cardAria: (intent: 'add' | 'import' | 'manage', name: string, status: string | undefined, description: string) => `${intent === 'add' ? '新增' : intent === 'import' ? '匯入' : '管理'}帳號連線:${name}${status ? `,狀態:${status}` : ''},${description.replace(/[。.!!??]+$/u, '')}`,
+ connectTitle: (name: string) => `連線 ${name}`, addAccountTitle: (name: string) => `新增 ${name} 帳號`, login: (name: string) => `登入 ${name}`, loginAndAdd: '登入並新增', signedOut: (name: string) => `${name} 尚未登入。`,
+ storageFailed: (name: string) => `${name} 本地憑據讀取失敗,請重新登入。`, providerUnavailable: (name: string) => `${name} 已登入,但目前 provider 狀態不可用。`,
+ },
+} as const;
export type ProviderSettingsCopy = WidenCopy<typeof zhCopy>;
@@ -416,7 +609,11 @@ const enCopy: ProviderSettingsCopy = {
},
};
-const PROVIDER_SETTINGS_COPY = { zh: zhCopy, en: enCopy } satisfies UiCatalog<ProviderSettingsCopy>;
+const PROVIDER_SETTINGS_COPY = {
+ 'zh-CN': zhCopy,
+ 'zh-TW': zhTwCopy,
+ en: enCopy,
+} satisfies UiCatalog<ProviderSettingsCopy>;
export function getProviderSettingsCopy(locale: UiLocale): ProviderSettingsCopy {
return PROVIDER_SETTINGS_COPY[locale];
diff --git a/apps/desktop/src/renderer/features/connection-settings/subscription-result-message.ts b/apps/desktop/src/renderer/features/connection-settings/subscription-result-message.ts
new file mode 100644
index 0000000000..125e073d68
--- /dev/null
+++ b/apps/desktop/src/renderer/features/connection-settings/subscription-result-message.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { generalizedErrorMessageForLocale, redactSecrets } from '@maka/core/redaction';
+import type { UiLocale } from '@maka/core/ui-locale';
+
+export function subscriptionResultMessage(
+ message: string | undefined,
+ fallback: string,
+ locale: UiLocale = 'zh-CN',
+ reason?: string,
+): string {
+ const raw = redactSecrets(message ?? '').trim();
+ // The Host refuses an enrollment this install has not opted into and says so
+ // with a typed reason. Read the reason, not the English message: a reworded
+ // string or an added locale must not silently disable this branch. The
+ // message match stays only as a fallback for callers without a typed reason.
+ if (reason === 'experimental_disabled' || /enrollment is disabled for this provider/i.test(raw)) {
+ if (locale === 'zh-CN') return '本机未启用该账号登录方式;可改用导入兼容凭据,或由管理员启用后重试。';
+ if (locale === 'zh-TW') return '本機未啟用該帳號登入方式;可改用匯入相容憑據,或由管理員啟用後重試。';
+ return 'This sign-in is not enabled on this install. Import a compatible credential instead, or ask an operator to enable it.';
+ }
+ if (!raw) return fallback;
+ if (/already in progress|superseded by a new attempt/i.test(raw)) {
+ if (locale === 'zh-CN') return '上一轮浏览器登录仍在进行或已切换,请再点一次登录,或稍后再试。';
+ if (locale === 'zh-TW') return '上一輪瀏覽器登入仍在進行或已切換,請再按一次登入,或稍後再試。';
+ return 'A previous browser login is still running or was superseded. Try logging in again shortly.';
+ }
+ if (/did not present OAuth|no matching OAuth presentation/i.test(raw)) {
+ if (locale === 'zh-CN') return '无法打开系统浏览器完成登录,请检查是否拦截了弹窗后重试。';
+ if (locale === 'zh-TW') return '無法開啟系統瀏覽器完成登入,請檢查是否封鎖了彈出式視窗後再試。';
+ return 'Could not open the system browser for login. Check popup blockers and try again.';
+ }
+ const classified = generalizedErrorMessageForLocale(new Error(raw), '', locale);
+ if (classified) return classified;
+ return locale === 'zh-CN' || !/[\u4e00-\u9fff]/.test(raw) ? raw : fallback;
+}
diff --git a/apps/desktop/src/renderer/features/conversation/model/task-readiness-notice.ts b/apps/desktop/src/renderer/features/conversation/model/task-readiness-notice.ts
index 6ebe6b11f9..df90d2a57e 100644
--- a/apps/desktop/src/renderer/features/conversation/model/task-readiness-notice.ts
+++ b/apps/desktop/src/renderer/features/conversation/model/task-readiness-notice.ts
@@ -86,36 +86,56 @@ function noticeForBlocker(
locale: UiLocale,
): TaskReadinessNotice {
if (blocker.id === 'runtime') {
- return locale === 'zh'
- ? {
- tone: 'destructive',
- title: 'Maka 运行服务暂时不可用。',
- description: '任务尚未提交。重新检测运行服务后再试。',
- actionLabel: '重新检测',
- action: 'retry',
- }
- : {
- tone: 'destructive',
- title: 'The Maka runtime is unavailable.',
- description: 'The task was not submitted. Check the runtime again before retrying.',
- actionLabel: 'Check again',
- action: 'retry',
- };
- }
- const action = blocker.repairTarget?.kind === 'workspace_picker' ? 'workspace_picker' : 'retry';
- return locale === 'zh'
- ? {
+ if (locale === 'zh-CN') {
+ return {
tone: 'destructive',
- title: '当前任务的工作区不可用。',
- description: '原目录可能已移动、删除或无法访问。请选择可用工作区。',
- actionLabel: action === 'workspace_picker' ? '选择工作区' : '重新检测',
- action,
- }
- : {
+ title: 'Maka 运行服务暂时不可用。',
+ description: '任务尚未提交。重新检测运行服务后再试。',
+ actionLabel: '重新检测',
+ action: 'retry',
+ };
+ }
+ if (locale === 'zh-TW') {
+ return {
tone: 'destructive',
- title: 'This task workspace is unavailable.',
- description: 'The folder may have moved, been deleted, or become inaccessible. Choose an available workspace.',
- actionLabel: action === 'workspace_picker' ? 'Choose workspace' : 'Check again',
- action,
+ title: 'Maka 執行服務暫時無法使用。',
+ description: '任務尚未送出。重新檢查執行服務後再試。',
+ actionLabel: '重新檢查',
+ action: 'retry',
};
+ }
+ return {
+ tone: 'destructive',
+ title: 'The Maka runtime is unavailable.',
+ description: 'The task was not submitted. Check the runtime again before retrying.',
+ actionLabel: 'Check again',
+ action: 'retry',
+ };
+ }
+ const action = blocker.repairTarget?.kind === 'workspace_picker' ? 'workspace_picker' : 'retry';
+ if (locale === 'zh-CN') {
+ return {
+ tone: 'destructive',
+ title: '当前任务的工作区不可用。',
+ description: '原目录可能已移动、删除或无法访问。请选择可用工作区。',
+ actionLabel: action === 'workspace_picker' ? '选择工作区' : '重新检测',
+ action,
+ };
+ }
+ if (locale === 'zh-TW') {
+ return {
+ tone: 'destructive',
+ title: '目前任務的工作區無法使用。',
+ description: '原始資料夾可能已移動、刪除或無法存取。請選擇可用的工作區。',
+ actionLabel: action === 'workspace_picker' ? '選擇工作區' : '重新檢查',
+ action,
+ };
+ }
+ return {
+ tone: 'destructive',
+ title: 'This task workspace is unavailable.',
+ description: 'The folder may have moved, been deleted, or become inaccessible. Choose an available workspace.',
+ actionLabel: action === 'workspace_picker' ? 'Choose workspace' : 'Check again',
+ action,
+ };
}
diff --git a/apps/desktop/src/renderer/features/runtime-host-management/index.ts b/apps/desktop/src/renderer/features/runtime-host-management/index.ts
index 005423d585..7b16b77ad4 100644
--- a/apps/desktop/src/renderer/features/runtime-host-management/index.ts
+++ b/apps/desktop/src/renderer/features/runtime-host-management/index.ts
@@ -17,16 +17,19 @@
* under the License.
*/
-export { RuntimeHostPeerMeshDialog } from './ui/runtime-host-peer-mesh-dialog';
-export { PeerMeshPeerIdButton } from './ui/peer-mesh-peer-id-button';
-export { RuntimeHostAddComputerMenu } from './ui/runtime-host-add-computer-menu';
-export { RuntimeHostConnectionCodeButton } from './ui/runtime-host-connection-code-button';
-export { RuntimeHostConnectionCodeDialog } from './ui/runtime-host-connection-code-dialog';
+export {
+ getRuntimeHostPeerMeshCopy,
+ RuntimeHostPeerMeshDialog,
+} from './ui/runtime-host-peer-mesh-dialog.js';
+export { PeerMeshPeerIdButton } from './ui/peer-mesh-peer-id-button.js';
+export { RuntimeHostAddComputerMenu } from './ui/runtime-host-add-computer-menu.js';
+export { RuntimeHostConnectionCodeButton } from './ui/runtime-host-connection-code-button.js';
+export { RuntimeHostConnectionCodeDialog } from './ui/runtime-host-connection-code-dialog.js';
export {
RuntimeHostPairingRecoveryButton,
RuntimeHostProfileMoreMenu,
-} from './ui/runtime-host-profile-pairing-actions';
-export type { RuntimeHostPairingActionCopy } from './ui/runtime-host-profile-pairing-actions';
-export { RuntimeHostManagementServicesProvider } from './services-context';
-export { PeerMeshOperationOutcomeUnknownError } from './ports';
-export type { RuntimeHostManagementServices } from './ports';
+} from './ui/runtime-host-profile-pairing-actions.js';
+export type { RuntimeHostPairingActionCopy } from './ui/runtime-host-profile-pairing-actions.js';
+export { RuntimeHostManagementServicesProvider } from './services-context.js';
+export { PeerMeshOperationOutcomeUnknownError } from './ports.js';
+export type { RuntimeHostManagementServices } from './ports.js';
diff --git a/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-peer-mesh-dialog.tsx b/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-peer-mesh-dialog.tsx
index 49788c0f29..ecd956369b 100644
--- a/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-peer-mesh-dialog.tsx
+++ b/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-peer-mesh-dialog.tsx
@@ -25,6 +25,7 @@ import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
import { HStack } from '@astryxdesign/core/Stack';
import { Tooltip } from '@astryxdesign/core/Tooltip';
import type { PeerMeshProjection, PeerMeshQueryResult } from '@maka/runtime-host/protocol';
+import type { UiLocale } from '@maka/core/ui-locale';
import {
decodeRuntimeHostWebRtcStunPolicy,
type RuntimeHostWebRtcStunPolicy,
@@ -126,7 +127,7 @@ export function RuntimeHostPeerMeshDialog(props: {
readonly onClose: () => void;
}) {
const locale = useUiLocale();
- const copy = peerMeshCopy(locale);
+ const copy = getRuntimeHostPeerMeshCopy(locale);
const toast = useToast();
const services = useRuntimeHostManagementServices().peerMesh;
const [snapshot, setSnapshot] = useState<PeerMeshQueryResult>();
@@ -855,7 +856,7 @@ export function RuntimeHostPeerMeshDialog(props: {
function PeerMeshAdvancedSettings(props: {
readonly localPeerId?: string;
readonly connectivity?: DesktopConnectivityControls;
- readonly copy: ReturnType<typeof peerMeshCopy>;
+ readonly copy: ReturnType<typeof getRuntimeHostPeerMeshCopy>;
readonly onCopyPeerId: (peerId: string) => void;
}) {
const { connectivity } = props;
@@ -1007,7 +1008,7 @@ function PeerMeshAdvancedSettings(props: {
function Overview(props: {
readonly snapshot: PeerMeshQueryResult | undefined;
- readonly copy: ReturnType<typeof peerMeshCopy>;
+ readonly copy: ReturnType<typeof getRuntimeHostPeerMeshCopy>;
readonly working: boolean;
readonly localPeerLabel: string;
readonly onInvite: (meshId: string) => void;
@@ -1232,7 +1233,7 @@ function Overview(props: {
function UnavailableEndpoint(props: {
readonly setup: ManagedHostPeerSetup;
readonly working: boolean;
- readonly copy: ReturnType<typeof peerMeshCopy>;
+ readonly copy: ReturnType<typeof getRuntimeHostPeerMeshCopy>;
readonly onEnable: () => void;
readonly onInspect?: () => void;
readonly onRefresh: () => void;
@@ -1315,7 +1316,7 @@ function UnavailableEndpoint(props: {
function JoinView(props: {
readonly value: string;
readonly working: boolean;
- readonly copy: ReturnType<typeof peerMeshCopy>;
+ readonly copy: ReturnType<typeof getRuntimeHostPeerMeshCopy>;
readonly onChange: (value: string) => void;
}) {
return (
@@ -1347,7 +1348,7 @@ function JoinView(props: {
function InvitationView(props: {
readonly invitation: Extract<PeerMeshDialogView, { readonly kind: 'invitation' }>;
- readonly copy: ReturnType<typeof peerMeshCopy>;
+ readonly copy: ReturnType<typeof getRuntimeHostPeerMeshCopy>;
}) {
return (
<div className="settingsPeerMeshFocusedView">
@@ -1398,7 +1399,7 @@ function InvitationView(props: {
function MeshCard(props: {
readonly mesh: PeerMeshProjection;
readonly transit: PeerMeshQueryResult['transit'];
- readonly copy: ReturnType<typeof peerMeshCopy>;
+ readonly copy: ReturnType<typeof getRuntimeHostPeerMeshCopy>;
readonly working: boolean;
readonly onInvite: () => void;
readonly onRemove: (peerId: string) => void;
@@ -1698,7 +1699,7 @@ function TransitMetric(props: { readonly label: string; readonly value: string }
function PeerIdText(props: {
readonly peerId: string;
- readonly copy: ReturnType<typeof peerMeshCopy>;
+ readonly copy: ReturnType<typeof getRuntimeHostPeerMeshCopy>;
readonly onCopy: (peerId: string) => void;
}) {
return (
@@ -1717,7 +1718,7 @@ function PeerIdText(props: {
function MeshIdText(props: {
readonly meshId: string;
- readonly copy: ReturnType<typeof peerMeshCopy>;
+ readonly copy: ReturnType<typeof getRuntimeHostPeerMeshCopy>;
readonly onCopy: (meshId: string) => void;
}) {
return (
@@ -1803,9 +1804,8 @@ function abbreviate(peerId: string): string {
return peerId.length <= 22 ? peerId : `${peerId.slice(0, 11)}…${peerId.slice(-7)}`;
}
-function peerMeshCopy(locale: string) {
- const zh = locale.startsWith('zh');
- return zh
+export function getRuntimeHostPeerMeshCopy(locale: UiLocale) {
+ return locale === 'zh-CN'
? {
title: 'Peer Mesh',
experimental: '实验性',
@@ -1881,6 +1881,9 @@ function peerMeshCopy(locale: string) {
meshIdHelp: '用于诊断和识别此 Mesh;点击 ID 可复制完整值。',
thisRuntimeHost: '本机 Runtime Host',
thisDesktop: '本机 Desktop',
+ peerPathDirect: '直连',
+ peerPathTransit: '成员转发',
+ peerPathOther: '其他',
displayName: '在 Mesh 中显示的名称',
meshDisplayName: 'Mesh 名称',
unnamedMesh: '未命名 Mesh',
@@ -1888,6 +1891,7 @@ function peerMeshCopy(locale: string) {
renameMesh: '修改 Mesh 名称',
save: '保存',
peerIdCopied: 'Peer ID 已复制',
+ peerIdCopyFailed: '无法复制 Peer ID',
meshIdCopied: 'Mesh ID 已复制',
copyPeerId: (value: string) => `复制完整 Peer ID:${value}`,
copyMeshId: (value: string) => `复制完整 Mesh ID:${value}`,
@@ -1965,7 +1969,170 @@ function peerMeshCopy(locale: string) {
localHostMissing: '本机 Runtime Host 尚未加入',
localHostMissingHint: '加入后,其他成员才能通过此 Mesh 连接本机分享的任务。',
}
- : {
+ : locale === 'zh-TW'
+ ? {
+ title: 'Peer Mesh',
+ experimental: '實驗性',
+ failed: 'Peer Mesh 操作失敗',
+ invalidResult: 'Peer Mesh 回傳了無效結果',
+ unknownError: 'Peer Mesh 操作失敗',
+ outcomeUnknown: 'Host 可能已完成此操作。已重新整理目前狀態,請確認後再重試。',
+ invitationOutcomeUnknown:
+ 'Host 可能已建立邀請碼,但代碼未能回傳且無法復原。它會自動過期;建立新邀請前請先檢查待使用邀請數量。',
+ outcomeUnknownRefreshFailed:
+ 'Host 可能已完成此操作,但目前狀態也無法重新整理。請恢復連線並重新整理後再重試。',
+ unavailable: '目前 endpoint 不支援 Peer Mesh',
+ loading: '正在讀取 Mesh 狀態…',
+ checkingPeerConnection: '正在檢查此 Runtime Host 的 Peer 連線…',
+ peerConnectionDisabled: '此 Runtime Host 尚未開啟 Peer 連線',
+ peerConnectionDisabledHint:
+ '開啟後,此 Host 才能建立或加入 Mesh;現有 SSH 連線會繼續保留。',
+ peerConnectionDisableProfileFirst:
+ '此 Host 的 Direct peer 連線正在使用中,請先在 Host 列表中將它停用。',
+ enablePeerConnection: '開啟 Peer 連線',
+ peerConnectionStarting: 'Peer 連線已開啟,Mesh endpoint 正在就緒',
+ peerConnectionStartingHint: '通常只需幾秒;也可以立即重新檢查。',
+ peerConnectionUpgradeRequired: '此 Runtime Host 版本尚不支援 Peer Mesh 管理',
+ peerConnectionUpgradeRequiredHint: '請先更新此 Host,再回來開啟 Peer 連線。',
+ working: {
+ refresh: '正在重新整理 Peer Mesh…',
+ create: '正在建立 Mesh…',
+ join: '正在加入 Mesh…',
+ invite: '正在準備邀請…',
+ 'add-host': '正在將 Runtime Host 加入 Mesh…',
+ 'enable-peer': '正在為 Runtime Host 開啟 Peer 連線…',
+ update: '正在更新 Mesh…',
+ rename: '正在儲存名稱…',
+ },
+ settling: '正在確認最終狀態…',
+ endpoint: '管理對象',
+ desktopEndpoint: 'Desktop Client',
+ hostEndpoint: '本機 Runtime Host',
+ desktopEndpointHelp: '此 Client 用於連線至 Mesh 中的 Runtime Host。',
+ hostEndpointHelp: '此 Host 加入後,其他成員才能連線至本機分享的任務。',
+ advancedSettings: '進階設定',
+ technicalDetails: '身分與連線設定',
+ connectivityAutomatic: '自動連線(建議)',
+ connectivityKnownRoutesOnly: '僅使用已知路徑',
+ connectivityCustom: '自訂位址探索',
+ restartRequired: '需要重新啟動',
+ adaptiveConnectivity: '自適應連線',
+ adaptiveConnectivityHelp:
+ 'Maka 會自動競速可用的直接連線方式,並在獲准時使用成員轉送;此處不需要選擇特定通訊協定。',
+ connectivityPolicyLoading: '正在讀取連線策略…',
+ connectivityPolicyLoadFailed: '無法讀取連線策略',
+ connectivityPolicySaveFailed: '無法儲存連線策略',
+ restoreDefaultConnectivityPolicy: '恢復預設設定',
+ connectivityPolicyRestartRequired:
+ '重新啟動 Maka 後,已儲存的連線策略變更會套用至新連線。',
+ publicAddressDiscovery: '公網位址探索',
+ publicStunDefault: '公共 STUN(建議)',
+ publicStunDisabled: '不使用公共 STUN',
+ publicStunCustom: '自訂 STUN',
+ customStunUrls: 'STUN 位址',
+ customStunUrlsInvalid: '請輸入以逗號分隔的 stun:主機[:連接埠] 位址,最多 8 個。',
+ publicStunDefaultHelp:
+ '使用 Cloudflare 公共 STUN 盡力探索公網對映。它不承載 Maka 流量,但提供者可觀察來源 IP 和請求時間;Maka 不保證其可用性。',
+ publicStunDisabledHelp:
+ '僅嘗試本機位址和其他已知直接連線路徑;跨 NAT 的直接連線成功率可能降低。',
+ publicStunCustomHelp:
+ '使用逗號分隔的 stun: 位址。STUN 只探索網路位址,不承載 Session 內容。',
+ saveConnectivityPolicy: '儲存變更',
+ peerId: 'Peer ID',
+ peerIdHelp: '此 endpoint 在 Mesh 中的技術身分;點選 ID 可複製完整值。',
+ meshId: 'Mesh ID',
+ meshIdHelp: '用於診斷和識別此 Mesh;點選 ID 可複製完整值。',
+ thisRuntimeHost: '本機 Runtime Host',
+ thisDesktop: '本機 Desktop',
+ peerPathDirect: '直接連線',
+ peerPathTransit: '成員轉送',
+ peerPathOther: '其他',
+ displayName: '在 Mesh 中顯示的名稱',
+ meshDisplayName: 'Mesh 名稱',
+ unnamedMesh: '未命名 Mesh',
+ rename: '修改名稱',
+ renameMesh: '修改 Mesh 名稱',
+ save: '儲存',
+ peerIdCopied: 'Peer ID 已複製',
+ peerIdCopyFailed: '無法複製 Peer ID',
+ meshIdCopied: 'Mesh ID 已複製',
+ copyPeerId: (value: string) => `複製完整 Peer ID:${value}`,
+ copyMeshId: (value: string) => `複製完整 Mesh ID:${value}`,
+ empty: '建立你的第一個 Mesh',
+ emptyHint: '建立新的 Mesh,或透過一次性邀請碼加入。',
+ meshes: 'Mesh',
+ mesh: 'Mesh',
+ members: '成員',
+ activeMeshCount: (value: number) => `${value} 個使用中`,
+ showClosedMeshes: (value: number) => `顯示已關閉(${value})`,
+ noActiveMeshes: '沒有使用中的 Mesh',
+ noActiveMeshesHint: '已關閉的 Mesh 預設隱藏;可透過上方篩選查看。',
+ authority: '管理者',
+ member: '成員',
+ closed: '已關閉',
+ memberCount: (value: number) => `${value} 個成員`,
+ pending: (value: number) => `${value} 個待使用邀請`,
+ transit: '成員轉送',
+ transitHelp: '允許此 Mesh 的成員透過本機建立連線;會使用本機頻寬。',
+ transitToggle: '為此 Mesh 提供轉送',
+ transitStatus: '成員轉送狀態',
+ transitLimitsLabel: '成員轉送限制',
+ transitLimits: (value: PeerMeshQueryResult['transit']) =>
+ value
+ ? `固定上限:每位成員 ${value.maxCircuitsPerPeer} 條連線,每條最長 ${formatHours(value.maxCircuitDurationSeconds)},最多 ${formatMebibytes(value.maxCircuitBytes)}。一次只能為一個 Mesh 開啟。`
+ : '成員轉送使用固定資源上限,一次只能為一個 Mesh 開啟。',
+ allowedMembers: '允許成員',
+ reservations: 'Reservation',
+ circuits: '連線',
+ routeState: {
+ local: '本機',
+ route_available: '路徑可用',
+ coordination_only: '僅協調路徑',
+ stale: '路徑已過期',
+ unknown: '路徑未知',
+ },
+ endpointKind: {
+ client: 'Client',
+ host: 'Runtime Host',
+ unknown: '未識別 Peer',
+ },
+ endpointKindHelp: {
+ client: 'Client 是操作介面:它連線至 Host、瀏覽任務並發起操作,本身不持有任務。',
+ host: 'Runtime Host 持有任務和執行狀態,並執行經過授權的工作。',
+ unknown: '此 Peer 尚未回報它是 Client 或 Runtime Host,通常來自舊版本。',
+ },
+ joinTitle: '加入 Mesh',
+ joinHint: '貼上另一個 Peer 產生的一次性邀請碼。',
+ joinCode: '邀請碼',
+ join: '加入',
+ joinMesh: '加入 Mesh',
+ invite: '邀請成員',
+ invitationTitle: '邀請成員',
+ invitationFor: (value: string) => `Mesh ${value}`,
+ invitationWarning: '此代碼只能使用一次;取得代碼的人可以讓一個 peer 加入此 Mesh。',
+ invitationDirectOnly:
+ '尚未連線至協調節點。此邀請碼只包含直接位址,跨 NAT 時可能無法連線。',
+ invitationExpires: (value: string) => `有效期限至 ${value}`,
+ invitationCopied: '邀請碼已複製',
+ copyInvitation: '複製邀請碼',
+ create: '建立 Mesh',
+ refresh: '重新整理',
+ back: '返回',
+ leave: '退出 Mesh',
+ closeMesh: '關閉 Mesh',
+ remove: '移除成員',
+ cancel: '取消',
+ closeConfirm: '關閉這個 Mesh?',
+ leaveConfirm: '退出這個 Mesh?',
+ removeConfirm: '移除這個成員?',
+ meshActions: 'Mesh 操作',
+ memberActions: (peerId: string) => `${peerId} 的操作`,
+ addLocalHost: '新增本機 Runtime Host',
+ localRuntimeHost: 'Runtime Host',
+ localHostMissing: '本機 Runtime Host 尚未加入',
+ localHostMissingHint: '加入後,其他成員才能透過此 Mesh 連線至本機分享的任務。',
+ }
+ : {
title: 'Peer Mesh',
experimental: 'Experimental',
failed: 'Peer Mesh operation failed',
@@ -2041,6 +2208,9 @@ function peerMeshCopy(locale: string) {
meshIdHelp: 'Used to identify and diagnose this Mesh. Select the ID to copy its full value.',
thisRuntimeHost: 'This Runtime Host',
thisDesktop: 'This Desktop',
+ peerPathDirect: 'Direct',
+ peerPathTransit: 'Member transit',
+ peerPathOther: 'Other',
displayName: 'Name shown in the Mesh',
meshDisplayName: 'Mesh name',
unnamedMesh: 'Unnamed Mesh',
@@ -2048,6 +2218,7 @@ function peerMeshCopy(locale: string) {
renameMesh: 'Rename Mesh',
save: 'Save',
peerIdCopied: 'Peer ID copied',
+ peerIdCopyFailed: 'Could not copy Peer ID',
meshIdCopied: 'Mesh ID copied',
copyPeerId: (value: string) => `Copy full Peer ID: ${value}`,
copyMeshId: (value: string) => `Copy full Mesh ID: ${value}`,
diff --git a/apps/desktop/src/renderer/features/session-navigation/model/session-project-grouping.ts b/apps/desktop/src/renderer/features/session-navigation/model/session-project-grouping.ts
index 8fa85cf5a6..9499e88782 100644
--- a/apps/desktop/src/renderer/features/session-navigation/model/session-project-grouping.ts
+++ b/apps/desktop/src/renderer/features/session-navigation/model/session-project-grouping.ts
@@ -28,7 +28,7 @@ const UNGROUPED_KEY = '__ungrouped__';
export function deriveProjectGroups(
sessions: ReadonlyArray<SessionSummary>,
projects: ReadonlyArray<ProjectRecord>,
- locale: UiLocale = 'zh',
+ locale: UiLocale = 'zh-CN',
): SessionHistoryGroup[] {
const sessionsByProject = new Map<string, SessionSummary[]>();
const canonicalProjectIds = new Map<string, string>();
diff --git a/apps/desktop/src/renderer/features/workbar/testing.ts b/apps/desktop/src/renderer/features/workbar/testing.ts
index e7f38293e8..4c33d49d3e 100644
--- a/apps/desktop/src/renderer/features/workbar/testing.ts
+++ b/apps/desktop/src/renderer/features/workbar/testing.ts
@@ -53,6 +53,7 @@ export * from './tools/terminal/session-terminal-hydration.js';
export * from './tools/terminal/session-terminal-query.js';
export * from './tools/terminal/session-terminal-frame.js';
export * from './tools/inspector/use-session-trace.js';
+export * from './tools/tasks/use-session-todo.js';
export * from './controller/use-workbar-controller.js';
export { SideChatCloseConfirmation } from './ui/side-chat-close-confirmation.js';
diff --git a/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-pane.tsx b/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-pane.tsx
index c81f06ce6e..89566fe11c 100644
--- a/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-pane.tsx
+++ b/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-pane.tsx
@@ -63,7 +63,7 @@ import {
import type { ArtifactDescriptor, ArtifactKind } from '@maka/core/artifacts';
import type { UiLocale } from '@maka/core/ui-locale';
import { formatRelativeTimestamp } from '@maka/core/relative-time';
-import { generalizedErrorMessage, generalizedErrorMessageChinese, redactSecrets } from '@maka/core/redaction';
+import { generalizedErrorMessageForLocale, redactSecrets } from '@maka/core/redaction';
import {
Badge,
Banner,
@@ -631,11 +631,9 @@ function saveArtifactFailureCopy(reason: string, copy: ArtifactCopy): string {
function artifactActionErrorMessage(error: unknown, locale: UiLocale, copy: ArtifactCopy): string {
const raw = redactSecrets(error instanceof Error ? error.message : String(error ?? '')).trim();
if (!raw) return copy.pane.actionFailed;
- const classified = locale === 'zh'
- ? generalizedErrorMessageChinese(new Error(raw), '')
- : generalizedErrorMessage(new Error(raw), '');
+ const classified = generalizedErrorMessageForLocale(new Error(raw), '', locale);
if (classified) return classified;
- return locale === 'zh' && /[\u4e00-\u9fff]/.test(raw) ? raw : copy.pane.actionFailed;
+ return locale === 'zh-CN' && /[\u4e00-\u9fff]/.test(raw) ? raw : copy.pane.actionFailed;
}
function KindIcon(props: { kind: ArtifactKind }) {
diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/use-session-trace.ts b/apps/desktop/src/renderer/features/workbar/tools/inspector/use-session-trace.ts
index 92df88050c..560ff4f47f 100644
--- a/apps/desktop/src/renderer/features/workbar/tools/inspector/use-session-trace.ts
+++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/use-session-trace.ts
@@ -18,7 +18,7 @@
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { generalizedErrorMessage, generalizedErrorMessageChinese } from '@maka/core/redaction';
+import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
import { type UiLocale } from '@maka/core/ui-locale';
import {
mergeSessionTraces,
@@ -145,9 +145,7 @@ export function useSessionTrace(
loading: false,
loadingEarlier: false,
error:
- copy.locale === 'zh'
- ? generalizedErrorMessageChinese(error, copy.loadFailed)
- : generalizedErrorMessage(error, copy.loadFailed),
+ generalizedErrorMessageForLocale(error, copy.loadFailed, copy.locale),
}
: current,
);
@@ -202,9 +200,7 @@ export function useSessionTrace(
loading: false,
loadingEarlier: false,
error:
- copy.locale === 'zh'
- ? generalizedErrorMessageChinese(error, copy.loadFailed)
- : generalizedErrorMessage(error, copy.loadFailed),
+ generalizedErrorMessageForLocale(error, copy.loadFailed, copy.locale),
}
: current,
);
diff --git a/apps/desktop/src/renderer/features/workbar/tools/review/session-review-panel.tsx b/apps/desktop/src/renderer/features/workbar/tools/review/session-review-panel.tsx
index 3f6884d0c0..a31b9f34cf 100644
--- a/apps/desktop/src/renderer/features/workbar/tools/review/session-review-panel.tsx
+++ b/apps/desktop/src/renderer/features/workbar/tools/review/session-review-panel.tsx
@@ -27,7 +27,7 @@ import { Section } from '@astryxdesign/core/Section';
import { Skeleton } from '@astryxdesign/core/Skeleton';
import { Text } from '@astryxdesign/core/Text';
import { redactSecrets as displayRedactSecrets } from '@maka/core/display-redaction';
-import { generalizedErrorMessage, generalizedErrorMessageChinese } from '@maka/core/redaction';
+import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
import { type GitReviewReadResult } from '@maka/core/git-review';
import { DiffCodePreview, useUiLocale } from '@maka/ui';
import { ICON_SIZE, GitBranch } from '@maka/ui/icons';
@@ -76,9 +76,7 @@ export function SessionReviewPanel(props: {
} catch (nextError) {
if (revision === revisionRef.current) {
setError(
- locale === 'zh'
- ? generalizedErrorMessageChinese(nextError, copy.loadFailed)
- : generalizedErrorMessage(nextError, copy.loadFailed),
+ generalizedErrorMessageForLocale(nextError, copy.loadFailed, locale),
);
}
} finally {
diff --git a/apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx b/apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx
index d5ff70372d..d49fafb4a0 100644
--- a/apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx
+++ b/apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx
@@ -34,10 +34,7 @@ import {
type ComposerHandle,
} from '@maka/ui';
import type { SessionSummary } from '@maka/core/session';
-import {
- generalizedErrorMessage,
- generalizedErrorMessageChinese,
-} from '@maka/core/redaction';
+import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
import { useQuoteCompanion } from './use-quote-companion';
import { useComposerAttachments } from '../../../../use-composer-attachments';
import { useComposerMentionsContext } from '../../../../composer-mentions.js';
@@ -182,9 +179,7 @@ export function QuoteCompanionPanel(props: {
const compactCopy = getDesktopConversationCopy(locale).quoteCompanion;
toast.error(
compactCopy.compactErrorTitle,
- locale === 'zh'
- ? generalizedErrorMessageChinese(error, compactCopy.compactErrorFallback)
- : generalizedErrorMessage(error, compactCopy.compactErrorFallback),
+ generalizedErrorMessageForLocale(error, compactCopy.compactErrorFallback, locale),
undefined,
{ sessionId },
);
diff --git a/apps/desktop/src/renderer/features/workbar/tools/tasks/use-session-todo.ts b/apps/desktop/src/renderer/features/workbar/tools/tasks/use-session-todo.ts
index fd2dc7d7df..9aa01b000d 100644
--- a/apps/desktop/src/renderer/features/workbar/tools/tasks/use-session-todo.ts
+++ b/apps/desktop/src/renderer/features/workbar/tools/tasks/use-session-todo.ts
@@ -18,7 +18,7 @@
*/
import { useCallback, useEffect, useRef, useState } from 'react';
-import { generalizedErrorMessage, generalizedErrorMessageChinese } from '@maka/core/redaction';
+import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
import type { SessionTodoItem } from '@maka/core/session-todo';
import type { UiLocale } from '@maka/ui';
import { useWorkbarServices } from '../../services-context.js';
@@ -58,10 +58,7 @@ export function useSessionTodo(
sessionId: targetSessionId,
items: current.sessionId === targetSessionId ? current.items : [],
loading: false,
- error:
- copy.locale === 'zh'
- ? generalizedErrorMessageChinese(error, copy.loadFailed)
- : generalizedErrorMessage(error, copy.loadFailed),
+ error: generalizedErrorMessageForLocale(error, copy.loadFailed, copy.locale),
}));
},
);
diff --git a/apps/desktop/src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx b/apps/desktop/src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx
index 7ebf7c34bb..86593ba060 100644
--- a/apps/desktop/src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx
+++ b/apps/desktop/src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx
@@ -20,10 +20,7 @@
import { useEffect, useRef, useState } from 'react';
import { Banner } from '@astryxdesign/core/Banner';
import { EmptyState } from '@astryxdesign/core/EmptyState';
-import {
- generalizedErrorMessage,
- generalizedErrorMessageChinese,
-} from '@maka/core/redaction';
+import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
import { useUiLocale } from '@maka/ui';
import { ICON_SIZE, Terminal as TerminalIcon } from '@maka/ui/icons';
import { FitAddon } from '@xterm/addon-fit';
@@ -148,9 +145,7 @@ export function SessionTerminalPanel(props: {
.catch((nextError) => {
if (disposed || !hydration.isCurrent(epoch)) return;
setError(
- locale === 'zh'
- ? generalizedErrorMessageChinese(nextError, copy.loadFailed)
- : generalizedErrorMessage(nextError, copy.loadFailed),
+ generalizedErrorMessageForLocale(nextError, copy.loadFailed, locale),
);
});
};
@@ -169,9 +164,7 @@ export function SessionTerminalPanel(props: {
.catch((nextError) => {
if (disposed) return;
setError(
- locale === 'zh'
- ? generalizedErrorMessageChinese(nextError, copy.writeFailed)
- : generalizedErrorMessage(nextError, copy.writeFailed),
+ generalizedErrorMessageForLocale(nextError, copy.writeFailed, locale),
);
});
});
diff --git a/apps/desktop/src/renderer/locales/artifact-copy.ts b/apps/desktop/src/renderer/locales/artifact-copy.ts
index 97dc2ec0fa..ad73b11379 100644
--- a/apps/desktop/src/renderer/locales/artifact-copy.ts
+++ b/apps/desktop/src/renderer/locales/artifact-copy.ts
@@ -92,7 +92,7 @@ export type ArtifactCopy = {
};
const ARTIFACT_COPY = {
- zh: {
+ 'zh-CN': {
pane: {
refreshFailed: '刷新生成文件失败', openFailed: '无法在 Finder 中打开生成文件', copyFailed: '复制失败',
readTextFailed: '无法读取生成文件文本内容。', copied: '已复制生成文件文本', saved: '已另存生成文件', saveFailed: '另存失败',
@@ -128,6 +128,42 @@ const ARTIFACT_COPY = {
unsupported: '暂不支持的预览', name: '名称', unnamed: '(未命名)', type: '类型', size: '大小', openInFinder: '在 Finder 中打开', loadingImage: '加载图片预览…',
},
},
+ 'zh-TW': {
+ pane: {
+ refreshFailed: '重新整理生成檔案失敗', openFailed: '無法在 Finder 中開啟生成檔案', copyFailed: '複製失敗',
+ readTextFailed: '無法讀取生成檔案文本內容。', copied: '已複製生成檔案文本', saved: '已另存生成檔案', saveFailed: '另存失敗',
+ fallbackName: '生成檔案', deleteTitle: (name) => `刪除 "${name}"`, deleteDescription: '軟刪除:在記錄中標記為已刪除,檔案保留 6 小時可恢復。',
+ delete: '刪除', deleteReadOnly: '刪除(只讀檔案)', cancel: '取消', deleted: (name) => `已刪除 ${name}`, deleteFailed: (name) => `刪除 ${name} 失敗`, panelAria: '生成檔案預覽面板',
+ listLoadFailed: '生成檔案列表載入失敗', retrying: '重試中…', retry: '重試', listAria: '生成檔案列表', deletedBadge: '已刪除',
+ previewNamed: (name) => `預覽 ${name}`, empty: '暫無生成檔案', emptyHint: '助手生成檔案後會顯示在這裡。',
+ back: '返回生成檔案列表', moreActions: (name) => `${name} 的更多操作`,
+ openInFinder: '在 Finder 中開啟', saveAs: '另存為', copy: '複製',
+ saveFailures: { not_found: '生成檔案不存在。', not_allowed: '生成檔案路徑檢查未透過。', deleted: '生成檔案已刪除,不能另存。', write_failed: '目標位置無法寫入。', default: '無法儲存生成檔案。' },
+ actionFailed: '生成檔案操作失敗,請稍後重試。',
+ },
+ preview: {
+ loadingFile: '載入檔案預覽…', loadingDiff: '載入 diff 預覽…', loadingHtml: '載入 HTML 預覽…',
+ externalLinks: (count) => `此預覽中已停用外部連結 · ${count} 個連結`, frameTitle: (name) => `生成檔案預覽 · ${name}`,
+ loadingPdf: '載入 PDF 預覽…', pdfFallback: '如果瀏覽器沒有內建 PDF 渲染,請透過更多選單「在 Finder 中開啟」檢視。',
+ rendered: '預覽', source: '原始碼', previewLimited: (limit) => `僅顯示前 ${limit};可透過更多選單開啟或另存完整檔案。`,
+ renderLimited: (limit, lines) => `為保證流暢,富文本預覽僅展開前 ${limit}、最多 ${lines} 行;完整原始碼仍可檢視。`,
+ highlightLimited: (limit, lines) => `為保證流暢,僅高亮前 ${limit}、最多 ${lines} 行,其餘內容以純文本顯示。`,
+ diffLinesLimited: (count) => `為保證流暢,另有 ${count} 行未在預覽中展開。`,
+ readFailed: { title: '無法讀取生成檔案', description: '路徑可能已被外部刪除。請透過更多選單「在 Finder 中開啟」檢查檔案位置。' },
+ notAllowed: { title: '無法讀取生成檔案', description: '路徑檢查未透過,檔案已不在允許預覽的生成檔案目錄內。' },
+ tooLarge: (bytes) => ({ title: '檔案超出預覽大小', description: `${bytes} 位元組超過文本預覽閾值,請透過更多選單開啟或另存完整內容。` }),
+ deleted: { title: '此生成檔案已刪除', description: '預覽已停止。如需檢視原檔案請使用「在 Finder 中開啟」。' },
+ unsupportedMime: { title: '不支援的檔案型別', description: '該生成檔案的 MIME 型別不在內聯預覽允許列表中。請使用工具欄「在 Finder 中開啟」或「另存為」。' },
+ },
+ registry: {
+ kindDisallowed: { title: '目前預覽暫不支援該型別', description: '此類生成檔案不能在面板內直接預覽。請使用「在 Finder 中開啟」檢視。' },
+ mimeDisallowed: { title: '格式暫不支援預覽', description: '已識別到檔案的 MIME 型別,但目前預覽只支援 PNG / JPEG / GIF / WebP / AVIF。' },
+ unknownType: { title: '無法識別檔案型別', description: '檔案沒有 MIME 後設資料,副檔名也未符合。請透過「在 Finder 中開啟」檢視。' },
+ oversize: { title: '檔案過大,暫不預覽', description: '為避免在記憶體中載入大體積圖片,超過 2 MB 的檔案不在此處展開預覽。' },
+ readFailed: { title: '載入預覽失敗', description: '無法讀取檔案內容(可能已被刪除、移動或權限不足)。請透過「在 Finder 中開啟」檢查檔案。' },
+ unsupported: '暫不支援的預覽', name: '名稱', unnamed: '(未命名)', type: '型別', size: '大小', openInFinder: '在 Finder 中開啟', loadingImage: '載入圖片預覽…',
+ },
+ },
en: {
pane: {
refreshFailed: 'Failed to refresh generated files', openFailed: 'Could not show generated file in Finder', copyFailed: 'Copy failed',
diff --git a/apps/desktop/src/renderer/locales/browser-copy.ts b/apps/desktop/src/renderer/locales/browser-copy.ts
index 237ffe6544..f2e7a82dc5 100644
--- a/apps/desktop/src/renderer/locales/browser-copy.ts
+++ b/apps/desktop/src/renderer/locales/browser-copy.ts
@@ -45,7 +45,7 @@ export type BrowserCopy = {
};
const BROWSER_COPY = {
- zh: {
+ 'zh-CN': {
unsupportedScheme: '嵌入式浏览器只支持打开 HTTP/HTTPS 网页地址。',
invalidUrl: '这个地址无法识别,请检查网址后重试。',
openFailed: '无法打开地址',
@@ -69,6 +69,30 @@ const BROWSER_COPY = {
title: '嵌入式浏览器',
description: '输入网址打开页面,或让助手帮你导航并操作。',
},
+ 'zh-TW': {
+ unsupportedScheme: '嵌入式瀏覽器只支援開啟 HTTP/HTTPS 網頁地址。',
+ invalidUrl: '這個地址無法識別,請檢查網址後重試。',
+ openFailed: '無法開啟地址',
+ navigationFailed: '瀏覽器導航失敗',
+ navigationFailedDetail: '頁面暫時無法開啟,請稍後重試。',
+ panelAria: '嵌入式瀏覽器',
+ panelAriaWithTitle: (title) => `嵌入式瀏覽器:${title}`,
+ insecure: '這個站點用 HTTP 傳輸,連線未加密。',
+ backAria: '瀏覽器後退',
+ back: '後退',
+ forwardAria: '瀏覽器前進',
+ forward: '前進',
+ stopAria: '停止載入頁面',
+ refreshAria: '重新整理頁面',
+ stop: '停止',
+ refresh: '重新整理',
+ addressAria: '瀏覽器地址',
+ addressPlaceholder: '輸入網址並回車',
+ closeAria: '關閉瀏覽器頁面',
+ close: '關閉頁面',
+ title: '嵌入式瀏覽器',
+ description: '輸入網址開啟頁面,或讓助手幫你導航並操作。',
+ },
en: {
unsupportedScheme: 'The embedded browser only supports HTTP and HTTPS addresses.',
invalidUrl: 'This address is not valid. Check it and try again.',
diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts
index 797dda1f15..b4fef1cfa2 100644
--- a/apps/desktop/src/renderer/locales/conversation-copy.ts
+++ b/apps/desktop/src/renderer/locales/conversation-copy.ts
@@ -457,7 +457,7 @@ function enDetail(parts: readonly string[]): string {
}
const COPY = {
- zh: {
+ 'zh-CN': {
actions: { stopFailedTitle: '停止失败', stopFailedFallback: '任务操作失败,请稍后重试。', refreshSessionsFailedTitle: '刷新任务列表失败', refreshSessionsFailedFallback: '刷新任务列表失败,请稍后重试。', conversationErrorTitle: '任务出错', conversationErrorFallback: '任务运行失败,请稍后重试。', regenerateStartedTitle: '已发起重新生成', regenerateStartedDescription: '正在生成新的一轮回答', branchCreatedTitle: '已创建分支', branchCreatedDescription: (name) => `新任务 ${name}`, revisionStartedTitle: '已创建修改版草稿', revisionStartedDescription: '原任务仍会保留;修改后发送将在新版本中继续', rev [...]
attachments: { tooMany: '附件数量超过 8 个', tooLarge: '附件大小超过 50MB', duplicate: '附件来源重复,请勿重复添加同一文件。' },
model: {
@@ -711,6 +711,251 @@ const COPY = {
},
turnError: { unknown: '出错了,原因不明。重新发消息重试。', contextOverflow: '上下文超出模型窗口限制,减少附件或开启新任务。', timeout: '模型请求超时,重新发消息重试。', auth: '模型鉴权失败,请到设置里重新连接或登录。', providerBilling: '模型服务计费受限,请检查账号余额或订阅状态。', providerCapacity: '模型服务暂时满载,等几分钟重试,或换一个模型。', rateLimit: '模型请求太频繁被限流了,等一会儿再发消息重试。', network: '网络连接失败,检查网络后重新发消息。', provider: '模型服务返回错误,稍后重试或换一个模型。', stepCap: '达到工具调用步数上限,任务可能没做完。发消息让它继续。', tool: '工具调用失败,看一下上面的工具结果再决定要不要重试。', permission: '这一轮在等权限确认时结束了,重新发消息会再问一次。', restarted: '本地应�
��重启,上一轮没有完成', sandbo [...]
},
+ 'zh-TW': {
+ actions: { stopFailedTitle: '停止失敗', stopFailedFallback: '任務操作失敗,請稍後重試。', refreshSessionsFailedTitle: '重新整理任務列表失敗', refreshSessionsFailedFallback: '重新整理任務列表失敗,請稍後重試。', conversationErrorTitle: '任務出錯', conversationErrorFallback: '任務執行失敗,請稍後重試。', regenerateStartedTitle: '已發起重新生成', regenerateStartedDescription: '正在生成新的一輪迴答', branchCreatedTitle: '已建立分支', branchCreatedDescription: (name) => `新任務 ${name}`, revisionStartedTitle: '已建立修改版草稿', revisionStartedDescription: '原任務仍會保留;修改後傳送將在新版本中繼續', [...]
+ attachments: { tooMany: '附件數量超過 8 個', tooLarge: '附件大小超過 50MB', duplicate: '附件來源重複,請勿重複新增同一檔案。' },
+ model: {
+ fakeBackendLabel: '本地模擬連線',
+ setupTitle: '等待設定真實模型',
+ connectionMissingTitle: '連線已刪除',
+ configurationFallback: '模型連線暫時無法用於傳送,請到 設定 · 模型 檢查後重試。',
+ configurationReason: {
+ missing_default_connection: '等待設定預設模型。請到 設定 · 模型 新增一個可用模型連線後再發送。',
+ connection_missing: '該任務依賴的模型連線已刪除,請到 設定 · 模型 重新選擇或重建連線。',
+ connection_disabled: '目前模型連線已停用。請到 設定 · 模型 啟用或選擇其他預設模型。',
+ missing_api_key: '目前模型連線還沒有可用憑據。請到 設定 · 模型 補齊 API key 或重新登入後再發送。',
+ missing_model: '目前模型連線還沒有可用模型。請到 設定 · 模型 選擇預設模型後再發送。',
+ empty_model_list: '目前模型連線沒有啟用模型。請到 設定 · 模型 新增或啟用模型後再發送。',
+ model_not_enabled: '目前任務選擇的模型未啟用。請到 設定 · 模型 重新選擇可用模型後再發送。',
+ model_not_chat_capable: '目前任務選擇的模型不能用於聊天。請到 設定 · 模型 重新選擇支援聊天的模型後再發送。',
+ fake_backend: '目前任務來自舊的本地模擬連線。請到 設定 · 模型 新增真實模型後建立任務。',
+ provider_retired: '目前任務繫結的連線,其登入方式已從 Maka 移除,無法用於傳送。請到 設定 · 模型 改用其他連線後建立任務。',
+ },
+ },
+ footer: { labels: { regenerate: '重新生成', branch: '分支', copy: '複製', info: '詳情' }, pending: '正在處理…', regenerateRunning: '目前回答仍在進行中,結束後再重新生成', regenerateAgain: '已重新生成過,再次點選將建立新的並行回答', requestRegenerate: '請求擁有者核准重新生成本輪回答', regenerate: '讓模型重新生成本輪迴答', branchRunning: '目前回答仍在進行中,結束後再分支', branchAborted: '從中斷前的上下文分支出新任務', branch: '基於此回答的上下文分支出新任務', copy: '複製回答到剪貼簿', copyEmpty: '此回答尚無可複製的內容' },
+ lineage: { regeneratedFrom: '重新生成自舊回答', regeneratedFromTooltip: '這是重新生成的並行回答,點選檢視被保留的舊回答', regeneratedTo: '已重新生成 → 新回答', regeneratedToTooltip: '點選跳轉到重新生成的新回答' },
+ workbar: {
+ ariaLabel: '任務工作欄',
+ sectionsAriaLabel: '任務工作欄標籤',
+ review: '變更',
+ terminal: '終端',
+ terminalNumbered: (index) => `終端 ${index}`,
+ tasks: '待辦',
+ todoLoadFailed: '待辦載入失敗,請重試。',
+ workBoard: '工作看板',
+ browser: '瀏覽器',
+ files: '生成檔案',
+ inspector: '追蹤',
+ sideChat: '側邊對話',
+ sideChatNumbered: (index) => `側邊對話 ${index}`,
+ openTab: '開啟工作欄標籤',
+ openTools: '開啟工具',
+ closeTab: (label) => `關閉${label}`,
+ tabMenu: (label) => `${label}標籤選單`,
+ moveLeft: '向左移動',
+ moveRight: '向右移動',
+ moveToRight: '移動到右側面板',
+ moveToBottom: '移動到底部面板',
+ pinTab: '固定標籤',
+ pinTabHint: '預覽標籤,雙擊或在內容中操作即可固定',
+ close: '關閉',
+ closeOthers: '關閉其他標籤',
+ closeToRight: '關閉右側標籤',
+ launcher: {
+ review: '檢視目前 Git 工作區變化',
+ terminal: '檢視目前任務的終端執行和即時輸出',
+ tasks: '檢視和維護這個任務的待辦臺賬',
+ workBoard: '記錄和管理暫緩事項',
+ browser: '開啟內建瀏覽器並保留目前頁面',
+ files: '瀏覽目前任務生成的檔案',
+ inspector: '檢查任務呼叫、工具與耗時記錄',
+ sideChat: '在不打斷主任務的情況下追問和只讀探索',
+ },
+ },
+ workBoardPanel: {
+ inbox: 'Inbox',
+ project: '目前專案',
+ noProject: '未選擇專案',
+ createPlaceholder: '記錄稍後處理的事項…',
+ create: '新增',
+ empty: '暫無暫緩事項',
+ loading: '正在載入工作看板…',
+ loadMore: '載入更多',
+ retry: '重試',
+ loadFailed: '工作看板載入失敗',
+ actionFailed: '操作失敗',
+ complete: '完成',
+ reopen: '重開',
+ rename: '改名',
+ renameSave: '儲存',
+ moveToInbox: '移到 Inbox',
+ moveToProject: '移到專案',
+ archive: '歸檔',
+ unarchive: '恢復',
+ delete: '刪除',
+ archived: '已歸檔',
+ },
+ reviewPanel: {
+ ariaLabel: 'Git 變更',
+ empty: '目前 Git 工作區沒有變化',
+ emptyHelp: '提交、暫存或修改檔案後,變化會顯示在這裡。',
+ notGitRepository: '目前任務目錄不是 Git 倉庫',
+ workspaceUnavailable: '目前任務目錄已不可用',
+ unbornRepository: 'Git 倉庫還沒有可比較的提交',
+ gitFailed: '無法讀取 Git 工作區變化',
+ invalidBaseBranch: '選擇的比較分支已不可用',
+ truncated: '變化過多,僅顯示前一部分檔案',
+ showMore: (remaining) => `再顯示 ${Math.min(20, remaining)} 個檔案`,
+ hiddenLines: (count) => `另有 ${count} 行未顯示`,
+ changedFiles: (count) => `${count} 個檔案有變更`,
+ addedLines: (count) => `新增 ${count} 行`,
+ deletedLines: (count) => `刪除 ${count} 行`,
+ added: (count) => `新增 ${count}`,
+ deleted: (count) => `刪除 ${count}`,
+ loadFailed: '無法讀取 Git 變化',
+ retry: '重試',
+ },
+ terminalPanel: {
+ ariaLabel: '任務終端',
+ empty: '目前任務還沒有終端執行',
+ emptyHelp: '任務啟動終端後會顯示在這裡。',
+ loadFailed: '無法讀取終端執行',
+ retry: '重試',
+ refresh: '重新整理終端',
+ readOnly: '顯示代理和你在目前任務中啟動的終端執行',
+ runCount: (count) => `${count} 個終端執行`,
+ newTerminal: '建立終端',
+ commandPlaceholder: '輸入命令並回車',
+ commandLabel: '終端命令',
+ runCommand: '執行命令',
+ stopTerminal: '停止目前終端',
+ startFailed: '無法啟動終端',
+ writeFailed: '無法傳送終端輸入',
+ stopFailed: '無法停止終端',
+ },
+ inspector: {
+ ariaLabel: '任務追蹤',
+ copyPricingKey: '複製定價鍵',
+ pricingKeyCopied: '已複製定價鍵',
+ unpricedPricingKey: '未計價的定價鍵',
+ copyFailed: '複製失敗',
+ copyFailedDetail: '剪貼簿不可用或被系統拒絕。',
+ loadFailed: '追蹤讀取失敗',
+ retry: '重試',
+ empty: '這個任務還沒有可追蹤的活動',
+ emptyHelp: '任務尚無活動記錄。',
+ costUnavailable: '費用未知',
+ costEstimateHelp: '基於已記錄用量和定價估算;缺失或未定價的呼叫可能未計入。',
+ loadEarlier: '載入更早記錄',
+ hideEarlier: '隱藏所有更早記錄',
+ loadingEarlier: '正在載入…',
+ loadingTrace: '正在讀取時間線…',
+ loadingSummary: '正在估算完整會話用量…',
+ summaryUnavailable: '完整會話用量暫時無法估算。',
+ totals: {
+ cost: '估算成本',
+ },
+ tokenUsage: {
+ title: 'Token 統計',
+ segment: {
+ cacheRead: '快取輸入',
+ cacheMiss: '未命中輸入',
+ output: '輸出(含思考)',
+ },
+ },
+ durationUsage: {
+ title: '耗時統計',
+ center: '記錄時長',
+ segment: {
+ model: (count) => `LLM 呼叫 × ${count}`,
+ tool: (count) => `工具執行 × ${count}`,
+ },
+ },
+ coveragePartial: (parts) => `部分呼叫未能完整顯示,下面的數字只少不多${zhDetail(parts)}`,
+ coverageAbsent: (parts) => `這個後端不記錄每次呼叫的明細${zhDetail(parts)}`,
+ unreadable: (count) => `${count} 條記錄讀不出來`,
+ oversizedRuns: (count) => `${count} 條執行記錄過大,無法線上顯示`,
+ turnsMissing: (count) => `${count} 輪沒有呼叫記錄`,
+ turnsShort: (count) => `${count} 輪的呼叫記錄不全`,
+ stepKind: { permission: '權限', compaction: '上下文壓縮', error: '錯誤' },
+ callKind: (kind) => ZH_CALL_KIND[kind as keyof CallKindCopy] ?? kind,
+ permissionDecision: (decision) => ZH_PERMISSION_DECISION[decision] ?? decision,
+ recoveredAs: (disposition) => `已恢復:${ZH_RECOVERED[disposition] ?? disposition}`,
+ retries: (count) => `重試 ${count} 次`,
+ turnFailure: (code) => ZH_TURN_FAILURE[code] ?? '本輪失敗',
+ turnLabel: (startedAt) => `輪次 · ${startedAt}`,
+ overview: {
+ context: '上下文視窗',
+ segment: {
+ cacheRead: '快取命中',
+ fresh: '快取未命中',
+ used: '已佔用',
+ free: '剩餘',
+ },
+ cacheHit: '快取命中率',
+ timelineTab: '時間軸',
+ composition: {
+ title: '構成估算',
+ basis: '按請求位元組估算,非模型報告的 token',
+ part: {
+ system_instructions: '系統提示',
+ tool_definitions: '工具定義',
+ messages: '對話記錄',
+ other: '其他引數',
+ },
+ tools: '按工具',
+ remainingTools: (count) => `其餘 ${count} 個工具`,
+ unlabelled: '未命名的工具',
+ unrecorded: '這次呼叫沒有留下構成記錄',
+ },
+ },
+ },
+ quoteCompanion: {
+ compactStartedTitle: '正在壓縮上下文', compactStartedDescription: '正在將較早的上下文整理為檢查點摘要。', compactSuccessTitle: '上下文已壓縮', compactSuccessDescription: '較早的上下文已替換為檢查點摘要。', compactUnchangedTitle: '無需壓縮', compactUnchangedDescription: '任務已使用最新的檢查點。', compactErrorTitle: '壓縮失敗', compactErrorFallback: '任務暫時無法壓縮,請稍後重試。', workspaceUnavailableTitle: '工作目錄無法使用', workspaceUnavailableDescription: '工作目錄不存在或無法存取。請選擇有效目錄建立新任務。',
+ namePrefix: '側聊:',
+ permissionStreaming: '側邊對話執行中暫時不能更改權限',
+ scrollToBottom: '滾動側邊對話到底部',
+ closeConfirmation: {
+ title: (count) => count > 1 ? `關閉 ${count} 個側邊對話?` : '關閉側邊對話?',
+ description: (count) =>
+ count > 1
+ ? `這 ${count} 個臨時側邊對話會被永久刪除,之後無法恢復。`
+ : '這個臨時側邊對話會被永久刪除,之後無法恢復。',
+ dontAskAgain: '以後不再詢問',
+ cancel: '取消',
+ confirm: '關閉側邊對話',
+ },
+ errors: {
+ forkSetupFailed: '無法建立側邊對話,請稍後重試。',
+ forkSourceBusy: '主對話或子任務仍在執行,請等待完成後重試。',
+ forkUnsupported: '目前對話上下文暫不支援建立側邊對話。',
+ sendRejected: '追問未能開始,請稍後重試。',
+ sendFailed: '追問失敗,請稍後重試。',
+ settlementFailed: '執行已結束,但訊息載入失敗。請重試或重新開啟側邊對話。',
+ respondFailed: '回應失敗,請稍後重試。',
+ },
+ },
+ health: {
+ blocked: {
+ fake_backend: { label: '任務已過期 · 請先設定真實模型', tooltip: () => '原任務使用舊的本地模擬連線,需要先到 設定 · 模型 新增並啟用一個真實模型才能傳送。' },
+ provider_retired: { label: '登入方式已停用', tooltip: (name) => `任務繫結的連線 "${name}" 使用的登入方式已從 Maka 移除,傳送會失敗。請到 設定 · 模型 改用其他連線。` },
+ missing_default_connection: { label: '未設定可用模型', tooltip: () => '目前任務沒有可用的模型連線,傳送會失敗。請到 設定 · 模型 新增並啟用一個模型。' },
+ legacy_connection_identity: { label: '需要選擇模型連線', tooltip: () => '此任務來自舊版本,請選擇要使用的連線和模型。', actionLabel: '選擇連線和模型', settingsTooltip: () => '目前沒有可用連線,請先到 設定 · 模型 新增或啟用連線。' },
+ connection_missing: { label: '連線已刪除', tooltip: () => '此任務依賴的模型連線已被刪除,傳送會失敗。請到 設定 · 模型 檢查連線設定。' },
+ connection_identity_mismatch: { label: '連線身分不符', tooltip: () => '請重新選擇要使用的連線和模型。', actionLabel: '選擇連線和模型', settingsTooltip: () => '目前沒有可用連線,請先到 設定 · 模型 新增或啟用連線。' },
+ connection_disabled: { label: '連線已停用', tooltip: (name) => `任務繫結的連線 "${name}" 已停用,傳送會失敗。請到 設定 · 模型 啟用它或選擇其他連線。` },
+ missing_api_key: { label: '連線缺少金鑰', tooltip: (name) => `連線 "${name}" 未填寫 API key 或未完成登入,傳送會失敗。請到 設定 · 模型 補齊憑據。` },
+ missing_model: { label: '連線未選擇模型', tooltip: (name) => `連線 "${name}" 沒有預設模型,傳送會失敗。請到 設定 · 模型 選擇一個模型。` },
+ empty_model_list: { label: '連線沒有啟用模型', tooltip: (name) => `連線 "${name}" 沒有啟用任何模型,傳送會失敗。請到 設定 · 模型 先新增模型。` },
+ model_not_enabled: { label: '任務模型未啟用', tooltip: (name, model) => `模型 "${model}" 不在連線 "${name}" 的啟用列表中,傳送會失敗。請到 設定 · 模型 重新選擇。` },
+ model_not_chat_capable: { label: '任務模型不支援聊天', tooltip: (name, model) => `模型 "${model}" 不能用於聊天,傳送會失敗。請到 設定 · 模型 選擇支援聊天的模型。` },
+ },
+ connectionChoicesLoading: { tooltip: '連線清單尚未載入完成。', actionLabel: '重新載入連線' },
+ reauth: { label: '上次連線測試鑑權失敗', tooltip: '最近一次連線測試回傳鑑權失敗(401 / 403),金鑰可能已過期或被吊銷。這不會攔截發送,但若傳送失敗請到 設定 · 模型 重新登入。' },
+ testError: { label: '上次連線測試失敗', tooltip: '最近一次連線測試因網路 / 超時 / 5xx 失敗。這不會攔截發送,但若問題持續請到 設定 · 模型 檢查 Base URL / 代理。' },
+ },
+ turnError: { unknown: '出錯了,原因不明。重新傳送訊息重試。', contextOverflow: '上下文超出模型視窗限制,減少附件或開啟新任務。', timeout: '模型請求逾時,重新傳送訊息重試。', auth: '模型鑑權失敗,請到設定裡重新連線或登入。', providerBilling: '模型服務計費受限,請檢查帳號餘額或訂閱狀態。', providerCapacity: '模型服務暫時滿載,請等待幾分鐘或切換模型。', rateLimit: '模型請求太頻繁而受到速率限制,請稍候再傳送訊息重試。', network: '網路連線失敗,檢查網路後重新傳送訊息。', provider: '模型服務回傳錯誤,稍後重試或切換模型。', stepCap: '達到工具呼叫步數上限,任務可能尚未完成。傳送訊息讓它繼續。', tool: '工具呼叫失敗,先看上面的工具結果再決定是否重試。', permission: '這一輪在等待權限確認時結束,重新傳送訊息會再詢問一次。', restart
ed: '本機應用程式重啟,上一輪沒有完成', [...]
+ },
en: {
actions: { stopFailedTitle: 'Failed to stop', stopFailedFallback: 'The task action failed. Try again later.', refreshSessionsFailedTitle: 'Failed to refresh tasks', refreshSessionsFailedFallback: 'The task list could not be refreshed. Try again later.', conversationErrorTitle: 'Task error', conversationErrorFallback: 'The task run failed. Try again later.', regenerateStartedTitle: 'Regeneration started', regenerateStartedDescription: 'Generating a new response', branchCreatedTitle: ' [...]
attachments: { tooMany: 'You can attach at most 8 files', tooLarge: 'Attachments must be 50 MB or smaller', duplicate: 'This attachment was already added.' },
diff --git a/apps/desktop/src/renderer/locales/external-session-import-copy.ts b/apps/desktop/src/renderer/locales/external-session-import-copy.ts
index 76e6265918..48cb7a829c 100644
--- a/apps/desktop/src/renderer/locales/external-session-import-copy.ts
+++ b/apps/desktop/src/renderer/locales/external-session-import-copy.ts
@@ -94,7 +94,7 @@ type ExternalSessionImportCopy = {
};
const COPY = {
- zh: {
+ 'zh-CN': {
sourceLabel: '来源',
sourceNames: { codex: 'Codex', 'claude-code': 'Claude Code' },
includeArchived: '包含已归档的对话',
@@ -150,6 +150,62 @@ const COPY = {
importOutcomeUnknownDescription: (names) =>
`以下对话的导入结果无法确认:${names.map((name) => `「${name}」`).join('、')}。请先在任务列表中查找,已经出现的不要再次导入。`,
},
+ 'zh-TW': {
+ sourceLabel: '來源',
+ sourceNames: { codex: 'Codex', 'claude-code': 'Claude Code' },
+ includeArchived: '包含已歸檔的對話',
+ searchLabel: '搜尋',
+ searchHelp: '符合對話標題與專案路徑。留空顯示全部。',
+ searchPlaceholder: '標題或路徑的一部分',
+ searchEmpty: (term) => `沒有標題或路徑包含「${term}」的對話。`,
+ loading: '正在讀取外部對話…',
+ listAria: '可匯入的對話',
+ emptyTitle: '沒有可匯入的對話',
+ emptyDescription: '目前來源中沒有找到符合條件的根對話。',
+ unavailableTitle: '沒有檢測到支援的 Agent',
+ // The title already says nothing was detected, so this says what to do
+ // about it instead of saying it again. It names Codex because the renderer
+ // only ever learns which sources *were* detected — nothing but a copy
+ // string can tell someone with none what to go install. The second half is
+ // the promise that earns the permission to read another app's files.
+ unavailableDescription: '在本機使用過 Codex 後,它的對話會出現在這裡。Maka 只讀取這些檔案,不會修改。',
+ loadFailedTitle: '無法讀取外部對話',
+ loadFailedFallback: '外部對話目錄暫時無法讀取,請重試。',
+ retry: '重試',
+ archived: '已歸檔',
+ loadMore: '載入更多',
+ loadingMore: '正在載入…',
+ duplicateNote: '再次匯入同一個對話會建立一個獨立的任務。',
+ importedCount: (count) => `已匯入 ${count} 次`,
+ openLatestImportedTask: '開啟最近匯入的任務',
+ openLatestImportedTaskFor: (name) => `開啟「${name}」最近匯入的任務`,
+ import: '匯入',
+ importAgain: '再次匯入',
+ importTask: (name) => `匯入「${name}」`,
+ importTaskAgain: (name) => `再次匯入「${name}」`,
+ importing: '正在匯入…',
+ importingTask: (name) => `正在匯入「${name}」`,
+ importInProgressTitle: '正在匯入',
+ importInProgressDescription: (name) => `正在匯入「${name}」,完成後會直接開啟這個任務。`,
+ importFailedTitle: '匯入失敗',
+ importFailedFallback: '該對話無法轉換或儲存。請檢查來源後重試。',
+ importRecoveredTitle: '已確認匯入',
+ importRecoveredDescription: (name) => `「${name}」匯入的任務現已可用。`,
+ importNotRecordedTitle: '沒有發現新任務',
+ importNotRecordedDescription: '沒有記錄到新的任務,可以安全重試。',
+ importOutcomeUnknownTitle: '需要確認匯入結果',
+ selectAllAriaLabel: '全選或全部取消選取',
+ selectedCount: (selected, listed) => `已選 ${selected} / ${listed}`,
+ selectRowAriaLabel: (name) => `選取 ${name}`,
+ importSelected: '匯入所選項目',
+ batchProgress: (done, total) => `正在匯入 ${done} / ${total}`,
+ batchDoneTitle: (imported) => `已匯入 ${imported} 個對話`,
+ batchDuplicated: (count) => `其中 ${count} 個先前已匯入,現在各有兩份。`,
+ batchFailed: (count) => `另有 ${count} 個無法匯入。`,
+ batchNothingImported: '沒有匯入任何對話。',
+ importOutcomeUnknownDescription: (names) =>
+ `以下對話的匯入結果無法確認:${names.map((name) => `「${name}」`).join('、')}。請先在任務列表中查詢,已經出現的不要再次匯入。`,
+ },
en: {
sourceLabel: 'Source',
sourceNames: { codex: 'Codex', 'claude-code': 'Claude Code' },
diff --git a/apps/desktop/src/renderer/locales/mcp-copy.ts b/apps/desktop/src/renderer/locales/mcp-copy.ts
index 3d6c41275a..8eb2ad8aaf 100644
--- a/apps/desktop/src/renderer/locales/mcp-copy.ts
+++ b/apps/desktop/src/renderer/locales/mcp-copy.ts
@@ -71,7 +71,7 @@ export type McpCopy = {
};
const MCP_COPY = {
- zh: {
+ 'zh-CN': {
errors: {
load: '载入 MCP 失败', install: (name) => `安装 ${name} 失败`, cancelInstall: (name) => `取消安装 ${name} 失败`, save: '保存 MCP 失败',
import: '导入 MCP 失败', update: '更新 MCP 失败', test: 'MCP 测试失败', remove: '删除 MCP 失败', unavailableStatus: 'Server 没有返回可用状态。',
@@ -130,6 +130,65 @@ const MCP_COPY = {
stdioProtocolHelp: '自动协商和“仅 2026-07-28”会先启动一个使用相同命令、参数、目录和环境的短期探测进程;探测结束后才启动实际连接。旧配置默认使用传统协议,只启动一个进程。',
},
},
+ 'zh-TW': {
+ errors: {
+ load: '載入 MCP 失敗', install: (name) => `安裝 ${name} 失敗`, cancelInstall: (name) => `取消安裝 ${name} 失敗`, save: '儲存 MCP 失敗',
+ import: '匯入 MCP 失敗', update: '更新 MCP 失敗', test: 'MCP 測試失敗', remove: '刪除 MCP 失敗', unavailableStatus: 'Server 沒有返回可用狀態。',
+ mapLine: (line) => `第 ${line} 行應為 KEY=value`, importJson: 'MCP 設定必須是有效的 JSON', importObject: 'MCP JSON 必須是 object',
+ importVersion: (version) => `不支援 MCP 設定版本 ${version},目前支援 version 1、2 和 3`, importServersObject: 'mcpServers 必須是 object',
+ importProtocolVersion: 'remote 的 protocol 需要 version 2 或 3;stdio 的 protocol 需要 version 3',
+ },
+ toast: {
+ templateInstalled: (name) => `${name} 模板已安裝`, templateInstalledDetail: '請在「已安裝」中完成憑據設定,再啟用連線。',
+ installed: (name) => `${name} 已安裝`, installedDetail: '發現的工具會從下一次 agent turn 開始生效。', installCancelled: (name) => `已取消安裝 ${name}`,
+ saved: 'MCP 已儲存', savedDetail: '新工具會從下一次 agent turn 開始生效。', imported: '已匯入 MCP',
+ importedDetail: (count) => `本次匯入 ${count} 個 server。`, connectionOk: 'MCP 連線正常',
+ toolLatency: (count, latencyMs) => `${count} 個工具 · ${latencyMs} ms`, connectionFailed: 'MCP 連線失敗', removed: 'MCP 已刪除',
+ },
+ remove: { title: (id) => `刪除 MCP「${id}」?`, description: '它提供的工具會從下一次 agent turn 中移除,設定無法自動恢復。', confirm: '刪除', cancel: '取消' },
+ page: {
+ actionsAria: 'MCP 操作', refreshing: '重新整理中…', refresh: '重新整理', add: '新增 MCP',
+ metaInstalled: (count) => `${count} 個已安裝`, metaErrors: (count) => `${count} 個連線異常`,
+ searchMatches: (count) => `${count} 個符合`,
+ workspaceAria: 'MCP 市場與已安裝項', toolbarAria: 'MCP 瀏覽操作', setupTitle: '把 Maka 連線到你的工作環境', setupDescription: '從精選模板開始,或新增任意 stdio、Streamable HTTP 與 SSE server。',
+ localStdio: '本地 stdio', categoriesAria: 'MCP 分類', market: '市場', installed: '已安裝',
+ searchPlaceholder: '搜尋 MCP…', searchAria: '搜尋 MCP', noMarket: '沒有找到符合的 MCP', noMarketDetail: (query) => `換一個關鍵詞,或清空「${query}」檢視全部模板。`,
+ clearSearch: '清空搜尋', loading: '正在讀取 MCP 設定…', noInstalled: '還沒有安裝 MCP', noInstalledDetail: '從市場選擇模板,或手動新增你自己的 server。',
+ browseMarket: '瀏覽市場', noInstalledMatch: '沒有符合的已安裝 MCP', noInstalledMatchDetail: (query) => `換一個關鍵詞,或清空「${query}」檢視全部已安裝項。`,
+ },
+ detail: {
+ label: '伺服器詳情', enabled: '啟用', transport: '傳輸方式', endpoint: '端點',
+ toolsLabel: '工具', statusLabel: '狀態', protocolLabel: 'MCP 協議',
+ negotiatedProtocol: (era, revision) => `${era === 'modern' ? '現代' : '傳統'} · ${revision}`,
+ inspectorOpened: (id) => `已開啟 ${id} 的詳情`,
+ },
+ card: {
+ macOnly: '僅 macOS', manage: '管理', cancellingAria: (name) => `正在取消安裝 ${name}`, cancelAria: (name) => `取消安裝 ${name}`, installAria: (name) => `安裝 ${name}`,
+ cancelling: '正在取消…', cancel: '取消安裝', install: '安裝',
+ },
+ row: {
+ testing: '測試中…', test: '測試', edit: '編輯',
+ delete: '刪除', tools: (count) => `${count} 個工具`,
+ disabled: '已停用', disconnected: '未連線', connecting: '連線中', connected: (count) => `${count} 個工具`, failed: '連線失敗',
+ },
+ editor: {
+ importTitle: '透過 JSON 匯入', editTitle: (id) => `編輯 ${id}`, addTitle: '新增 MCP', importSubtitle: '貼上 mcpServers 設定,同名 server 會被更新。',
+ manualSubtitle: '設定儲存在目前工作區的 mcp.json。', modeAria: 'MCP 新增方式', manual: '手動設定', pasteJson: '貼上 JSON', jsonConfig: 'JSON 設定',
+ jsonHelp: '支援完整 mcpServers 設定或直接的 server map。未在本次匯入中出現的已有 MCP 會保留。', cancel: '取消', importConnect: '匯入並連線',
+ transportAria: '連線方式', localStdio: '本地 stdio', remoteUrl: '遠端 URL',
+ serverId: '伺服器 ID', command: '命令',
+ commandPlaceholder: 'npx -y @modelcontextprotocol/server-filesystem /path/to/folder',
+ commandHelp: '完整命令列;含空格的引數用引號包裹,不經過 shell 解析。',
+ workingDirectory: '工作目錄', workingDirectoryPlaceholder: '可選,例如 /path/to/project',
+ environment: '環境變數', environmentHelp: '每行一個 KEY=value;按 MCP 要求填寫。', url: 'MCP URL', headers: 'HTTP 請求頭', headersHelp: '每行一個 Header=value。',
+ saveConnect: '儲存並連線',
+ required: '此欄位為必填項。', invalidUrl: '請輸入有效的 HTTP 或 HTTPS URL。', unbalancedQuote: '引號未閉合。',
+ transportLabel: '傳輸協議', transportAuto: '自動回退', transportStreamableHttp: 'Streamable HTTP', transportLegacySse: '舊版 SSE',
+ protocolLabel: '協議偏好', protocolLegacy: '傳統', protocolAuto: '自動協商', protocolModern: '僅 2026-07-28',
+ protocolHelp: '舊設定預設使用傳統協議;自動協商會根據 server 能力選擇協議。', sseProtocolHelp: '舊版 SSE 僅支援傳統協議。', expandAdvanced: '顯示進階設定', collapseAdvanced: '隱藏進階設定',
+ stdioProtocolHelp: '自動協商和“僅 2026-07-28”會先啟動一個使用相同命令、引數、目錄和環境的短期探測程序;探測結束後才啟動實際連線。舊設定預設使用傳統協議,只啟動一個程序。',
+ },
+ },
en: {
errors: {
load: 'Failed to load MCP', install: (name) => `Failed to install ${name}`, cancelInstall: (name) => `Failed to cancel installation of ${name}`, save: 'Failed to save MCP',
diff --git a/apps/desktop/src/renderer/locales/onboarding-copy.ts b/apps/desktop/src/renderer/locales/onboarding-copy.ts
index e68a6243d4..f408b0ab3b 100644
--- a/apps/desktop/src/renderer/locales/onboarding-copy.ts
+++ b/apps/desktop/src/renderer/locales/onboarding-copy.ts
@@ -51,7 +51,7 @@ export interface OnboardingCatalog {
}
const ONBOARDING_COPY_BY_LOCALE: UiCatalog<OnboardingCatalog> = {
- zh: {
+ 'zh-CN': {
hero: {
needs_connection: {
eyebrow: '欢迎使用 Maka',
@@ -100,6 +100,55 @@ const ONBOARDING_COPY_BY_LOCALE: UiCatalog<OnboardingCatalog> = {
skip: '跳过引导',
snapshotErrorFallback: '首次使用状态暂时不可用,请稍后重试。',
},
+ 'zh-TW': {
+ hero: {
+ needs_connection: {
+ eyebrow: '歡迎使用 Maka',
+ title: '串接一個 AI,開始第一項任務。',
+ body: 'Maka 在本地執行,使用你自己的模型帳號或 API key。先選一個常用服務商。',
+ cta: { label: '瀏覽全部服務商' },
+ },
+ needs_connection_credentials: {
+ eyebrow: '繼續設定連線',
+ title: '補齊這個連線的憑據。',
+ body: '新增 API key 或完成帳號登入,Maka 才能呼叫模型。',
+ cta: { label: '設定連線憑據' },
+ },
+ needs_model: {
+ eyebrow: '繼續設定連線',
+ title: '為這個連線啟用一個可用模型。',
+ body: '啟用一個可用於對話的模型,新任務就可以開始了。',
+ cta: { label: '選擇可用模型' },
+ },
+ 'blocked:all_connections_unhealthy': {
+ eyebrow: '連線需要處理',
+ title: '模型連線暫時不可用。',
+ body: '現有連線都沒有透過驗證。檢查憑據、登入狀態或網路後重新測試。',
+ cta: { label: '修復模型連線' },
+ tone: 'destructive',
+ },
+ 'blocked:all_connections_retired': {
+ eyebrow: '連線需要處理',
+ title: '現有連線的登入方式已停用。',
+ body: '這些連線使用的登入方式已從 Maka 移除,無法再登入,也無法用於對話。新增一個新的模型連線即可繼續。',
+ cta: { label: '新增模型連線' },
+ tone: 'destructive',
+ },
+ },
+ needsConnection: {
+ pickLabel: '選擇常用服務商',
+ browseProviders: '瀏覽全部服務商',
+ },
+ refresh: {
+ connection: '重新檢測連線',
+ credentials: '重新檢測憑據',
+ model: '重新檢測模型',
+ blocked: '重新檢測連線',
+ },
+ connectionLabel: '連線',
+ skip: '跳過引導',
+ snapshotErrorFallback: '首次使用狀態暫時不可用,請稍後重試。',
+ },
en: {
hero: {
needs_connection: {
diff --git a/apps/desktop/src/renderer/locales/permission-center-copy.ts b/apps/desktop/src/renderer/locales/permission-center-copy.ts
index 3a2e4713ea..91df593854 100644
--- a/apps/desktop/src/renderer/locales/permission-center-copy.ts
+++ b/apps/desktop/src/renderer/locales/permission-center-copy.ts
@@ -97,7 +97,7 @@ export type PermissionCenterCopy = {
};
const PERMISSION_CENTER_COPY = {
- zh: {
+ 'zh-CN': {
readiness: {
not_configured: { label: '等待配置', detail: '需要先打开开关或补齐配置才能启用。', tone: 'neutral' },
denied: { label: '系统拒绝', detail: '所需系统权限被拒绝或当前平台不支持。', tone: 'error' },
@@ -144,6 +144,53 @@ const PERMISSION_CENTER_COPY = {
auditSection: '审计记录', noAudit: '暂无审计记录', auditAria: (label) => `${label}审计记录列表`,
impact: '影响功能', opening: '打开中…', openSettings: '前往系统设置', requesting: '请求中…', request: '请求授权', dragGrant: '引导授权', dragGranting: '引导中…',
},
+ 'zh-TW': {
+ readiness: {
+ not_configured: { label: '等待設定', detail: '需要先開啟開關或補齊設定才能啟用。', tone: 'neutral' },
+ denied: { label: '系統拒絕', detail: '所需系統權限被拒絕或目前平臺不支援。', tone: 'error' },
+ enabled: { label: '執行可用', detail: '目前快照標記為可用,具體層級見下方。', tone: 'success' },
+ degraded: { label: '部分可用', detail: '已有一部分能力可用,但仍有執行態、權限或子功能需要處理。', tone: 'attention' },
+ paused: { label: '已暫停', detail: '功能開關被顯式關閉,但設定仍保留。', tone: 'neutral' },
+ },
+ osPermissions: {
+ accessibility: { label: '輔助功能', purpose: 'Computer Use 需要它來讀取視窗焦點 / 模擬鍵盤滑鼠。', impact: 'Computer Use · 自動化鍵鼠操作' },
+ screen_recording: { label: '螢幕錄製', purpose: 'Computer Use 需要它來讀取視窗內容;未來螢幕活動錄製也會使用。', impact: 'Computer Use · 截圖上下文' },
+ notifications: { label: '通知', purpose: '權限申請、回顧完成等系統通知需要它。', impact: '權限申請提醒 · 每日回顧完成通知' },
+ automation: { label: '自動化(Apple Events)', purpose: 'Computer Use 控制其他 App 需要逐 target 授權。', impact: 'Computer Use · 跨 App 自動化' },
+ },
+ osStates: {
+ unsupported: { label: '目前平臺不支援', tone: 'neutral' }, unknown: { label: '無法讀取狀態', tone: 'neutral' },
+ not_determined: { label: '等待授權', tone: 'attention' }, denied: { label: '已拒絕', tone: 'error' }, granted: { label: '已授權', tone: 'success' },
+ },
+ loading: '正在載入權限快照', readFailed: '無法讀取權限快照', noData: '權限服務未返回資料。', readAgain: '重新讀取',
+ actionFailed: '權限操作失敗',
+ actionFailures: {
+ invalid_id: '內部錯誤:權限 id 無法識別。',
+ unsupported_platform: '目前作業系統不支援這個權限操作。',
+ unsupported_permission: '目前平臺沒有提供這個權限的直串接口。',
+ denied: '你沒有授予這項權限;可以前往系統設定重新開啟。',
+ already_open: '另一個權限引導仍在進行,請先完成或關閉它。',
+ open_settings_failed: '無法開啟系統設定,請手動前往「隱私與安全性」。',
+ failed: '權限操作未成功,請稍後重試。',
+ },
+ title: '權限與能力', subtitle: '檢視 Maka 需要的系統權限和目前授權狀態,直接從這裡前往「系統設定 → 隱私與安全性」完成授權或撤銷,不必自己翻選單。',
+ lastRead: '最近讀取:', detectAgain: '重新檢測', summaryAria: '按授權狀態篩選系統權限', summaryFilterAria: (label, count, selected) => selected ? `${label} ${count} 項,目前篩選;再次按下顯示全部` : `僅顯示${label}權限,共 ${count} 項`, granted: '已授權', pending: '等待授權', denied: '已拒絕', other: '未知 / 不支援',
+ osSection: '系統權限', osSectionHelp: 'Maka 讀到的 OS 級權限狀態。點選右側按鈕可以直接前往「系統設定 → 隱私與安全性」對應分割槽。', osListAria: '系統權限列表',
+ capabilitiesSection: '功能能力', capabilitiesHelp: '每個能力的就緒狀態由「功能開關 · 設定 · 系統權限 · 執行態探測」共同決定。',
+ capabilityListAria: '功能能力列表',
+ footnote: 'Maka 不會自動授予 Accessibility、Automation 或 Screen Recording。高風險自動化能力必須保持逐項審批、可審計、可撤銷。這裡只讀取系統權限與功能能力的目前快照,授權變更仍需在「系統設定 → 隱私與安全性」完成。',
+ layers: {
+ aria: (label) => `${label}能力狀態明細`, feature: '功能開關', configuration: '設定', approval: '操作審批', memory: '記憶寫入', runtime: '執行態探測',
+ featureStates: { enabled: '已開啟', partial: '部分可用', disabled: '已關閉', not_available: '未開放' },
+ configurationStates: { not_required: '不需要設定', missing: '等待補齊設定', present: '已填寫' },
+ approvalStates: { not_required: '不需要審批', required_per_action: '每次呼叫都需審批', required_scoped_lease: '按目標與動作類別授權', pending: '審批掛起', approved: '目前任務已批准', denied: '目前任務已拒絕' },
+ memoryStates: { not_applicable: '不涉及記憶寫入', disabled: '記憶寫入已關閉', draft_required: '需要先草擬 memory 協議', accepted: '記憶寫入已接受' },
+ runtimeStates: { not_available: '尚無執行態探測', not_run: '探測未執行', healthy: '探測透過', degraded: '探測降級' },
+ },
+ requiredPermissions: '所需系統權限', requiredPermissionsAria: (label) => `${label}所需系統權限列表`, guidance: '處理建議', guidanceAria: (label) => `${label}處理建議列表`,
+ auditSection: '審計記錄', noAudit: '暫無審計記錄', auditAria: (label) => `${label}審計記錄列表`,
+ impact: '影響功能', opening: '開啟中…', openSettings: '前往系統設定', requesting: '請求中…', request: '請求授權', dragGrant: '引導授權', dragGranting: '引導中…',
+ },
en: {
readiness: {
not_configured: { label: 'Needs setup', detail: 'Enable the feature or complete its configuration first.', tone: 'neutral' },
diff --git a/apps/desktop/src/renderer/locales/plan-mode-copy.ts b/apps/desktop/src/renderer/locales/plan-mode-copy.ts
index df164c8bee..29f000c729 100644
--- a/apps/desktop/src/renderer/locales/plan-mode-copy.ts
+++ b/apps/desktop/src/renderer/locales/plan-mode-copy.ts
@@ -50,7 +50,7 @@ export interface PlanModeCopy {
}
const COPY = {
- zh: {
+ 'zh-CN': {
abandonConfirmation: {
title: '放弃这个计划?',
description: (title) => `“${title}”的执行记录会保留,但之后不能继续恢复。`,
@@ -69,6 +69,25 @@ const COPY = {
stepStatuses: { pending: '未开始', in_progress: '正在执行', completed: '已完成', skipped: '已跳过' },
},
},
+ 'zh-TW': {
+ abandonConfirmation: {
+ title: '放棄這個計劃?',
+ description: (title) => `“${title}”的執行記錄會保留,但之後不能繼續恢復。`,
+ confirm: '放棄計劃',
+ cancel: '取消',
+ },
+ proposal: {
+ aria: '計劃方案', kicker: '計劃方案', revision: '修訂版', steps: '執行步驟',
+ risks: '風險', revise: '繼續修改', execute: '執行計劃',
+ statuses: { pending_approval: '等待確認', approved: '已批准', stale: '已過期' },
+ },
+ execution: {
+ aria: '計劃執行狀態', interrupted: '計劃已中斷', running: '正在執行計劃',
+ approvedPlan: '已批准計劃', stepCount: (completed, total) => `${completed}/${total} 步`,
+ resume: '恢復執行', abandon: '放棄計劃',
+ stepStatuses: { pending: '未開始', in_progress: '正在執行', completed: '已完成', skipped: '已跳過' },
+ },
+ },
en: {
abandonConfirmation: {
title: 'Abandon this plan?',
diff --git a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts
index 4b2635b2bb..5c8b3d7036 100644
--- a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts
+++ b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts
@@ -19,7 +19,7 @@
import type { UiCatalog, UiLocale } from '@maka/core/ui-locale';
-const ZH = {
+const ZH_CN = {
shareAction: '分享此任务',
shareTitle: '分享任务',
shareDescription: '创建一次性邀请,让另一台 Maka 访问这个任务。',
@@ -119,13 +119,112 @@ const ZH = {
};
type SessionCollaborationCopy = {
- readonly [Key in keyof typeof ZH]: (typeof ZH)[Key] extends (
+ readonly [Key in keyof typeof ZH_CN]: (typeof ZH_CN)[Key] extends (
...args: infer Args
) => string
? (...args: Args) => string
: string;
};
+const ZH_TW = {
+ shareAction: '分享此任務',
+ shareTitle: '分享任務',
+ shareDescription: '建立一次性邀請,讓另一台 Maka 存取這個任務。',
+ enableRemoteAccessTitle: '請先開啟本機遠端存取',
+ enableRemoteAccessBody: '已為你開啟 Runtime Host 設定;開啟遠端存取後即可分享此任務。',
+ disclosureTitle: '分享前請確認',
+ disclosureBody:
+ '對方將看到這個任務已有及之後產生的所有可見內容,包括其中已出現的檔案路徑、憑證或其他機密。撤銷存取只能阻止後續讀取,無法收回對方已複製的內容。',
+ accessLabel: '存取權限',
+ observe: '唯讀',
+ observeHelp: '檢視完整歷史記錄和即時更新',
+ requestTurn: '可請求新輪次',
+ revokeTurnRequests: '撤銷輪次請求權限',
+ requestTurnHelp: '檢視完整內容,並提出需要你逐次核准的新輪次請求',
+ createInvitation: '建立邀請',
+ invitationCode: '一次性邀請碼',
+ invitationHelp: '邀請碼包含連線位址和訪客憑證,不包含擁有者憑證。',
+ coordinationReady: '已包含跨網路協調路徑',
+ coordinationReadyBody:
+ 'Runtime Host 已取得 Relay reservation;這能提高不同網路中的裝置成功建立連線的機會。',
+ coordinationUnavailable: '尚未取得 Relay reservation',
+ coordinationUnavailableBody:
+ '此邀請碼目前只包含直接路徑,不同網路中的裝置可能無法連線。可稍後撤銷並重新建立。',
+ copy: '複製邀請碼',
+ copied: '已複製邀請碼',
+ close: '完成',
+ activeAccess: '目前存取',
+ accessUnavailable: '分享管理暫時無法使用;此視窗會自動重試。',
+ guest: '訪客',
+ noAccess: '尚未分享給任何人',
+ pending: '待領取',
+ active: '已連線',
+ revoke: '撤銷',
+ joinAction: '加入分享任務',
+ joinTitle: '加入分享任務',
+ joinDescription: '貼上邀請碼,建立獨立的訪客連線。',
+ code: '邀請碼',
+ pasteInvitation: '貼上邀請碼',
+ clipboardEmpty: '剪貼簿中沒有可貼上的邀請碼。',
+ clipboardUnavailable: '剪貼簿中沒有有效的邀請碼;請手動貼上。',
+ join: '加入',
+ validatingInvitation: '正在驗證邀請碼…',
+ discoveringHost: '正在尋找任務所在的 Runtime Host…',
+ preparingRoute: '正在準備可用路徑…',
+ connectingHost: '正在連線至 Runtime Host…',
+ authenticatingGuest: '正在驗證訪客憑證…',
+ finalizingAccess: '正在確認訪客存取權限…',
+ loadingSession: '正在載入分享任務…',
+ invalidCode: '邀請碼無效',
+ connectionFailed: '無法加入分享任務',
+ directPathUnavailable: '無法連線至任務所在的 Runtime Host。請確認目前 Desktop Client 和該 Host 位於同一個 Peer Mesh,且存在可用的直接連線或成員轉送路徑;可在 Peer Mesh 設定中同步路徑並檢查成員轉送。',
+ insecureTitle: '此連線未加密',
+ insecureBody: '訪客憑證、完整任務內容和輪次請求可能被同一網路中的第三方攔截。僅在你了解並接受風險時繼續。',
+ shareInsecure: '接受風險並建立',
+ joinInsecure: '接受風險並加入',
+ retainedTasks: '已加入的分享任務',
+ mountConnected: '已連線',
+ mountConnecting: '正在連線',
+ mountReconnecting: '正在重新連線',
+ mountUnavailable: '目前不可用',
+ directConnection: '直連',
+ memberTransitConnection: '成員轉發',
+ disconnect: '中斷連線',
+ disconnectFailed: '無法中斷分享任務連線',
+ turnRequests: '輪次請求',
+ noTurnRequests: '尚無輪次請求',
+ approve: '核准',
+ reject: '拒絕',
+ turnRequestPlaceholder: '描述你希望發起的新輪次',
+ submitTurnRequest: '請求新輪次',
+ turnRequestSent: '請求已送出,等待擁有者核准',
+ regenerateRequest: '重新生成回答',
+ regenerateRequestSent: '重新生成請求已送出,等待擁有者核准',
+ ownerRegenerateRequestTitle: '訪客請求重新生成回答',
+ turnRequestCount: (count: number) => `${count} 個輪次請求`,
+ turnRequestReconnecting: '正在重新連線 Runtime Host;草稿會保留,連線恢復後即可傳送。',
+ turnRequestWithdrawn: '請求已撤回',
+ withdrawTurnRequest: '撤回',
+ viewSourceTurn: '檢視原輪次',
+ recoveryStarted: '共享任務正在後台連線',
+ recoveryStartedBody: '任務已儲存在此裝置;連線恢復後會自動出現在任務清單中。',
+ turnRequestReconciling: '正在確認 Host 是否已收到請求…',
+ turnRequestPending: '等待核准',
+ turnRequestApproved: '已核准',
+ turnRequestRejected: '已拒絕',
+ turnRequestStarted: '已開始',
+ turnRequestBlocked: '無法開始',
+ turnRequestFailed: '准入失敗',
+ dismissTurnRequest: '關閉',
+ sharedTask: '分享任務',
+ newTurnRequestTitle: (count: number) => count === 1 ? '新的輪次請求' : `${count} 個新的輪次請求`,
+ newTurnRequestSummary: (count: number) => `${count} 個輪次請求正在等待核准`,
+ reviewTurnRequest: '檢視並核准',
+ ownerTurnRequestTitle: '訪客請求開始新輪次',
+ moreTurnRequests: (count: number) => `還有 ${count} 個請求等待核准`,
+ pendingTurnRequestCount: (count: number) => `${count} 個待核准輪次請求`,
+} satisfies SessionCollaborationCopy;
+
const EN = {
shareAction: 'Share this task',
shareTitle: 'Share task',
@@ -226,7 +325,7 @@ const EN = {
pendingTurnRequestCount: (count: number) => `${count} pending Turn ${count === 1 ? 'request' : 'requests'}`,
} satisfies SessionCollaborationCopy;
-const COPY = { zh: ZH, en: EN } satisfies UiCatalog<SessionCollaborationCopy>;
+const COPY = { 'zh-CN': ZH_CN, 'zh-TW': ZH_TW, en: EN } satisfies UiCatalog<SessionCollaborationCopy>;
export function getSessionCollaborationCopy(locale: UiLocale) {
return COPY[locale];
diff --git a/apps/desktop/src/renderer/locales/settings-bot-copy.ts b/apps/desktop/src/renderer/locales/settings-bot-copy.ts
index 698aa1f4d9..b34d2fbca1 100644
--- a/apps/desktop/src/renderer/locales/settings-bot-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-bot-copy.ts
@@ -114,6 +114,92 @@ const zhCopy = {
waiting: '等待扫码确认… 窗口会每 3 秒刷新登录状态。', retrying: '重试中…', retry: '重试', bridgeGenerating: 'bridge 正在生成二维码', bridgeGeneratingHint: '二维码就绪后会自动显示,也可以手动重新获取。', fetching: '获取中…', fetchAgain: '重新获取',
},
} as const;
+const zhTwCopy = {
+ providers: {
+ telegram: { label: 'Telegram', help: '透過 @BotFather 建立 Bot 並取得 Token' },
+ feishu: { label: '飛書', help: '在飛書開放平台建立應用並取得憑證' },
+ wecom: { label: '企業微信', help: '透過企業微信 AI 應用串接,使用 WebSocket 長連線' },
+ wechat: { label: '微信', help: '透過本機 wechat-bridge 串接個人微信,需 iOS / Android 微信 8.0.70+。' },
+ discord: { label: 'Discord', help: '在 Discord Developer Portal 建立 Bot' },
+ dingtalk: { label: '釘釘', help: '在釘釘開發者後臺建立機器人應用' },
+ qq: { label: 'QQ', help: '在 QQ 開放平台建立機器人並取得 AppID 和 AppSecret' },
+ slack: { label: 'Slack', help: '使用 Bot Token 與 App-Level Token 透過 Socket Mode 串接' },
+ } satisfies Record<BotProvider, { label: string; help: string }>,
+ readiness: {
+ unscaffolded: { label: '未開放', detail: '該平台目前不可作為遠端串接管道。', tone: 'neutral' },
+ scaffolded: { label: '待設定', detail: '等待補齊這個平台需要的憑證設定。', tone: 'neutral' },
+ configured: { label: '已設定', detail: '已填寫設定;等待完成憑證或執行狀態驗證。', tone: 'attention' },
+ credentials_valid: { label: '憑證有效', detail: '憑證探測透過;這不代表已能收發訊息。', tone: 'attention' },
+ operational: { label: '執行可用', detail: '最近一次真實執行探測成功。', tone: 'success' },
+ degraded: { label: '執行降級', detail: '之前可用,但最近執行狀態探測失敗。', tone: 'error' },
+ } satisfies Record<BotReadinessState, { label: string; detail: string; tone: StatusSemantic }>,
+ planned: { label: '未開放', detail: '該平台目前不會儲存為遠端串接管道或定時任務投遞目標。', tone: 'neutral' as const },
+ status: {
+ disabled: '開關關閉', noToken: '等待填寫 Bot Token', missingFeishuCredentials: '等待填寫飛書 App ID 或 App Secret',
+ feishuDomainRequired: '飛書憑證有效,等待填寫事件訂閱域名', feishuEventsNotConnected: '飛書憑證有效,等待事件回呼串接',
+ unavailable: '該平台目前不可作為遠端串接管道', stopped: '監聽已停止', detailsInLogs: '執行狀態詳情請見記錄',
+ polling: '長輪詢', gateway: '事件通道', webhook: 'Webhook', none: '無',
+ },
+ overview: {
+ loadFailed: '遠端串接狀態載入失敗', reload: '重新載入', active: '正在使用', sortHint: '按需要處理、最近活動排序',
+ empty: '還沒有正在使用的管道', emptyHelp: '從下方選擇一個訊息平台開始設定。', more: '串接更多管道', choose: '選擇平台開始設定',
+ listening: '監聽中', manageAria: (name: string, status: string) => `管理 ${name},${status}`, connectAria: (name: string) => `串接 ${name}`,
+ },
+ page: {
+ saveFailed: (name: string) => `${name} 儲存失敗`, loadFailed: '載入遠端串接狀態失敗', refreshFailed: '重新整理遠端串接狀態失敗',
+ credentialVerified: (name: string) => `${name} 憑證已驗證`, credentialVerifiedDetail: '憑證檢查已透過。', credentialTestFailed: (name: string) => `${name} 憑證測試失敗`, credentialTestFailedDetail: '請檢查憑證和網路設定後重試。', testError: (name: string) => `${name} 測試出錯`,
+ listening: (name: string) => `${name} 已開始監聽`, notListening: (name: string) => `${name} 啟動後未進入監聽`, startFailed: (name: string) => `${name} 啟動失敗`,
+ disconnectTitle: '斷開微信登入?', disconnectDescription: '將清除本機儲存的掃碼登入憑證,之後需要重新掃碼才能繼續使用微信管道。',
+ disconnect: '斷開登入', cancel: '取消', disconnected: '微信登入已斷開', credentialsCleared: '本機關聯憑證已清除。',
+ },
+ detail: {
+ unavailableHint: '該平台未開放,暫不能啟用。', scanFirstHint: '先掃碼串接後才能啟用。', testFirstHint: '先測試並連線後才能啟用。',
+ back: '返回遠端串接', configDocs: '檢視設定文件', enableAria: (name: string) => `啟用${name}管道`, listening: '正在監聽新訊息', healthy: '連線正常,無需處理。',
+ actionsAria: (name: string) => `${name}管道操作`, quickBind: '快捷綁定', scanLogin: '掃碼登入', scanConnect: '掃碼串接', disconnecting: '斷開中…', disconnectWechat: '斷開微信登入', bridgeQr: '本機橋接二維碼',
+ testing: '測試中…', test: '測試連線', connecting: '連線中…', testAndConnect: '測試並連線', restarting: '重啟中…', restart: '重啟監聽',
+ runtimeAria: (name: string) => `${name}執行狀態`, identity: '身份', unknownIdentity: '未取得', connectionType: '通道型別', lastEvent: '最近事件', noneYet: '暫無', lastTest: '最近一次測試', neverTested: '從未測試',
+ statusRefreshFailed: '執行狀態重新整理失敗', latestFailure: '最近一次失敗', latestFailureDetail: '請檢查設定、網路和執行記錄後重試。', savedButNotConnected: '憑證已儲存,但連線尚未成功啟動。', setupMethod: '串接方式', connectionSettings: '連線設定', localCredentials: '憑證僅儲存在本機', autosave: '自動儲存',
+ setupAria: (name: string) => `${name}串接方式`, quickRecommended: '快捷串接(推薦)', manual: '手動設定', quickAria: (name: string) => `${name}快捷串接`,
+ quickWecomTitle: '掃碼建立並綁定機器人', quickTitle: '掃碼自動建立應用與機器人', quickWecomDetail: '企業管理員掃碼確認後,Maka 會儲存 Bot ID 與 Secret 並啟動長連線。',
+ quickQqTitle: '使用手機 QQ 掃碼建立並綁定機器人', quickQqDetail: '確認後,QQ 會安全返回 AppID 與 AppSecret,Maka 在本機儲存憑證並啟動 Gateway。',
+ telegramOfficialFlow: 'Telegram 官方目前僅支援透過 @BotFather 取得 Bot Token,不提供掃碼建立 Bot 並回傳 Token 的 API。',
+ quickDetail: '掃碼確認後,Maka 會在 main process 內儲存憑證並啟動訊息連線。', feishuRegionAria: '選擇飛書帳號區域', feishu: '飛書',
+ beginQuickBind: '開始快捷綁定', scanWith: (name: string) => `使用${name}掃碼串接`, planned: '這個平台目前只作為平台清單展示,不會進入可用管道,也不會儲存為定時任務投遞目標。',
+ credentialsSaved: (name: string) => `${name}憑證已儲存`, scanComplete: (name: string) => `${name}已完成掃碼串接`, savedAndConnected: '憑證已安全儲存並開始連線',
+ proxy: '代理地址', chinaRequired: '(國內網路必填)', authOnly: '(僅用於 Bot 鑑權)', telegramProxyAria: 'Telegram 代理地址',
+ telegramNotice: '請開啟網路的 TUN 模式後重啟應用,以便完成 Telegram Bot 設定', feishuCredentialId: '飛書憑證 ID', feishuSecret: '飛書 App Secret',
+ feishuDomain: '飛書域名', feishuOption: '飛書 (feishu.cn)', discordProxyAria: 'Discord 代理地址',
+ discordNotice: '國內網路存取 Discord:上方代理僅作用於 Bot 鑑權請求,訊息收發走 WebSocket 長連線需要系統級代理。請開啟網路的 TUN 模式後重啟應用。',
+ dingtalkId: '釘釘應用金鑰', dingtalkSecret: '釘釘 Client Secret', wecomBotPlaceholder: '企業微信 AI 應用 Bot ID', wecomBotAria: '企業微信 Bot ID',
+ wecomSecretPlaceholder: 'AI 應用 Secret', wecomSecretAria: '企業微信 Secret', qqId: 'QQ 應用編號',
+ allowedUsersLabel: (count: number, max: number) => `允許的使用者 ID(${count} / ${max})`, allowedUsersPlaceholder: '每行一個使用者 ID,留空表示不限\n例如:123456789',
+ allowedUsersHelp: 'Telegram 使用者 ID 是 64 位整數;填入後只接收列表裡這些 ID 的來信,其它人發的訊息會被靜默忽略(不會回彈任何提示)。',
+ limitReached: '(已達到上限)', invalidUsers: (values: string) => `下列不是數字 ID,可能是使用者名稱之類的輸入,符合不到任何人:${values}`, moreInvalid: (count: number) => ` 等 ${count} 項`,
+ },
+ onboarding: {
+ providers: {
+ dingtalk: { title: '設定釘釘', ariaLabel: '設定釘釘掃碼串接', qrAlt: '設定釘釘二維碼', subtitle: '在釘釘中掃碼完成應用註冊', waiting: '請使用釘釘掃描二維碼並確認授權', scanned: '已掃碼,請在釘釘中完成確認' },
+ feishu: { title: '設定飛書', ariaLabel: '設定飛書掃碼串接', qrAlt: '設定飛書二維碼', subtitle: '使用飛書掃描二維碼,自動建立並設定機器人', waiting: '請使用飛書掃描二維碼並確認建立', scanned: '已掃碼,請在飛書中完成確認' },
+ wecom: { title: '設定企業微信', ariaLabel: '設定企業微信掃碼串接', qrAlt: '設定企業微信二維碼', subtitle: '快捷綁定會自動建立並連線企業微信機器人', waiting: '開啟企業微信,掃描二維碼完成機器人建立', scanned: '已掃碼,請在企業微信中完成確認' },
+ wechat: { title: '掃碼登入', ariaLabel: '微信掃碼登入', qrAlt: '微信掃碼登入二維碼', subtitle: '請使用微信掃描二維碼完成連線', waiting: '請使用微信掃描二維碼並在手機上確認', scanned: '已掃碼,請在微信中完成確認' },
+ qq: { title: '設定 QQ', ariaLabel: '設定 QQ 掃碼串接', qrAlt: '設定 QQ 二維碼', subtitle: '使用手機 QQ 掃碼建立並綁定機器人', waiting: '請使用手機 QQ 掃描二維碼並確認綁定', scanned: '已掃碼,請在 QQ 中完成確認' },
+ },
+ lark: { title: '設定 Lark', ariaLabel: '設定 Lark 掃碼串接', qrAlt: '設定 Lark 二維碼', subtitle: '使用 Lark 掃描二維碼,自動建立並設定機器人', waiting: '請使用 Lark 掃描二維碼並確認建立', scanned: '已掃碼,請在 Lark 中完成確認' },
+ connectedRefreshFailed: (message: string) => `連線已完成,但狀態重新整理失敗:${message}`, close: (title: string) => `關閉${title}`,
+ generatingAria: '正在生成二維碼', privacy: '憑證僅儲存在本機,不會傳給 renderer 或 Maka 雲端。', openBrowser: '無法掃碼?在瀏覽器中開啟',
+ done: '完成', regenerate: '重新生成', refreshQr: '重新整理二維碼', cancel: '取消', generating: '正在生成安全二維碼…', connecting: '授權完成,正在儲存憑證並啟動連線…',
+ connected: (name: string) => `${name} 已連線`, connectedWarning: '憑證已儲存,但連線尚未成功啟動。', expired: '二維碼已過期,請重新生成', denied: '授權已取消,請重新生成二維碼', cancelled: '掃碼串接已取消', failed: '掃碼串接失敗,請重試', preparing: '準備掃碼串接…',
+ },
+ wechat: {
+ token: '微信 Bot Token', tokenPlaceholder: '本機 wechat-bridge Bearer Token', collapseAdvanced: '收起進階設定', expandAdvanced: '進階設定(公眾號 / 本機 bridge 地址)',
+ bridgeAddress: '本機 bridge 地址', appId: '公眾號 App ID', appIdPlaceholder: '微信公眾號 App ID',
+ appSecret: '公眾號 App Secret', appSecretPlaceholder: '微信公眾號 App Secret',
+ advancedNotice: '本機 bridge 預設為 http://127.0.0.1:18400。公眾號 App ID / App Secret 僅用於公眾號訊息傳送,個人微信掃碼登入走本機 bridge。',
+ readQrFailed: '讀取本機 wechat-bridge 二維碼失敗,請確認 bridge 已啟動。', title: '微信掃碼登入', subtitle: '使用手機微信掃描二維碼,並在手機上確認登入本機 wechat-bridge。', close: '關閉微信掃碼登入',
+ generating: '正在生成二維碼…', loggedIn: '微信已登入,返回後可以測試連線或重啟監聽。', expired: '二維碼已過期', expiredHint: '重新整理二維碼後重新掃碼即可繼續登入。', refreshing: '重新整理中…', refresh: '重新整理二維碼', qrAlt: '微信掃碼登入二維碼',
+ waiting: '等待掃碼確認… 視窗會每 3 秒重新整理登入狀態。', retrying: '重試中…', retry: '重試', bridgeGenerating: 'bridge 正在生成二維碼', bridgeGeneratingHint: '二維碼就緒後會自動顯示,也可以手動重新取得。', fetching: '取得中…', fetchAgain: '重新取得',
+ },
+} as const;
export type BotSettingsCopy = WidenCopy<typeof zhCopy>;
@@ -143,7 +229,11 @@ const enCopy: BotSettingsCopy = {
wechat: { token: 'WeChat Bot Token', tokenPlaceholder: 'Local wechat-bridge Bearer Token', collapseAdvanced: 'Hide advanced settings', expandAdvanced: 'Advanced settings (Official Account / local bridge URL)', bridgeAddress: 'Local bridge URL', appId: 'Official Account App ID', appIdPlaceholder: 'WeChat Official Account App ID', appSecret: 'Official Account App Secret', appSecretPlaceholder: 'WeChat Official Account App Secret', advancedNotice: 'The local bridge defaults to http://127. [...]
};
-const BOT_SETTINGS_COPY = { zh: zhCopy, en: enCopy } satisfies UiCatalog<BotSettingsCopy>;
+const BOT_SETTINGS_COPY = {
+ 'zh-CN': zhCopy,
+ 'zh-TW': zhTwCopy,
+ en: enCopy,
+} satisfies UiCatalog<BotSettingsCopy>;
export function getBotSettingsCopy(locale: UiLocale): BotSettingsCopy {
return BOT_SETTINGS_COPY[locale];
diff --git a/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts b/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts
index 0c015bd453..489ba80480 100644
--- a/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts
@@ -40,7 +40,7 @@ export type DailyReviewSettingsCopy = {
};
const SETTINGS_DAILY_REVIEW_COPY = {
- zh: {
+ 'zh-CN': {
defaultModel: '跟随任务默认',
saveFailed: '保存每日回顾设置失败',
aria: '每日回顾',
@@ -59,6 +59,25 @@ const SETTINGS_DAILY_REVIEW_COPY = {
model: '分析模型',
modelHelp: '未指定时跟随当前任务的默认模型。',
},
+ 'zh-TW': {
+ defaultModel: '跟隨任務預設',
+ saveFailed: '儲存每日回顧設定失敗',
+ aria: '每日回顧',
+ unavailable: '目前版本無法讀取每日回顧設定。',
+ loadFailed: (error) => `讀取每日回顧設定失敗:${error}`,
+ scheduleTitle: '定時分析',
+ scheduleDescription: '按本地時間自動分析前一個完整自然日的活動。',
+ enabled: '啟用定時分析',
+ enabledHelp: '每天生成一份昨日活動分析。',
+ executeTime: '執行時間',
+ executeTimeHelp: '使用 24 小時制的本地時間。',
+ executeTimePlaceholder: 'HH:mm',
+ executeTimeInvalid: '請輸入 24 小時制時間,例如 08:00。',
+ analysisTitle: '分析',
+ analysisDescription: '選擇用於生成固定結構報告的模型。',
+ model: '分析模型',
+ modelHelp: '未指定時跟隨目前任務的預設模型。',
+ },
en: {
defaultModel: 'Follow task default',
saveFailed: 'Failed to save Daily Review settings',
diff --git a/apps/desktop/src/renderer/locales/settings-data-copy.ts b/apps/desktop/src/renderer/locales/settings-data-copy.ts
index 7f38a86812..9ad0c79a31 100644
--- a/apps/desktop/src/renderer/locales/settings-data-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-data-copy.ts
@@ -40,7 +40,7 @@ export type DataSettingsCopy = {
};
const SETTINGS_DATA_COPY = {
- zh: {
+ 'zh-CN': {
categories: {
connections: { label: '模型连接', detail: '供应商连接与默认模型(不含密钥)' },
settings: { label: '应用设置', detail: '常规、搜索、机器人、代理等设置' },
@@ -67,6 +67,33 @@ const SETTINGS_DATA_COPY = {
sensitiveWarning: '⚠️ 密钥将以明文写入导出文件。任何拿到该文件的人都能使用这些密钥,请妥善保管、不要分享。',
conflictAria: '导入时同名连接的处理方式', skip: '跳过', overwrite: '覆盖', exportConfig: '导出配置…', importConfig: '导入配置…',
},
+ 'zh-TW': {
+ categories: {
+ connections: { label: '模型連線', detail: '供應商連線與預設模型(不含金鑰)' },
+ settings: { label: '應用設定', detail: '常規、搜尋、機器人、代理等設定' },
+ memory: { label: '本地記憶', detail: '本機 MEMORY.md 的內容' },
+ credentials: { label: '憑據(API 金鑰、權杖)', detail: '模型金鑰與訂閱權杖等敏感資訊', sensitive: true },
+ },
+ importSummary: {
+ connections: (created, overwritten, skipped) => `連線 新增${created}·覆蓋${overwritten}·跳過${skipped}`,
+ settings: '設定已應用', credentials: (applied, skipped) => skipped > 0 ? `憑據 ${applied}(跳過 ${skipped})` : `憑據 ${applied}`,
+ memory: '記憶已應用', empty: '檔案不含可匯入的內容',
+ },
+ loadFailed: '載入資料目錄失敗', openFailed: (label) => `無法開啟${label}`, pathCopied: '已複製工作區路徑', copyFailed: '複製失敗', copyFailedDetail: '剪貼簿不可用或被系統拒絕。',
+ historyCleared: '已清空輸入歷史', historyClearedDetail: '已傳送的提示詞記錄已從本機移除。', selectCategory: '請至少選擇一個類別',
+ exported: '已匯出設定', exportedDetail: (items) => `包含:${items.join('、')}`, exportFailed: '匯出失敗', noCategories: '未選擇任何類別', tryAgain: '請稍後重試',
+ imported: '已匯入設定', importFailed: '匯入失敗', invalidFile: '檔案無效或版本不受支援。',
+ rows: {
+ workspace: '工作區路徑', workspaceDetail: '任務、設定、憑據和技能檔案都存在這個目錄下。', loadValueFailed: '載入失敗', loading: '正在載入…',
+ history: '輸入歷史', historyDetail: '上箭頭 / 下箭頭調出的已傳送提示詞記錄,儲存在本機、重啟後仍在。清空後無法恢復。',
+ },
+ actionsAria: '工作區資料操作', opening: '開啟中…', openWorkspace: '開啟工作區資料夾', copying: '複製中…', copyPath: '複製路徑', clearing: '清空中…', clearHistory: '清空輸入歷史',
+ backupTitle: '備份與恢復', backupNotice: '本機資料儲存在工作區。需要備份時先退出 Maka,再複製整個目錄;恢復時替換同一路徑後重啟。模型連線憑據隨工作區恢復後需要重新測試;訂閱帳號權杖通常需要重新登入。',
+ pathLoadFailed: (error) => `無法載入工作區路徑:${error}`, configAria: '設定匯入匯出', configTitle: '設定匯入匯出',
+ configHelp: '勾選要匯出的內容,生成一個 JSON 備份檔案;換機或重灌時可再匯入。預設不含金鑰。', categoryAria: '選擇匯出內容',
+ sensitiveWarning: '⚠️ 金鑰將以明文寫入匯出檔案。任何拿到該檔案的人都能使用這些金鑰,請妥善保管、不要分享。',
+ conflictAria: '匯入時同名連線的處理方式', skip: '跳過', overwrite: '覆蓋', exportConfig: '匯出設定…', importConfig: '匯入設定…',
+ },
en: {
categories: {
connections: { label: 'Model connections', detail: 'Provider connections and default models (without secrets)' },
diff --git a/apps/desktop/src/renderer/locales/settings-health-copy.ts b/apps/desktop/src/renderer/locales/settings-health-copy.ts
index 6bb4aebbb9..f1b9ed34f0 100644
--- a/apps/desktop/src/renderer/locales/settings-health-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-health-copy.ts
@@ -75,6 +75,71 @@ const layersZh: HealthCenterCopy['layers'] = {
storage: { label: '存储', description: '工作区文件、JSONL、SQLite 等本地存储健康度。' },
};
+const layersZhTw: HealthCenterCopy['layers'] = {
+ configuration: { label: '設定', description: '設定頁中的必填項目是否完整。' },
+ validation: { label: '驗證', description: '憑證與端點的連線測試結果;驗證通過不代表傳送路徑可用。' },
+ permission: { label: '系統權限', description: '所需的 OS 與 TCC 權限是否已授權。' },
+ feature: { label: '功能狀態', description: '功能是否已明確啟用,以及目前是否可用。' },
+ action_approval: { label: '操作核准', description: '工具呼叫與高風險操作的核准原則狀態。' },
+ memory_acceptance: { label: '記憶寫入', description: '是否已接受記憶寫入約定,以及是否已啟用寫入。' },
+ runtime_probe: { label: '執行狀態探測', description: '最近一次實際傳送、串流或事件接收的探測結果。' },
+ storage: { label: '儲存空間', description: '工作區檔案、JSONL、SQLite 和其他本機儲存空間的健康狀態。' },
+};
+
+const healthMessageZhTw: Readonly<Record<string, string>> = {
+ '连接已关闭。': '連線已關閉。',
+ '等待选择默认模型。': '等待選擇預設模型。',
+ '凭据与端点验证已通过。': '憑證與端點驗證已通過。',
+ '连接需要重新修复认证。': '連線需要重新完成驗證。',
+ '上次连接验证失败。': '上次連線驗證失敗。',
+ '没有启用任何模型。': '尚未啟用任何模型。',
+ '不是工作区的默认模型来源。': '不是工作區的預設模型來源。',
+ '等待验证连接。': '等待驗證連線。',
+ '等待完成发送运行态探测。': '等待完成傳送執行狀態探測。',
+ '能力门禁已满足。': '能力門檻已滿足。',
+ '能力已关闭或暂停。': '能力已關閉或暫停。',
+ '等待补齐能力配置。': '等待完成能力設定。',
+ '能力被必要系统权限阻塞。': '能力受到必要系統權限阻擋。',
+ '能力运行态探测处于降级状态。': '能力執行狀態探測目前處於降級狀態。',
+ '最近一次发送已完成。': '最近一次傳送已完成。',
+ '最近一次发送已由用户停止。': '最近一次傳送已由使用者停止。',
+ '最近一次发送失败。': '最近一次傳送失敗。',
+};
+
+const healthDetailZhTw: Readonly<Record<string, string>> = {
+ '这是连接验证结果,不代表发送、流式输出或中断通路已经运行通过。': '這是連線驗證結果,不代表傳送、串流輸出或中斷路徑已實際執行成功。',
+ '在 设置 · 模型 的连接详情里启用至少一个模型后才能使用该连接。': '請在「設定・模型」的連線詳細資料中啟用至少一個模型,才能使用此連線。',
+ '在任务中显式选择该连接的模型即可正常使用;新对话的默认模型在 设置 · 通用 配置。': '在任務中明確選擇此連線的模型即可使用;新對話的預設模型可在「設定・一般」中設定。',
+ '凭据验证与真实发送、流式输出、中断通路是两层健康信号。': '憑證驗證與實際傳送、串流輸出、中斷路徑是兩層不同的健康訊號。',
+ '该能力当前已关闭。': '此能力目前已關閉。',
+ '等待填写平台凭据。': '等待填寫平台憑證。',
+ '仅 macOS 系统权限可探测。': '只能探測 macOS 系統權限。',
+ '系统未提供可直接读取的授权状态。': '系統未提供可直接讀取的授權狀態。',
+ '状态详情请见对应设置页。': '狀態詳細資料請參閱對應的設定頁。',
+};
+
+function healthSignalLabelZhTw(signal: HealthSignal): string {
+ return signal.label.endsWith(' 运行态')
+ ? `${signal.label.slice(0, -' 运行态'.length)} 執行狀態`
+ : signal.label;
+}
+
+function healthSignalMessageZhTw(signal: HealthSignal): string {
+ return healthMessageZhTw[signal.message]
+ ?? (/[\u3400-\u9fff]/u.test(signal.message) ? '健康狀態已更新。' : signal.message);
+}
+
+function healthSignalDetailZhTw(signal: HealthSignal): string | undefined {
+ if (!signal.detail) return undefined;
+ const runtimeDetail = /^模型=(.*?) · 延迟=(\d+)ms(?: · 错误类型=(.*))?$/u.exec(signal.detail);
+ if (runtimeDetail) {
+ const [, model, latency, errorClass] = runtimeDetail;
+ return `模型=${model} · 延遲=${latency}ms${errorClass ? ` · 錯誤類型=${errorClass}` : ''}`;
+ }
+ return healthDetailZhTw[signal.detail]
+ ?? (/[\u3400-\u9fff]/u.test(signal.detail) ? '詳細資料請參閱對應的設定頁。' : signal.detail);
+}
+
const layersEn: HealthCenterCopy['layers'] = {
configuration: { label: 'Configuration', description: 'Whether required settings are complete.' },
validation: { label: 'Validation', description: 'Credential and endpoint connectivity results. A passing validation does not prove the send path works.' },
@@ -87,7 +152,7 @@ const layersEn: HealthCenterCopy['layers'] = {
};
const SETTINGS_HEALTH_COPY = {
- zh: {
+ 'zh-CN': {
loading: '正在加载健康快照', readFailed: '无法读取健康快照', noData: '健康服务未返回数据。', readAgain: '重新读取',
title: '健康中心', subtitle: '各项能力当前的运行状况检查。',
badge: '只读快照', lastRead: '最近一次读取:', refresh: '刷新', summaryAria: '按状态筛选健康信号', summaryFilterAria: (label, count, selected) => selected ? `${label} ${count} 项,当前筛选;再次按下显示全部` : `仅显示${label}健康信号,共 ${count} 项`,
@@ -106,6 +171,25 @@ const SETTINGS_HEALTH_COPY = {
signalMessage: (signal) => signal.message,
signalDetail: (signal) => signal.detail,
},
+ 'zh-TW': {
+ loading: '正在載入健康快照', readFailed: '無法讀取健康快照', noData: '健康服務未返回資料。', readAgain: '重新讀取',
+ title: '健康中心', subtitle: '各項能力目前的執行狀況檢查。',
+ badge: '只讀快照', lastRead: '最近一次讀取:', refresh: '重新整理', summaryAria: '按狀態篩選健康訊號', summaryFilterAria: (label, count, selected) => selected ? `${label} ${count} 項,目前篩選;再次按下顯示全部` : `僅顯示${label}健康訊號,共 ${count} 項`,
+ blockers: {
+ send: (count, totalCount) => `全部健康訊號中,${count}/${totalCount} 條會阻塞傳送`,
+ capability: (count, totalCount) => `全部健康訊號中,${count}/${totalCount} 條會阻塞能力`,
+ },
+ layerAria: (label) => `${label}健康訊號`, layerListAria: (label) => `${label}健康訊號列表`,
+ footnote: '本頁不直接執行測試、修復或權限變更;它只彙總目前已記錄的健康訊號。需要處理問題時,請進入對應設定頁或重新觸發相關功能。',
+ layers: layersZhTw,
+ statuses: { ok: { label: '正常', tone: 'neutral' }, info: { label: '提示', tone: 'neutral' }, warning: { label: '警告', tone: 'attention' }, error: { label: '錯誤', tone: 'error' }, unknown: { label: '未知', tone: 'neutral' } },
+ scopes: { app: '應用', llm_connection: 'LLM 連線', bot: '機器人', capability: '能力', storage: '儲存' },
+ sources: { connection_test: '連線測試', capability_snapshot: '能力快照', permission_snapshot: '權限快照', runtime_probe: '執行態探測', settings: '設定', storage: '本地儲存' },
+ source: '來源:', blocksSend: '阻塞傳送', blocksCapability: '阻塞能力',
+ signalLabel: healthSignalLabelZhTw,
+ signalMessage: healthSignalMessageZhTw,
+ signalDetail: healthSignalDetailZhTw,
+ },
en: {
loading: 'Loading health snapshot', readFailed: 'Could not read health snapshot', noData: 'The health service returned no data.', readAgain: 'Read again',
title: 'Health center', subtitle: 'How each capability is currently doing.',
diff --git a/apps/desktop/src/renderer/locales/settings-memory-copy.ts b/apps/desktop/src/renderer/locales/settings-memory-copy.ts
index 3c8685a733..39dfd21856 100644
--- a/apps/desktop/src/renderer/locales/settings-memory-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-memory-copy.ts
@@ -82,14 +82,21 @@ const zhText = {
localFile: '本地 MEMORY.md', localFileHelp: '透明 Markdown 文件,保存在当前本机工作区。这里的内容不会自动从聊天里抽取。', enableLocalFile: '启用本地 MEMORY.md', agentReadable: '模型上下文可读取', agentReadableHelp: '默认关闭。开启后,发送消息时会把本地记忆一并提供给模型;隐身模式下仍然不提供。', enableAgentRead: '允许模型上下文读取本地记忆', waitingFile: '等待创建 MEMORY.md', waitingBackup: '等待生成上一版备份', dirty: '有未保存修改', savedDraft: '草稿已保存', backupCandidates: '备份候选', backupCandidatesAria: '本地记忆备份候选列表', opening: '打开中…', open: '打开', restoring: '恢复中…', restore: '恢复', copying: '复制中…', copyR [...]
} satisfies Record<MemoryTextKey, string>;
+const zhTwText = {
+ localFile: '本地 MEMORY.md', localFileHelp: '透明 Markdown 檔案,儲存在目前本機工作區。這裡的內容不會自動從聊天裡抽取。', enableLocalFile: '啟用本地 MEMORY.md', agentReadable: '模型上下文可讀取', agentReadableHelp: '預設關閉。開啟後,傳送訊息時會把本地記憶一併提供給模型;隱身模式下仍然不提供。', enableAgentRead: '允許模型上下文讀取本地記憶', waitingFile: '等待建立 MEMORY.md', waitingBackup: '等待生成上一版備份', dirty: '有未儲存修改', savedDraft: '草稿已儲存', backupCandidates: '備份候選', backupCandidatesAria: '本地記憶備份候選列表', opening: '開啟中…', open: '開啟', restoring: '恢復中…', restore: '恢復', copying: '複製中…', copyR [...]
+} satisfies Record<MemoryTextKey, string>;
+
const enText = {
localFile: 'Local MEMORY.md', localFileHelp: 'A transparent Markdown file stored in the current local workspace. Content is never extracted from chats automatically.', enableLocalFile: 'Enable local MEMORY.md', agentReadable: 'Available to model context', agentReadableHelp: 'Off by default. When enabled, local memory is given to the model along with your message; incognito mode still withholds it.', enableAgentRead: 'Allow model context to read local memory', waitingFile: 'Waiting to c [...]
} satisfies Record<MemoryTextKey, string>;
const SETTINGS_MEMORY_COPY = {
- zh: makeCopy('zh-CN', zhText, {
+ 'zh-CN': makeCopy('zh-CN', zhText, {
origins: { manual: '手动记录', imported: '导入记录', extracted: '确认提取', unknown: '手写条目' }, entryStatuses: { draft: '草稿', review_required: '待确认', active: '生效', archived: '已归档', rejected: '已拒绝', unknown: '未识别' }, backupKinds: { reset: '重置前备份', restore: '恢复前备份', save: '保存前备份' }, memoryStatuses: { ok: '本地文件已就绪', disabled: '已关闭', safe_mode: '安全模式', incognito_blocked: '隐身禁用', error: '读取失败' }, promptBlocked: { disabled: '本地记忆已关闭。', incognito: '隐身模式下不会提供本地记忆。', safeMode: 'MEMORY.md 过大,当前不会提供。', agen [...]
}),
+ 'zh-TW': makeCopy('zh-TW', zhTwText, {
+ origins: { manual: '手動記錄', imported: '匯入記錄', extracted: '確認提取', unknown: '手寫條目' }, entryStatuses: { draft: '草稿', review_required: '待確認', active: '生效', archived: '已歸檔', rejected: '已拒絕', unknown: '未識別' }, backupKinds: { reset: '重置前備份', restore: '恢復前備份', save: '儲存前備份' }, memoryStatuses: { ok: '本地檔案已就緒', disabled: '已關閉', safe_mode: '安全模式', incognito_blocked: '隱身停用', error: '讀取失敗' }, promptBlocked: { disabled: '本地記憶已關閉。', incognito: '隱身模式下不會提供本地記憶。', safeMode: 'MEMORY.md 過大,目前不會提供。', agen [...]
+ }),
en: makeCopy('en-US', enText, {
origins: { manual: 'Manual entry', imported: 'Imported entry', extracted: 'Confirmed extraction', unknown: 'Handwritten entry' }, entryStatuses: { draft: 'Draft', review_required: 'Needs review', active: 'Active', archived: 'Archived', rejected: 'Rejected', unknown: 'Unrecognized' }, backupKinds: { reset: 'Before reset', restore: 'Before restore', save: 'Before save' }, memoryStatuses: { ok: 'Local file ready', disabled: 'Off', safe_mode: 'Safe mode', incognito_blocked: 'Disabled in [...]
}),
@@ -99,26 +106,28 @@ export function getMemorySettingsCopy(locale: UiLocale): MemorySettingsCopy { re
function makeCopy(intlLocale: string, text: Record<MemoryTextKey, string>, values: Pick<MemorySettingsCopy, 'origins' | 'entryStatuses' | 'backupKinds' | 'memoryStatuses' | 'promptBlocked' | 'backupOversize' | 'previewOversize' | 'previewTruncationMarker'>): MemorySettingsCopy {
const plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}`;
- const isZh = intlLocale === 'zh-CN';
+ const isZh = intlLocale !== 'en-US';
+ const isZhTw = intlLocale === 'zh-TW';
+ const zh = (simplified: string, traditional: string) => isZhTw ? traditional : simplified;
return {
intlLocale, text, ...values,
- countActive: (count, draft) => isZh ? `${draft ? '草稿 ' : ''}${count} 条生效` : `${draft ? 'Draft · ' : ''}${plural(count, 'active entry', 'active entries')}`,
- countArchived: (count, draft) => isZh ? `${draft ? '草稿 ' : ''}${count} 条已归档` : `${draft ? 'Draft · ' : ''}${plural(count, 'archived entry', 'archived entries')}`,
- saveSummary: (active, archived) => isZh ? `当前 ${active} 条生效${archived > 0 ? ` / ${archived} 条已归档` : ''};已保留上一版备份。` : `${plural(active, 'active entry', 'active entries')}${archived > 0 ? ` / ${plural(archived, 'archived entry', 'archived entries')}` : ''}; the previous version was backed up.`,
- backupSummary: (active, archived) => isZh ? `${active} 条生效${archived > 0 ? ` / ${archived} 条已归档` : ''}` : `${plural(active, 'active entry', 'active entries')}${archived > 0 ? ` / ${plural(archived, 'archived entry', 'archived entries')}` : ''}`,
- countEntries: (count) => isZh ? `${count} 条记忆` : plural(count, 'memory', 'memories'),
- countMatches: (filtered, total) => isZh ? `${filtered} / ${total} 条匹配` : `${filtered} / ${total} matching`,
- listAria: (title) => isZh ? `${title}列表` : `${title} list`,
- entryActionsAria: (title) => isZh ? `${title} 记忆操作` : `${title} memory actions`,
+ countActive: (count, draft) => isZh ? `${draft ? '草稿 ' : ''}${count} ${zh('条生效', '則生效')}` : `${draft ? 'Draft · ' : ''}${plural(count, 'active entry', 'active entries')}`,
+ countArchived: (count, draft) => isZh ? `${draft ? '草稿 ' : ''}${count} ${zh('条已归档', '則已歸檔')}` : `${draft ? 'Draft · ' : ''}${plural(count, 'archived entry', 'archived entries')}`,
+ saveSummary: (active, archived) => isZh ? `${zh('当前', '目前')} ${active} ${zh('条生效', '則生效')}${archived > 0 ? ` / ${archived} ${zh('条已归档', '則已歸檔')}` : ''};${zh('已保留上一版备份。', '已保留上一版備份。')}` : `${plural(active, 'active entry', 'active entries')}${archived > 0 ? ` / ${plural(archived, 'archived entry', 'archived entries')}` : ''}; the previous version was backed up.`,
+ backupSummary: (active, archived) => isZh ? `${active} ${zh('条生效', '則生效')}${archived > 0 ? ` / ${archived} ${zh('条已归档', '則已歸檔')}` : ''}` : `${plural(active, 'active entry', 'active entries')}${archived > 0 ? ` / ${plural(archived, 'archived entry', 'archived entries')}` : ''}`,
+ countEntries: (count) => isZh ? `${count} ${zh('条记忆', '則記憶')}` : plural(count, 'memory', 'memories'),
+ countMatches: (filtered, total) => isZh ? `${filtered} / ${total} ${zh('条匹配', '則符合')}` : `${filtered} / ${total} matching`,
+ listAria: (title) => isZh ? `${title}${zh('列表', '清單')}` : `${title} list`,
+ entryActionsAria: (title) => isZh ? `${title} ${zh('记忆操作', '記憶操作')}` : `${title} memory actions`,
entryActionAria: (action, identity) => isZh ? `${action}:${identity}` : `${action}: ${identity}`,
- openBackupAria: (label) => isZh ? `打开备份候选 ${label}` : `Open backup candidate ${label}`, restoreBackupAria: (label) => isZh ? `恢复备份候选 ${label}` : `Restore backup candidate ${label}`, copyBackupAria: (label) => isZh ? `复制备份候选引用 ${label}` : `Copy backup candidate reference ${label}`,
- draftStatusAria: (action) => isZh ? `${action},保存前不会写入 MEMORY.md` : `${action}; MEMORY.md is not written until you save`,
- restoreLatestDescription: (label) => isZh ? `会先备份当前 MEMORY.md,再用最近一次备份覆盖当前文件。将恢复:${label}` : `The current MEMORY.md will be backed up before the latest backup replaces it. Restore: ${label}`,
- restoreCandidateDescription: (label) => isZh ? `会先备份当前 MEMORY.md,再用选中的备份覆盖当前文件。将恢复:${label}` : `The current MEMORY.md will be backed up before the selected backup replaces it. Restore: ${label}`,
- redactedDetail: (summary) => isZh ? `写入前已替换疑似 token、API key 或密码;${summary}` : `Suspected tokens, API keys, or passwords were redacted before writing; ${summary}`,
- openBackupFailed: (kind) => isZh ? `打开${kind}失败` : `Failed to open ${kind}`,
- previewTruncated: (limit) => isZh ? `预览已按 ${limit} 字符上限截断` : `Preview truncated at the ${limit}-character limit`,
- previewUsage: (length, limit) => isZh ? `预览 ${length} / ${limit} 字符` : `Preview ${length} / ${limit} characters`,
- previewLimit: (limit) => isZh ? `prompt 上限 ${limit} 字符` : `Prompt limit: ${limit} characters`,
+ openBackupAria: (label) => isZh ? `${zh('打开备份候选', '開啟備份候選')} ${label}` : `Open backup candidate ${label}`, restoreBackupAria: (label) => isZh ? `${zh('恢复备份候选', '還原備份候選')} ${label}` : `Restore backup candidate ${label}`, copyBackupAria: (label) => isZh ? `${zh('复制备份候选引用', '複製備份候選引用')} ${label}` : `Copy backup candidate reference ${label}`,
+ draftStatusAria: (action) => isZh ? `${action},${zh('保存前不会写入', '儲存前不會寫入')} MEMORY.md` : `${action}; MEMORY.md is not written until you save`,
+ restoreLatestDescription: (label) => isZh ? `${zh('会先备份当前 MEMORY.md,再用最近一次备份覆盖当前文件。将恢复', '會先備份目前的 MEMORY.md,再以最近一次備份覆蓋目前檔案。將還原')}:${label}` : `The current MEMORY.md will be backed up before the latest backup replaces it. Restore: ${label}`,
+ restoreCandidateDescription: (label) => isZh ? `${zh('会先备份当前 MEMORY.md,再用选中的备份覆盖当前文件。将恢复', '會先備份目前的 MEMORY.md,再以選取的備份覆蓋目前檔案。將還原')}:${label}` : `The current MEMORY.md will be backed up before the selected backup replaces it. Restore: ${label}`,
+ redactedDetail: (summary) => isZh ? `${zh('写入前已替换疑似', '寫入前已遮蔽疑似')} token、API key ${zh('或密码', '或密碼')};${summary}` : `Suspected tokens, API keys, or passwords were redacted before writing; ${summary}`,
+ openBackupFailed: (kind) => isZh ? `${zh('打开', '開啟')}${kind}${zh('失败', '失敗')}` : `Failed to open ${kind}`,
+ previewTruncated: (limit) => isZh ? `${zh('预览已按', '預覽已依')} ${limit} ${zh('字符上限截断', '字元上限截斷')}` : `Preview truncated at the ${limit}-character limit`,
+ previewUsage: (length, limit) => isZh ? `${zh('预览', '預覽')} ${length} / ${limit} ${zh('字符', '字元')}` : `Preview ${length} / ${limit} characters`,
+ previewLimit: (limit) => isZh ? `prompt ${zh('上限', '上限')} ${limit} ${zh('字符', '字元')}` : `Prompt limit: ${limit} characters`,
};
}
diff --git a/apps/desktop/src/renderer/locales/settings-navigation-copy.ts b/apps/desktop/src/renderer/locales/settings-navigation-copy.ts
index cd81e72bf8..ecd1395e0e 100644
--- a/apps/desktop/src/renderer/locales/settings-navigation-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-navigation-copy.ts
@@ -27,7 +27,7 @@ export type SettingsNavigationCopy = {
};
const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
- zh: {
+ 'zh-CN': {
groups: {
preferences: '偏好',
capabilities: '能力',
@@ -53,6 +53,32 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = {
about: { label: '关于', description: '版本、更新与隐私承诺。' },
},
},
+ 'zh-TW': {
+ groups: {
+ preferences: '偏好',
+ capabilities: '能力',
+ activity: '活動',
+ system: '系統',
+ },
+ sections: {
+ general: { label: '通用', description: '顯示名稱與介面語言、隱私與通知、任務預設與網路代理。' },
+ appearance: { label: '外觀', description: '介面主題與調色盤。' },
+ projects: { label: '工作區', description: '管理 Runtime Host 連線,以及預設 Host 上的專案。' },
+ models: { label: '模型', description: '模型連線、API key 與 OAuth 訂閱管理。' },
+ subagents: { label: '子 Agent', description: '設定主 Agent 可以自動選擇的子 Agent、能力邊界與模型。' },
+ usage: { label: '使用統計', description: 'token、模型、工具使用走勢與配額追蹤。' },
+ 'archived-tasks': { label: '已歸檔任務', description: '恢復或徹底刪除已歸檔的任務。' },
+ 'import-tasks': { label: '匯入任務', description: '把本機其他 Agent 的對話記錄轉換成 Maka 任務。' },
+ memory: { label: '記憶', description: 'Maka 記住的內容,以及本地 MEMORY.md 檔案。' },
+ 'daily-review': { label: '每日回顧', description: '每天分析本機任務,生成摘要、遺漏提醒和建議。' },
+ 'bot-chat': { label: '遠端串接', description: '透過 Telegram、飛書、微信等平臺從其他裝置與 Maka 對話。' },
+ search: { label: '聯網搜尋', description: '聯網搜尋供應商(如 Tavily)憑據與隱私邊界。' },
+ data: { label: '資料', description: '本地工作區路徑、備份與恢復。' },
+ permissions: { label: '權限與能力', description: '系統權限授予狀態與 Maka 能力執行時檢查。' },
+ health: { label: '健康', description: '執行時連線、模型探針與本地健康狀態。' },
+ about: { label: '關於', description: '版本、執行環境與隱私承諾。' },
+ },
+ },
en: {
groups: {
preferences: 'Preferences',
diff --git a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts
index d4f31f31eb..343e1ba79e 100644
--- a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts
@@ -269,10 +269,10 @@ export type SettingsPreferencesCopy = {
};
const SETTINGS_PREFERENCES_COPY_BY_LOCALE = {
- zh: {
+ 'zh-CN': {
personalization: {
saveFailed: '保存失败', displayName: '显示名称', displayNameHelp: 'Maka 在聊天里会以这个名字称呼你。留空就用默认的“你”。', displayNamePlaceholder: '例如:JK', displayNameUnset: '未设置,Maka 会称呼你“你”', displayNameChange: '更改', displayNameSet: '设置',
- interfaceLanguage: '界面语言', interfaceLanguageHelp: '选择 Maka 界面的显示语言。切换后立即生效,重启后保持。', localeOptions: [['auto', '跟随系统'], ['zh', '中文'], ['en', 'English']],
+ interfaceLanguage: '界面语言', interfaceLanguageHelp: '选择 Maka 界面的显示语言。切换后立即生效,重启后保持。', localeOptions: [['auto', '跟随系统'], ['zh-CN', '简体中文'], ['zh-TW', '繁體中文'], ['en', 'English']],
assistantTone: '助手语气偏好', assistantToneHelp: '最多 500 字,只影响回答的语气和风格。权限确认与安全规则不受影响;改动会自动保存。', assistantTonePlaceholder: '例如:技术严谨、偏简洁、不要 emoji。',
},
sections: {
@@ -367,9 +367,100 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = {
},
password: { copyFailed: '复制失败', clipboardUnavailable: '剪贴板不可用或被系统拒绝。', copying: '复制中', copied: '已复制', copy: '复制', hide: '隐藏', show: '显示', value: '凭据值' },
},
+ 'zh-TW': {
+ personalization: {
+ saveFailed: '儲存失敗', displayName: '顯示名稱', displayNameHelp: 'Maka 在聊天裡會以這個名字稱呼你。留空就用預設的“你”。', displayNamePlaceholder: '例如:JK', displayNameUnset: '未設定,Maka 會稱呼你“你”', displayNameChange: '更改', displayNameSet: '設定',
+ interfaceLanguage: '介面語言', interfaceLanguageHelp: '選擇 Maka 介面的顯示語言。切換後立即生效,重新啟動後仍會保留。', localeOptions: [['auto', '自動(跟隨系統)'], ['zh-CN', '简体中文'], ['zh-TW', '繁體中文'], ['en', 'English']],
+ assistantTone: '助手語氣偏好', assistantToneHelp: '最多 500 字,只影響回答的語氣和風格。權限確認與安全規則不受影響;改動會自動儲存。', assistantTonePlaceholder: '例如:技術嚴謹、偏簡潔、不要 emoji。',
+ },
+ sections: {
+ identity: '身份', identityHelp: 'Maka 如何稱呼你,以及介面語言和回答語氣。',
+ privacy: '隱私與通知', privacyHelp: '本地資料的讀寫範圍,以及桌面通知時機。',
+ chatDefaults: '任務預設', chatDefaultsHelp: '新任務的起始模型、權限模式與思考級別。',
+ shell: '命令列環境', shellHelp: '選擇 Runtime Host 執行 Bash 工具和終端命令時使用的 shell。',
+ network: '網路', networkHelp: 'AI 模型請求走的網路通道。',
+ theme: '主題', themeHelp: '介面跟隨系統,還是固定淺色或深色。',
+ palette: '調色盤', paletteHelp: '強調色與畫布色調;切換會立即生效並儲存在本地。',
+ appIcon: '應用圖示', appIconHelp: 'Dock、工作列和切換器裡顯示的 Maka 圖示;切換會立即生效。',
+ fontSize: '字型大小', fontSizeHelp: '介面與終端機的文字大小;調整會立即生效並儲存在本機。',
+ pets: '自訂寵物', petsHelp: '管理你自己匯入的 PetPack。Maka 不預裝、也不預設啟用任何寵物。',
+ },
+ appearance: {
+ saveFailed: '儲存外觀設定失敗', theme: '主題', palette: '調色盤',
+ themeOptions: { light: { label: '淺色', help: '始終使用淺色介面。' }, dark: { label: '深色', help: '始終使用深色介面。' }, auto: { label: '跟隨系統', help: '符合系統目前的淺色或深色偏好。' } },
+ paletteLabels: { default: '預設', onedark: 'One Dark', 'catppuccin-mocha': 'Catppuccin Mocha', 'tokyo-night': 'Tokyo Night', nord: 'Nord', coral: '珊瑚', azure: '湖藍', forest: '森林', dusk: '暮光', sand: '沙金', mono: '極簡灰' },
+ paletteHelp: { default: 'Maka 品牌藍強調色', onedark: '編輯器經典深色', 'catppuccin-mocha': '紫調柔和深色', 'tokyo-night': '深藍主題', nord: '北歐冷色', coral: '暖粉 / 珊瑚強調色', azure: '湖藍強調色,乾淨冷靜', forest: '深苔綠與暖蜂蜜強調色', dusk: '深紫羅蘭與冷調畫布', sand: '琥珀沙金與暖奶白', mono: '純灰階,無彩色干擾' },
+ paletteGroups: { editor: '編輯器主題', product: '產品色調' },
+ appIconLabels: { default: '經典', mono: '單色', 'sky': '原色天藍', 'cyan': '青藍', 'ice': '冰藍漸變', 'pale-inverted': '淡底深標', 'ink': '墨黑', 'paper': '紙白', 'graphite': '石墨', 'pencil-kraft': '鉛筆・牛皮紙', 'pencil-sky': '鉛筆・天藍', 'pencil-navy': '鉛筆・深藍', 'alpine': '晴空雪山', 'dusk': '黃昏', 'night': '夜山', 'midnight': '午夜藍', 'carbon': 'OLED 純黑', 'slate': '石板灰', 'obsidian': '黑曜石', 'neon-cyan': '霓虹青', 'matrix': '磷光綠', 'magenta': '洋紅', 'amber-crt': '琥珀 CRT', 'clay': '陶土', 'sage': '鼠尾草', 'dust': '灰粉', 'fog': '霧藍', [...]
+ appIconHelp: { default: 'Maka 預設品牌圖示', mono: '灰階版本,Dock 裡更安靜', 'sky': '幾何 M 標,品牌藍', 'cyan': '偏青的藍', 'ice': '由淺到深的藍色漸變', 'pale-inverted': '淡藍底配深藍標', 'ink': '黑底白標,對比最強', 'paper': '白底黑標', 'graphite': '白底黑標,筆尖為灰', 'pencil-kraft': '鉛筆意象,牛皮紙底', 'pencil-sky': '鉛筆意象,天藍底', 'pencil-navy': '鉛筆意象,深藍底', 'alpine': '雪頂山峰,晴空底', 'dusk': '雪頂山峰,黃昏底', 'night': '雪頂山峰,夜色底', 'midnight': '深藍底搭配亮藍標誌,在深色 Dock 上仍保有清楚輪廓', 'carbon': '純黑背景,OLED 螢幕只顯示標誌', 'slate': '冷色石板灰底搭配淺灰標誌', 'obsidian': '紫黑漸層底搭配淡紫標誌', 'neon [...]
+ appIconGroups: {
+ mascot: '擬人', blue: '藍色系', contrast: '黑白', pencil: '鉛筆', mountain: '高山',
+ dark: '深色', neon: '霓虹', muted: '柔和', warm: '暖色', nature: '自然', metal: '金屬', highContrast: '高對比',
+ custom: '自訂',
+ },
+ appIconSplitLabel: '淺色與深色模式使用不同圖示',
+ appIconSplitHelp: '關閉時,兩種外觀會共用同一個圖示。',
+ appIconTargets: { light: '淺色', dark: '深色' },
+ appIconCustom: '匯入的圖示',
+ appIconCustomHelp: '你自己匯入的圖片',
+ appIconImport: '匯入圖示…',
+ appIconImporting: '正在匯入…',
+ appIconImportHelp: '方形 PNG 最好;四周留約 10% 透明邊,Dock 裡才會和其它應用一樣大。',
+ appIconRemove: '刪除',
+ appIconImportError: '匯入圖示失敗',
+ appIconRemoveFailed: '刪除圖示失敗',
+ appIconSelectFailed: '切換圖示失敗',
+ appIconImportFailed: {
+ too_large: '檔案太大,換一張小一點的圖片',
+ too_many_pixels: '圖片尺寸太大,最多 4096×4096',
+ unsupported_format: '只支援 PNG 和 JPEG',
+ unreadable: '這個檔案讀不出影像',
+ too_small: '圖片太小,至少需要 128×128',
+ write_failed: '無法儲存匯入的圖示',
+ },
+ appIconUnavailable: '無法載入應用圖示',
+ fontSize: { uiLabel: 'UI 字型大小', uiHelp: '調整介面使用的基準字型大小', terminalLabel: '終端機字型大小', terminalHelp: '調整終端機命令輸出與程式碼使用的字型大小' },
+ },
+ pets: {
+ import: '匯入 PetPack', importing: '正在匯入…', loading: '正在載入自訂寵物…',
+ status: '桌面寵物', activePet: (name) => `目前使用:${name}`, disabled: '已關閉', disable: '關閉寵物', disabling: '正在關閉…',
+ empty: '還沒有匯入寵物', emptyHelp: '選擇一個包含 pet.json 和精靈圖的本地資料夾。',
+ selected: '正在使用', select: '使用', selecting: '正在切換…', remove: '刪除', removing: '正在刪除…',
+ removeTitle: (name) => `刪除“${name}”?`, removeDescription: '這會刪除 Maka 本地儲存的該寵物包,且無法撤銷。原始資料夾不會受影響。', confirmRemove: '刪除', cancel: '取消',
+ loadFailed: '無法載入自訂寵物', importFailed: '匯入寵物失敗', selectFailed: '切換寵物失敗', removeFailed: '刪除寵物失敗',
+ importErrors: { invalid_directory: '所選資料夾無效。', invalid_manifest: 'pet.json 不符合 maka.pet/v1 格式。', invalid_asset: '精靈圖缺失、無效或超出限制。', already_installed: '已經匯入了相同 ID 的寵物。', read_failed: '無法讀取所選資料夾。' },
+ selectErrors: { invalid_id: '寵物 ID 無效。', not_found: '該寵物已不在本地寵物庫中。', read_failed: '無法讀取寵物庫。', write_failed: '無法儲存寵物選擇。' },
+ removeErrors: { invalid_id: '寵物 ID 無效。', remove_failed: '無法刪除本機寵物包。' },
+ },
+ general: {
+ incognito: '隱身模式', incognitoHelp: '開啟後暫停本地記憶讀寫、聯網搜尋和定時任務觸發。', enableIncognito: '啟用隱身模式', incognitoFailed: '隱身模式切換失敗', notifications: '完成時傳送系統通知', notificationsHelp: '視窗不在前臺時,在回答完成或出錯後傳送桌面通知。', notificationsFailed: '通知設定切換失敗', workspaceInstructions: '遵循專案指令', workspaceInstructionsHelp: '自動讀取每個專案中已有的 AGENTS.md、CLAUDE.md 或 GEMINI.md;檔案仍由各自專案管理。', workspaceInstructionsFailed: '專案指令設定切換失敗', workHub: '啟用 WorkHub', workHubHelp: '在一個入口檢視已有工作,並將新輸入保守地送往普通任務。', workHubFailed: 'WorkHub 設定切換失敗 [...]
+ defaultModel: '預設模型', defaultModelHelp: '新任務預設使用的模型。', notSet: '未設定', saveDefaultModelFailed: '儲存預設模型失敗', defaultPermission: '預設權限模式', defaultPermissionHelp: '新任務預設使用的權限模式;可在任務內隨時切換。', saveDefaultPermissionFailed: '儲存預設權限模式失敗', defaultThinking: '預設思考級別', defaultThinkingHelp: '新任務的思考級別;目前模型不支援所選級別時用模型預設。', followModelDefault: '跟隨模型預設', saveDefaultThinkingFailed: '儲存預設思考級別失敗',
+ shellPreference: 'Bash 工具 shell', shellPreferenceHelp: '自動模式保持 Windows 的 PowerShell 優先規則;Git Bash 是僅對目前 Runtime Host 生效的顯式覆蓋。', shellAuto: '自動(推薦)', shellGitBash: 'Git Bash', shellExecutable: 'Git Bash 執行檔', shellExecutableHelp: '填寫 Runtime Host 所在 Windows 機器上 bash.exe 的絕對路徑。也支援該機器上的舊版 System32 WSL Bash;儲存時會驗證 GNU Bash。', saveShell: '儲存 shell 設定', savingShell: '正在儲存…', shellSaved: '已儲存', saveShellFailed: '儲存 shell 設定失敗', shellExecutableRejected: '目前 Runtime Host 無法把該路徑作為 GNU Bash 執 [...]
+ proxy: '代理伺服器', proxyHelp: '為 AI 模型請求設定網路代理', enableProxy: '啟用代理伺服器', saveNetworkFailed: '儲存網路設定失敗', proxyProtocol: '代理協議', serverAddress: '伺服器地址', port: '埠', proxyAuth: '代理認證', proxyAuthHelp: '需要使用者名稱和密碼時開啟。', enableProxyAuth: '啟用代理認證', username: '使用者名稱', password: '密碼', bypassList: '代理白名單', bypassHelp: '這些域名將繞過代理直連,多個用逗號分隔。', autoBypass: (count) => `已自動新增 ${count} 個域名。代理僅作用於 AI 模型請求。`, testing: '測試中…', testCurrent: '測試目前設定', proxyReachable: '代理可達', proxyTestFailed: '代理測試失敗', prox [...]
+ passwordSavedPlaceholder: '密碼已儲存;輸入新密碼以替換',
+ },
+ about: {
+ loadFailed: '載入關於資訊失敗', loading: '正在載入關於頁', unavailable: '無法載入關於資訊', copied: '已複製診斷資訊', pasteHint: '檢查內容後,可直接貼上到問題報告', copyFailed: '複製失敗', clipboardUnavailable: '剪貼簿不可用或被系統拒絕。', devBuild: '本地開發版', nightlyBuild: 'Nightly', buildLabel: '建構', supportTitle: '支援', copyAction: '複製', reportIssueHelp: '帶上診斷資訊去 GitHub Issues,回覆更快。', reportIssueOpen: '開啟', channelSummaries: { dev: '本地開發建構,不檢查更新。', nightly: '每日建構的預發佈版,自動更新到最新 nightly,會覆蓋正式版安裝。', release: '正式發佈版,自動接收穩定更新。' }, privacyLabel: '隱私 [...]
+
+ checkForUpdates: '檢查更新',
+ checkingForUpdates: '檢查中…',
+
+
+ updateIdle: '尚未檢查更新。',
+ updateNotAvailable: '已是最新版本。',
+ updateAvailable: (version) => `發現新版本 v${version},正在準備下載…`,
+ updateDownloading: (version, percent) => `正在下載 v${version}(${percent}%)…`,
+ updateVerifying: (version) => `正在驗證 v${version} 的發佈來源…`,
+ updateDownloaded: (version) => `v${version} 已下載,可在側欄選擇重啟安裝。`,
+ updateInstalling: (version) => `正在安裝 v${version}…`,
+ updateCheckFailed: '檢查更新失敗',
+ updateCheckFailedDetail: (message) => message,
+ },
+ password: { copyFailed: '複製失敗', clipboardUnavailable: '剪貼簿不可用或被系統拒絕。', copying: '複製中', copied: '已複製', copy: '複製', hide: '隱藏', show: '顯示', value: '憑據值' },
+ },
en: {
personalization: {
- saveFailed: 'Could not save', displayName: 'Display name', displayNameHelp: 'Maka uses this name when addressing you. Leave it blank to use “you”.', displayNamePlaceholder: 'For example: JK', displayNameUnset: 'Not set — Maka will say “you”', displayNameChange: 'Change', displayNameSet: 'Set', interfaceLanguage: 'Interface language', interfaceLanguageHelp: 'Choose the language used by Maka. Changes apply immediately and persist after restart.', localeOptions: [['auto', 'Follow syst [...]
+ saveFailed: 'Could not save', displayName: 'Display name', displayNameHelp: 'Maka uses this name when addressing you. Leave it blank to use “you”.', displayNamePlaceholder: 'For example: JK', displayNameUnset: 'Not set — Maka will say “you”', displayNameChange: 'Change', displayNameSet: 'Set', interfaceLanguage: 'Interface language', interfaceLanguageHelp: 'Choose the language used by Maka. Changes apply immediately and persist after restart.', localeOptions: [['auto', 'Follow syst [...]
},
sections: {
identity: 'Identity', identityHelp: 'How Maka addresses you, plus interface language and response tone.',
diff --git a/apps/desktop/src/renderer/locales/settings-projects-copy.ts b/apps/desktop/src/renderer/locales/settings-projects-copy.ts
index fa2aee3c2c..c393ee1f08 100644
--- a/apps/desktop/src/renderer/locales/settings-projects-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-projects-copy.ts
@@ -311,7 +311,7 @@ export type SettingsProjectsCopy = {
};
const SETTINGS_PROJECTS_COPY_BY_LOCALE = {
- zh: {
+ 'zh-CN': {
runtimeHost: {
title: 'Runtime Host',
description: 'Local 与其他已启用的 Host 会同时保持连接;任务仍由其所属 Host 处理。',
@@ -628,6 +628,309 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = {
emptyBody: '添加一个项目目录后,新任务就能默认从它打开,侧边栏也会按项目归类任务。',
moreActions: (projectName: string) => `更多操作:${projectName}`,
},
+ 'zh-TW': {
+ runtimeHost: {
+ title: 'Runtime Host',
+ description: 'Local 與啟用的遠端 Host 會同時保持連線;任務仍由其所屬 Host 處理。',
+ selected: '預設 Host',
+ selectedHelp: '新任務和未指定 Host 的設定使用預設 Host',
+ remoteTitle: '遠端 Host',
+ remoteDescription: '透過 SSH 自動設定一臺電腦,或手動連線已有 Runtime Host。',
+ addComputer: '新增電腦',
+ useConnectionCode: '使用連線碼', useConnectionCodeDescription: '貼上另一台電腦產生的一次性連線碼', pasteConnectionCode: '貼上', connectionCodeHelpLabel: '如何取得連線碼', connectionCodeHelp: '在目標電腦的 Maka 中開啟「設定 → 工作區 → 遠端存取」,或開啟已透過 SSH 管理的 Host 並選擇「新建連線碼」。也可在目標電腦執行 maka runtime-host access connection-code。連線碼將在 15 分鐘後過期且只能使用一次。', addSshComputer: '透過 SSH 設定', addSshComputerDescription: '在可透過 SSH 登入的電腦上安裝並連線 Host', addWslEnvironment: '新增 WSL 環境', addWslEnvironmentDescription: '在這台 Windows 電腦的 WSL 中安裝並連線 Host', se [...]
+ configureManually: '手動設定',
+ thisComputerRemoteAccess: '遠端存取',
+ thisComputerRemoteAccessHelp: '透過實驗性端對端直接連線存取此 Host;可自動探索公用協調節點以協助穿透 NAT',
+ remoteAccessOn: '已開啟',
+ remoteAccessOff: '未開啟',
+ enableRemoteAccess: '開啟',
+ remoteAccessEnabling: '正在準備並開啟遠端存取;首次可能需要一點時間。',
+ disableRemoteAccess: '關閉連線',
+ disableRemoteAccessConfirm: '關閉遠端連線?',
+ disableRemoteAccessDescription: '這只會停止 Direct peer 連線;已授予的分享存取權仍會保留。',
+ revokeSharedAccess: '撤銷分享存取權',
+ revokeSharedAccessConfirm: '撤銷分享存取權?',
+ revokeSharedAccessDescription: '已連線的 Desktop 將中斷連線,尚未使用的連線碼也會失效。',
+ revokeSharedAccessDone: '已撤銷分享存取權',
+ createConnectionCode: '建立連線碼',
+ connectionCodeTitle: '連線至這台電腦',
+ connectionCodeDescription: '連線碼將於 15 分鐘後過期且只能使用一次。對方將取得 Owner 權限;Direct peer 沒有備援連線。',
+ importConnectionCodeTitle: '使用連線碼',
+ importConnectionCodeDescription: '連線後將取得對方 Host 的 Owner 權限。Direct peer 沒有備援連線。',
+ connectionCode: '連線碼',
+ copyConnectionCode: '複製連線碼',
+ connectionCodeCopied: '已複製連線碼',
+ connectionCodeInvalid: '連線碼格式無效。',
+ connectionCodeUnavailable: '連線碼已過期或已被使用。請在另一台電腦上建立新的連線碼。',
+ connectionCodeHostUnreachable: '無法建立 Direct peer 連線。請確認兩台電腦均在線上,且網路允許 UDP。',
+ connectionCodeHostMismatch: '連線碼指向的 Host 與實際連線的 Host 不符,或版本不相容。',
+ connectionCodeUnknownError: '連線結果不明。請先檢查遠端 Host 清單,再決定是否重試。',
+ connectWithCode: '連線',
+ remoteAccessActiveTasks: '這台電腦仍有執行中的任務',
+ remoteAccessActiveTasksDescription: '開啟遠端存取需要將 Local Host 交由系統服務管理。是否中斷目前任務並繼續?',
+ uninstallActiveTasksDescription: '移除背景服務會停止目前任務。是否中斷這些任務並繼續?',
+ interruptAndEnable: '中斷任務並開啟',
+ interruptAndUninstall: '中斷任務並移除',
+ remoteAccessFailed: '遠端存取操作失敗',
+ setupTitle: '新增遠端電腦',
+
+ setupName: '顯示名稱(可選)',
+
+
+
+ wslDistribution: 'WSL 發行版本',
+ setupSshPort: 'SSH 埠(可選)',
+ setupDirectoryRootsDescription: '留空時使用遠端 Home 資料夾。新增資料夾後,只能瀏覽這些位置以加入專案。',
+ setupConnect: '連線',
+ setupCancel: '取消',
+ setupRetry: '重試',
+ setupDone: '完成',
+ setupChooseProject: '選擇專案',
+ setupComplete: 'Runtime Host 已連線',
+ setupPhase: {
+ preparing_cli: '正在準備本地 CLI…',
+ connecting_ssh: '正在連線 SSH…',
+ connecting_wsl: '正在連線 WSL 環境…',
+ checking_environment: '正在檢查遠端環境…',
+ installing_package: '正在安裝 Maka…',
+ installing_service: '正在啟動 Runtime Host…',
+ pairing_client: '正在配對這臺裝置…',
+ verifying_connection: '正在驗證憑據…',
+ connecting_host: '正在建立安全連線…',
+ },
+ add: '新增遠端 Host',
+ cancel: '取消',
+ name: '顯示名稱',
+ nameHelp: '僅用於在這臺裝置上識別該 Host',
+ transport: '連線方式',
+ transportHelp: '優先使用 TLS;內網中可透過 SSH tunnel 連線僅監聽本機的 Host',
+ tls: 'TLS',
+ ssh: 'SSH tunnel',
+ plaintext: '明文 WebSocket',
+ url: 'WSS 地址',
+ urlHelp: '遠端 Runtime Host 的 wss:// 地址',
+ plaintextUrl: 'WS 地址',
+ plaintextUrlHelp: '遠端 Runtime Host 的 ws:// 地址',
+ sshDestination: 'SSH 目標',
+ sshDestinationHelp: 'OpenSSH 可識別的 user@host 或 SSH config 別名',
+ sshPort: 'SSH 埠',
+ sshPortHelp: '可選;留空使用 OpenSSH 預設值或 SSH config',
+ remotePort: '遠端 Host 埠',
+ remotePortHelp: '遠端 Runtime Host 在 127.0.0.1 上監聽的 WebSocket 埠',
+ websocketPath: 'WebSocket 路徑',
+ websocketPathHelp: '通常為 /runtime-host',
+ plaintextAcknowledgement: '我瞭解明文連線的風險',
+ plaintextAcknowledgementHelp: '存取憑據和資料可能被同一網路中的第三方截獲',
+ plaintextWarning: '僅在可信且隔離的網路中使用;公網連線應使用 TLS 或 SSH tunnel',
+ sshTerminalTitle: '連線遠端 Runtime Host',
+ sshTerminalDescription: '按 OpenSSH 提示確認主機或輸入密碼。已有 SSH key 時通常無需操作。',
+ sshTerminalClosed: 'SSH 連線已結束',
+ sshTerminalClose: '關閉',
+ rootId: 'State Root ID',
+ rootIdHelp: '來自遠端 service 的 ready 輸出,用於確認連線的是預期 Host',
+ credential: '存取憑據',
+ credentialHelp: '在遠端機器使用 desktop-client preset 簽發',
+ saveAndEnable: '儲存並啟用',
+ defaultBadge: '預設',
+ defaultDisableHelp: '先選擇另一個預設 Host,才能停用此 Host',
+ unavailable: '無法連線',
+ manage: '管理',
+ managementTitle: (name: string) => `管理 ${name}`,
+ serviceStatus: '服務狀態',
+ serviceState: {
+ not_installed: '未安裝',
+ stopped: '已停止',
+ starting: '正在啟動',
+ running: '執行中',
+ failed: '啟動失敗',
+ },
+ experimentalBadge: '實驗性',
+ directPeer: 'Direct peer(實驗性)',
+ directPeerDescription: '建立獨立的實驗性 Direct profile。受限 NAT 或遭封鎖的 UDP 可能導致無法連線,且不會自動回退;請保留 SSH profile 以便手動復原。',
+ directPeerState: {
+ unsupported: '需要更新', not_configured: '未設定', disabled: '已停用', enabled: '已啟用', unavailable: '無法使用',
+ },
+ directPeerUnavailable: '無法讀取 Direct peer 狀態',
+ directPeerUpgradeRequired: '請先更新遠端 Runtime Host,再管理 Direct peer。',
+ directPeerClientUnavailable: '目前 Desktop 建置不包含 Direct peer 支援。',
+ directPeerDisableProfileFirst: '請先在 Runtime Host 清單中停用 Direct peer。',
+ directPeerId: 'Peer ID', directPeerRoutes: '可用路徑',
+ directPeerCoordinationRelays: '連線協調節點(可選)',
+ directPeerCoordinationRelaysPlaceholder: '多個位址請以逗號分隔',
+ directPeerAdvancedCoordination: '手動設定協調節點',
+ directPeerAdvancedNatTraversal: 'NAT 穿透(進階)',
+ directPeerStunPolicy: '公網位址探索',
+ directPeerStunPolicyOptions: {
+ default: '公共 STUN(建議)',
+ disabled: '不使用公共 STUN',
+ custom: '自訂 STUN',
+ },
+ directPeerStunUrls: 'STUN 位址',
+ directPeerStunDefaultHelp:
+ '使用 Cloudflare 公共 STUN 盡力探索公網對映。它不轉送 Maka 流量,但提供者可觀察來源 IP 和請求時間;Maka 不保證其可用性。',
+ directPeerStunDisabledHelp:
+ '僅嘗試本機位址和其他已知直接連線路徑;跨 NAT 的直接連線成功率可能降低。',
+ directPeerStunCustomHelp:
+ '使用逗號分隔的 stun: 位址。STUN 只探索網路位址,不承載 Session 內容。',
+ directPeerAutomaticRelayDiscovery: '自動探索協調節點',
+ directPeerAutomaticRelayDiscoveryHelp:
+ '協調節點使用 Circuit Relay v2 通訊協定,只協助建立端對端直接連線,不承載應用程式流量。Maka 會透過公用 IPFS 網路盡力探索可用節點;手動設定的節點優先。',
+ directPeerEnable: '啟用並新增', directPeerDisable: '停用', directPeerAddProfile: '新增至 Desktop',
+ directPeerActionFailed: 'Direct peer 操作失敗',
+ peerMesh: 'Peer Mesh',
+ peerMeshHelp: '管理此 Desktop peer 的私人 Mesh 成員資格和邀請',
+ managePeerMesh: '管理 Peer Mesh',
+ installedVersion: '版本',
+ operatingSystem: '系統',
+ processId: '程序 ID',
+ lastExitCode: '上次退出碼',
+ stateRoot: 'State Root',
+ directoryRoots: '可用目錄',
+ directoryRootsDescription: '遠端用戶端只能從這些資料夾瀏覽並新增專案。移除資料夾不會刪除已新增的專案。',
+ directoryRootsUnavailable: '更新或修復此 Host 後,即可在 Desktop 管理這些資料夾。',
+ directoryRootsChanged: '這些資料夾已在其他位置變更',
+ directoryRootsChangedDescription: '你的編輯內容仍會保留。請先載入目前設定,再繼續編輯。',
+ reloadDirectoryRoots: '載入目前設定',
+ noDirectoryRoots: '未設定額外目錄',
+ directoryRootLabel: '標籤',
+ directoryRootPath: '遠端絕對路徑',
+ addDirectoryRoot: '新增資料夾',
+ removeDirectoryRoot: '移除',
+ saveDirectoryRoots: '套用資料夾',
+ directoryRootsActiveTasks: '此 Host 仍有執行中的任務',
+ directoryRootsActiveTasksDescription: '套用資料夾需要安全地重新啟動遠端服務。只有在明確確認後,才會中斷這些任務。',
+ configureDirectoriesInterrupt: '中斷任務並套用',
+ refresh: '重新整理',
+ startService: '啟動',
+ restartService: '重啟',
+ restartActiveTasksDescription: '重新啟動會停止目前任務。是否中斷這些任務並繼續?',
+ restartInterrupt: '中斷任務並重啟',
+ repairService: '修復',
+ updateService: '安裝配套版本',
+ updatePolicy: '更新策略',
+ updatePolicyDescription: '選擇這個 Host 跟隨的 Maka 版本',
+ updatePolicyManual: '手動',
+ updatePolicyAutomatic: '自動',
+ updatePolicyOptions: {
+ manual: '手動更新',
+ fixed: '固定版本',
+ latest: 'Latest 穩定頻道',
+ next: 'Next 預覽頻道',
+ },
+ updatePolicyFixedVersion: '版本',
+ updatePolicySave: '儲存策略',
+ updatePolicyCheckNow: '立即檢查',
+ updatePolicyUnavailable: '無法讀取自動更新策略',
+ updateSchedulerUnavailable: '此 Runtime Host 尚不支援自動更新',
+ updateSchedulerUnavailableBody: '請先更新或修復服務,再啟用固定版本或釋出頻道',
+ updateSchedulerUnsupported: '不支援',
+ updateSchedulerInactive: '未執行',
+ updateSchedulerInactiveBody: '更新排程器未在執行,請啟動或修復服務後再啟用自動更新',
+ updateSchedulerNeedsRepair: '需要修復',
+ updateSchedulerNeedsRepairBody: '更新排程器未在執行,請修復服務後再啟用自動更新',
+ updatePolicyDisabled: '自動更新已關閉',
+ updatePolicyActiveTasks: 'Runtime Host 正在執行任務,本次更新已推遲',
+ updatePolicyNotNewer: (version: string) => `Maka ${version} 不高於目前版本`,
+ updatePolicyManualAction: (version: string) => `Maka ${version} 需要手動更新`,
+ updatePolicyManualReason: {
+ current_compatibility_unknown: '無法確認目前版本的儲存相容性',
+ target_compatibility_unknown: '無法確認目標版本的儲存相容性',
+ compatibility_mismatch: '目標版本需要手動處理儲存相容性',
+ },
+ updatePhase: {
+ preparing_cli: '正在準備本地 CLI…',
+ checking: '正在檢查版本…',
+ staging: '正在準備新版本…',
+ retiring: '正在安全停止目前 Runtime Host…',
+ replacing: '正在啟動並驗證新版本…',
+ },
+ updateBlockedTitle: 'Runtime Host 可能仍在執行任務',
+ updateBlockedBody: '無法確認目前 Host 可以安全停止。繼續更新會中斷目前執行,但會保留可恢復的任務狀態和無法確認的外部效果。',
+ updateInterrupt: '中斷任務並更新',
+ updateComplete: (from: string, to: string) => `Runtime Host 已從 ${from} 更新到 ${to}`,
+ updateRepaired: (version: string) => `Runtime Host ${version} 已恢復執行`,
+ updateAlreadyCurrent: (version: string) => `Runtime Host 已是 ${version}`,
+ showLogs: '檢視記錄',
+ noLogs: '沒有服務記錄',
+ uninstallService: '解除安裝服務',
+ uninstallConfirmTitle: '解除安裝遠端 Runtime Host?',
+ uninstallConfirmBody: '這會停止並移除 Maka 管理的服務與程式,但保留 State Root、專案和任務資料。目前 Desktop Profile 不會被刪除。',
+ uninstallConfirm: '解除安裝服務',
+ uninstallRetained: (path: string) => `服務已解除安裝,資料保留在 ${path}`,
+ managementActionFailed: '無法管理 Runtime Host 服務',
+ managementReconnectFailed: '變更已套用,但 Desktop 無法重新連線',
+ manageAccess: '管理存取權限',
+ accessTitle: '存取權限',
+ noAccessCredentials: '沒有存取憑據',
+ currentDesktop: '目前 Desktop',
+ accessKind: {
+ owner: '客戶端存取',
+ capabilityProvider: 'Capability Provider',
+ },
+ accessPending: '等待確認',
+ accessCreated: (date: string) => `建立於 ${date}`,
+ rotateCredential: '輪換憑據',
+ rotateCredentialConfirmTitle: '輪換目前 Desktop 的憑據?',
+ rotateCredentialConfirmBody: '輪換會重新連線這個 Runtime Host,並可能中斷正在進行的工作。請先完成或暫停活躍任務。',
+ rotateCredentialConfirm: '繼續輪換',
+ enableBeforeRotate: '請先啟用這個 Runtime Host,再輪換目前 Desktop 的憑據。',
+ startBeforeChangingAccess: '請先啟動 Runtime Host 服務,再修改存取權限。',
+ revokeCredential: '撤銷',
+ revokeCredentialConfirm: (name: string) => `撤銷 ${name} 的存取權限?`,
+ revokeCredentialConfirmBody: '使用此憑據的客戶端會立即斷開連線,並可能中斷正在進行的工作。',
+ accessActionFailed: '無法管理存取權限',
+ back: '返回',
+ remove: '移除',
+ empty: '還沒有遠端 Host',
+ loadFailed: '無法讀取 Runtime Host profiles',
+ selectFailed: '無法更新 Runtime Host',
+ saveFailed: '無法儲存 Runtime Host profile',
+ removeFailed: '無法移除 Runtime Host profile',
+ pairingRecoveryTitle: '有未完成的配對',
+ pairingRecoveryDescription: '可在對應 Host 的選單中重試;如果不再需要,也可以放棄配對並清理未完成的連線。',
+ resolvePairingRecovery: '重試配對',
+ resolvePairingRecoveryFailed: '無法處理配對恢復',
+ pairingPendingBadge: '配對未完成',
+ discardPairing: '放棄配對',
+ discardPairingConfirmTitle: '放棄這次配對?',
+ discardPairingConfirmBody: '這會刪除未完成的連線,並清理儲存在本機的暫時憑證。之後仍可使用新的邀請碼重新加入。',
+ discardPairingFailed: '無法放棄配對',
+ moreActions: (name: string) => `更多操作:${name}`,
+ },
+ section: '工作區',
+ // Says all three layers of the rule in one sentence, because a help line
+ // that only mentions the default would leave the user guessing what
+ // happens before they set one.
+ sectionHelp: '新任務預設開啟此專案;未設定時沿用上次使用的專案。任何任務都能在輸入框旁臨時切換。',
+ addProject: '新增專案',
+ defaultBadge: '預設',
+ setDefault: '設為預設',
+ setDefaultTitle: '新任務預設開啟這個專案',
+ setDefaultDisabledTitle: '目錄不可用,無法設為預設',
+ setDefaultFailed: '設定預設專案失敗',
+ rename: '重新命名',
+ renameLabel: '專案名稱',
+ renameFailed: '重新命名失敗',
+ openFolder: '開啟專案資料夾',
+ // Says which of the two things went wrong, because the fix differs: a
+ // missing folder is the user's to restore, a refusal to open is not.
+ openFolderFailed: '打不開這個目錄,它可能已被移動或刪除',
+ save: '儲存',
+ cancel: '取消',
+ clearDefault: '取消預設',
+ remove: '從 Maka 移除',
+ removeConfirmTitle: '從 Maka 移除這個專案?',
+ // The one thing a user actually fears here, stated first and plainly.
+ removeConfirmBody: '僅從 Maka 的專案列表移除,磁碟上的檔案不受影響。該專案下已有的任務會移到"未歸屬"分組,不會被刪除。',
+ removeConfirm: '移除',
+ removeCancel: '取消',
+ actionFailed: '操作失敗',
+ unavailable: '目錄不可用',
+ defaultUnavailable: '原來的預設專案已不可用,新任務暫時沿用上次使用的專案。',
+ emptyTitle: '還沒有專案',
+ emptyBody: '新增一個專案目錄後,新任務就能預設從它開啟,側邊欄也會按專案歸類任務。',
+ moreActions: (projectName: string) => `更多操作:${projectName}`,
+ },
en: {
runtimeHost: {
title: 'Runtime Host',
diff --git a/apps/desktop/src/renderer/locales/settings-shared-copy.ts b/apps/desktop/src/renderer/locales/settings-shared-copy.ts
index f414806ed0..0b087c10e9 100644
--- a/apps/desktop/src/renderer/locales/settings-shared-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-shared-copy.ts
@@ -68,7 +68,7 @@ export type SettingsSharedCopy = {
};
const SETTINGS_SHARED_COPY_BY_LOCALE = {
- zh: {
+ 'zh-CN': {
modalLabel: '设置',
contentLabel: '设置内容',
sidebarLabel: '设置侧栏',
@@ -108,6 +108,46 @@ const SETTINGS_SHARED_COPY_BY_LOCALE = {
reviewScheduleHelp: '每日回顾的生成时间与使用的模型。',
},
},
+ 'zh-TW': {
+ modalLabel: '設定',
+ contentLabel: '設定內容',
+ sidebarLabel: '設定側欄',
+ navigationLabel: '設定分組',
+ backToApp: '返回應用',
+ close: '關閉',
+ loading: '正在載入設定',
+ retry: '重試',
+ save: '儲存',
+ cancel: '取消',
+ copy: '複製',
+ copied: '已複製',
+ failed: '失敗',
+ settingsLoadFailed: '載入設定失敗',
+ usageLoadFailed: '載入使用統計失敗',
+ runtimeHost: 'Runtime Host',
+ runtimeHostUnavailable: '這個 Runtime Host 目前不可用。請選擇其他 Host,或在“專案”中重試連線。',
+ unknownError: '出現錯誤,請稍後重試。',
+ unavailablePage: '該設定頁已納入 Maka 設定樹,會隨對應 runtime 能力一起工作。',
+ showDetails: '展開詳情',
+ hideDetails: '收起詳情',
+ ready: '就緒',
+ groups: {
+ memorySources: '記憶',
+ memorySourcesHelp: 'Maka 會在任務中記住你確認過的資訊,用於之後的回答。',
+ memoryDocument: '記憶檔案與備份',
+ memoryDocumentHelp: '記憶儲存在本機 MEMORY.md 裡;這裡可以直接編輯原文或恢復備份。',
+ memoryEntries: '已記住的內容',
+ memoryEntriesHelp: '可以篩選、手動新增,或把不再需要的條目歸檔。',
+ searchProvider: '搜尋服務商',
+ searchProviderHelp: '聯網搜尋使用的服務商與憑據。',
+ searchBehavior: '搜尋行為',
+ searchBehaviorHelp: '什麼時候發起搜尋,以及每次取回多少結果。',
+ dataLocation: '資料位置',
+ dataLocationHelp: '任務、設定、使用統計與憑據都以檔案形式存放在本機的這個位置。',
+ reviewSchedule: '回顧計劃',
+ reviewScheduleHelp: '每日回顧的生成時間與使用的模型。',
+ },
+ },
en: {
modalLabel: 'Settings',
contentLabel: 'Settings content',
diff --git a/apps/desktop/src/renderer/locales/settings-subagents-copy.ts b/apps/desktop/src/renderer/locales/settings-subagents-copy.ts
index 6241da520e..a77ccfae23 100644
--- a/apps/desktop/src/renderer/locales/settings-subagents-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-subagents-copy.ts
@@ -100,7 +100,7 @@ export type SubagentSettingsCopy = {
};
const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = {
- zh: {
+ 'zh-CN': {
section: {
title: '已批准的子 Agent',
count: (total) => `共 ${total} 个配置`,
@@ -182,6 +182,88 @@ const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = {
max: '最大',
},
},
+ 'zh-TW': {
+ section: {
+ title: '已批准的子 Agent',
+ count: (total) => `共 ${total} 個設定`,
+ add: '新增子 Agent',
+ emptyTitle: '還沒有子 Agent 設定',
+ emptyDescription: '新增一個設定後,主 Agent 就能把合適的任務交給獨立模型處理。',
+ },
+ row: {
+ enabled: '啟用',
+ configure: (name) => `設定“${name}”`,
+ fallbackDescription: '尚未填寫適用場景',
+ },
+ status: {
+ missingConnection: '連線不存在',
+ providerRetired: '登入方式已移除 · 請改用其他連線',
+ connectionDisabled: '連線已停用',
+ modelDisabled: '模型未啟用',
+ },
+ editor: {
+ backToList: '返回子 Agent 列表',
+ createSubtitle: '建立一個可由主 Agent 自動選擇的模型設定。',
+ editSubtitle: '修改適用場景、能力邊界和模型路由。',
+ groupPurpose: '用途',
+ groupPurposeHelp: '主 Agent 主要根據這裡的名稱和適用場景挑選設定。',
+ groupRoute: '能力與模型',
+ groupRouteHelp: '固定這個子 Agent 能做什麼,以及它執行在哪個模型上。',
+ dangerZone: '刪除子 Agent',
+ dangerZoneHelp: '此操作不可撤銷。',
+ delete: '刪除',
+ enabled: '啟用',
+ enabledDescription: '關閉後設定仍會保留,但主 Agent 暫時不會選擇它。',
+ name: '顯示名稱',
+ namePlaceholder: '快速程式碼閱讀',
+ id: 'subagent_id',
+ idDescription: '建立後保持不變,主 Agent 和歷史任務會用它識別此設定。',
+ idPlaceholder: 'fast-reader',
+ description: '適用場景',
+ descriptionPlaceholder: '適合快速、低成本地閱讀大型倉庫',
+ profile: '能力 Profile',
+ connection: '模型連線',
+ model: '模型',
+ thinking: '思考級別',
+ defaultThinking: '跟隨模型預設',
+ implementationWarning: '實現程式碼 Profile 可以寫檔案和執行命令,並會在隔離 worktree 中執行。',
+ noConnection: '請先在“模型”頁啟用一個模型連線。',
+ noModel: '所選連線沒有已啟用的模型。',
+ requiredName: '請輸入顯示名稱。',
+ invalidId: (max) => `只能使用字母、數字、點、下劃線、冒號和連字元,最多 ${max} 個字元。`,
+ duplicateId: '這個 subagent_id 已經存在。',
+ invalidConnection: '請選擇一個已啟用的模型連線。',
+ invalidModel: '請選擇一個已啟用的模型。',
+ cancel: '取消',
+ create: '建立',
+ save: '儲存',
+ saving: '儲存中…',
+ },
+ remove: {
+ title: (name) => `刪除“${name}”?`,
+ description: '主 Agent 將不再看到這個設定。已建立的子任務不會被刪除。',
+ confirm: '刪除',
+ cancel: '取消',
+ },
+ toast: {
+ saveFailed: '儲存子 Agent 設定失敗',
+ rejected: '設定沒有被儲存。請確認名稱長度和設定數量都在上限之內。',
+ },
+ profiles: {
+ local_read: { label: '程式碼閱讀', description: '只讀存取目前工作區,適合搜尋、理解和總結程式碼。' },
+ web_research: { label: '網路研究', description: '只使用聯網搜尋,適合查詢外部資料和最新資訊。' },
+ implementation: { label: '實現程式碼', description: '可以讀寫檔案並執行命令,在隔離 worktree 中完成改動。' },
+ },
+ thinking: {
+ off: '關閉',
+ minimal: '最少',
+ low: '低',
+ medium: '中',
+ high: '高',
+ xhigh: '超高',
+ max: '最大',
+ },
+ },
en: {
section: {
title: 'Approved subagents',
diff --git a/apps/desktop/src/renderer/locales/settings-tasks-copy.ts b/apps/desktop/src/renderer/locales/settings-tasks-copy.ts
index 4bd7d41e33..d58505f2e6 100644
--- a/apps/desktop/src/renderer/locales/settings-tasks-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-tasks-copy.ts
@@ -60,7 +60,7 @@ export type SettingsTasksCopy = {
};
const SETTINGS_TASKS_COPY_BY_LOCALE = {
- zh: {
+ 'zh-CN': {
listAria: '已归档任务',
noProject: '无项目',
deletedParent: '原父任务已删除',
@@ -87,6 +87,33 @@ const SETTINGS_TASKS_COPY_BY_LOCALE = {
emptyTitle: '没有已归档的任务',
emptyBody: '在侧栏里归档一个任务后,可以在这里恢复或彻底删除它。',
},
+ 'zh-TW': {
+ listAria: '已歸檔任務',
+ noProject: '無專案',
+ deletedParent: '原父任務已刪除',
+ searchLabel: '搜尋已歸檔任務',
+ purgeAll: '清空全部',
+ purgeMatches: (count: number) => `刪除這 ${count} 條`,
+ purgeAllConfirmTitle: (count: number) => `清空全部 ${count} 條已歸檔任務?`,
+ purgeMatchesConfirmTitle: (count: number) => `刪除搜尋到的 ${count} 條任務?`,
+ purgeConfirmBody: '這些任務及其全部訊息會被永久刪除,無法撤銷。',
+ purgeSubtaskNote: '其中的普通子任務不會被刪除,將保留並移入歸檔。',
+ purgeConfirmAction: '永久刪除',
+ purgedToast: (count: number) => `已刪除 ${count} 條任務`,
+ purgedSubtaskNote: (count: number) => `${count} 個子任務已移入歸檔`,
+ purgeKeptRestored: (count: number) => `另有 ${count} 條在此期間被恢復,已保留。`,
+ purgeFailedTitle: '刪除任務失敗',
+ purgeFailedBody: (count: number) => `${count} 條仍在,請重試。`,
+ purgeUnverified: '任務已刪除,但無法讀取列表確認結果。請重新開啟本頁檢視。',
+ noMatchTitle: '沒有符合的任務',
+ noMatchBody: '換個關鍵詞試試。',
+ moreActions: (name: string) => `「${name}」的更多操作`,
+ restore: '恢復',
+ restoreTask: (name: string) => `恢復「${name}」`,
+ delete: '徹底刪除',
+ emptyTitle: '沒有已歸檔的任務',
+ emptyBody: '在側欄裡歸檔一個任務後,可以在這裡恢復或徹底刪除它。',
+ },
en: {
listAria: 'Archived tasks',
noProject: 'No project',
diff --git a/apps/desktop/src/renderer/locales/settings-test-result-copy.ts b/apps/desktop/src/renderer/locales/settings-test-result-copy.ts
index 48a4097fb9..ee5ce9b90d 100644
--- a/apps/desktop/src/renderer/locales/settings-test-result-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-test-result-copy.ts
@@ -44,7 +44,7 @@ type SettingsTestResultCopy = {
};
const COPY = {
- zh: {
+ 'zh-CN': {
proxy: {
reachable: (endpoint, location) =>
["代理配置有效", endpoint, location].filter(Boolean).join(" · "),
@@ -69,6 +69,31 @@ const COPY = {
connectionFailed: "请检查凭据和网络设置后重试。",
},
},
+ 'zh-TW': {
+ proxy: {
+ reachable: (endpoint, location) =>
+ ["代理設定有效", endpoint, location].filter(Boolean).join(" · "),
+ disabled: "請先啟用代理伺服器,再進行測試。",
+ configurationMissing: "請填寫代理伺服器地址和埠後再測試。",
+ credentialMissing: "代理認證已開啟,請輸入代理密碼後再測試。",
+ timeout: "代理測試超時,請檢查代理服務是否可達。",
+ httpError: (status) =>
+ status === undefined
+ ? "代理測試回傳了錯誤回應,請檢查代理服務或測試地址。"
+ : `代理測試回傳 HTTP ${status},請檢查代理服務或測試地址。`,
+ unreachable: "代理不可達,請檢查伺服器地址、埠和認證資訊。",
+ },
+ bot: {
+ credentialsValid: (username) =>
+ username
+ ? `憑據檢查已透過 · ${username}。這不代表訊息收發服務已啟動。`
+ : "憑據檢查已透過。這不代表訊息收發服務已啟動。",
+ tokenMissing: "請填寫 Bot Token 後再測試。",
+ tokenInvalid: "Bot Token 無效,請檢查後重試。",
+ appCredentialsMissing: "請填寫 App ID 和 App Secret 後再測試。",
+ connectionFailed: "請檢查憑據和網路設定後重試。",
+ },
+ },
en: {
proxy: {
reachable: (endpoint, location) =>
diff --git a/apps/desktop/src/renderer/locales/settings-usage-copy.ts b/apps/desktop/src/renderer/locales/settings-usage-copy.ts
index f5160b4e2c..75cf3752ad 100644
--- a/apps/desktop/src/renderer/locales/settings-usage-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-usage-copy.ts
@@ -37,7 +37,7 @@ export type UsageSettingsCopy = {
};
const SETTINGS_USAGE_COPY = {
- zh: {
+ 'zh-CN': {
saveFailed: '保存使用统计设置失败', toolbarAria: '使用统计范围与刷新', rangeAria: '使用统计时间范围', ranges: ['24h', '7天', '30天', '全部'],
refreshingAria: '正在刷新使用统计', refreshAria: '刷新使用统计', summaryAria: '使用统计汇总指标', totalRequests: '模型调用', totalCost: '总费用', costHelp: '以模型供应商最终结算为准',
totalTokens: '总 Token', tokenDetail: (input, output) => `输入 ${input} / 输出 ${output}`, cacheTokens: '缓存 Token',
@@ -59,6 +59,28 @@ const SETTINGS_USAGE_COPY = {
pricingEmptyBody: '未配置定价覆盖时,费用按内置模型定价表结算;在此可为特定模型登记自定义价格。',
},
},
+ 'zh-TW': {
+ saveFailed: '儲存使用統計設定失敗', toolbarAria: '使用統計範圍與重新整理', rangeAria: '使用統計時間範圍', ranges: ['24h', '7天', '30天', '全部'],
+ refreshingAria: '正在重新整理使用統計', refreshAria: '重新整理使用統計', summaryAria: '使用統計彙總指標', totalRequests: '總請求', totalCost: '總費用', costHelp: '以模型供應商最終結算為準',
+ totalTokens: '總 Token', tokenDetail: (input, output) => `輸入 ${input} / 輸出 ${output}`, cacheTokens: '快取 Token',
+ cacheDetail: (miss, read, creation) => `新 ${miss} / 命中 ${read} / 建立 ${creation}`, viewAria: '使用統計檢視', tabs: ['請求記錄', '供應商統計', '模型統計', '工具統計', '定價設定'],
+ filtersAria: '請求記錄篩選', filterPlaceholder: '按模型或工具篩選…', filterAria: '按模型或工具篩選請求記錄', statusAria: '請求狀態篩選',
+ statuses: ['全部狀態', '成功', '錯誤', '已中止'], details: '詳情記錄', detailsAria: '顯示使用統計詳情記錄', recordCount: (count) => `共 ${count} 條記錄`, clearFilters: '清除篩選',
+ summaryOnly: '目前僅顯示彙總指標。開啟詳情記錄後,可以檢視逐條模型請求和工具呼叫,按模型、工具或狀態篩選,並用於排查費用與失敗請求。',
+ costUnavailable: '費用未知', incompleteTitle: '統計可能不完整',
+ incompleteBody: '部分記錄可能無法讀取、尚未納入統計或超出顯示上限,實際用量可能高於此處顯示。',
+ showDetails: '顯示明細', filteredEmpty: '沒有符合篩選條件的請求記錄', filteredEmptyHelp: '調整或清除篩選條件後可檢視全部請求記錄。', requestEmpty: '暫無請求記錄',
+ tables: {
+ providersAria: '使用統計供應商統計表', modelsAria: '使用統計模型統計表', toolsAria: '使用統計工具統計表', pricingAria: '使用統計定價設定表', requestsAria: '使用統計請求記錄表',
+ providerHeaders: ['供應商', '請求', 'Token', '費用'], modelHeaders: ['模型', '請求', 'Token', '費用'], toolHeaders: ['工具', '呼叫', '成功', '錯誤', '平均耗時'],
+ pricingHeaders: ['供應商', '模型', '輸入 / 1M', '輸出 / 1M'], requestHeaders: ['時間', '型別', '物件', '任務', 'Token', '費用', '延遲', '狀態'],
+ noPricing: '暫無定價覆蓋設定', modelKind: '模型', toolKind: '工具', unknown: '未知', openSession: (label) => `開啟 ${label}`, untitledSession: '未命名會話', success: '成功', error: '錯誤', aborted: '已中止',
+ providerEmptyTitle: '暫無供應商用量', providerEmptyBody: '完成一次模型請求後,這裡會按供應商聚合請求數、Token 與費用。',
+ modelEmptyTitle: '暫無模型用量', modelEmptyBody: '完成一次模型請求後,這裡會按模型聚合請求數、Token 與費用。',
+ toolEmptyTitle: '暫無工具呼叫', toolEmptyBody: '智慧體呼叫工具後,這裡會按工具聚合呼叫次數、成功、錯誤與平均耗時。',
+ pricingEmptyBody: '未設定定價覆蓋時,費用按內建模型定價表結算;在此可為特定模型登記自訂價格。',
+ },
+ },
en: {
saveFailed: 'Failed to save usage settings', toolbarAria: 'Usage range and refresh', rangeAria: 'Usage time range', ranges: ['24h', '7 days', '30 days', 'All'],
refreshingAria: 'Refreshing usage', refreshAria: 'Refresh usage', summaryAria: 'Usage summary metrics', totalRequests: 'Model calls', totalCost: 'Total cost', costHelp: 'Final billing is determined by the model provider',
diff --git a/apps/desktop/src/renderer/locales/settings-web-search-copy.ts b/apps/desktop/src/renderer/locales/settings-web-search-copy.ts
index 9f42cbfcc7..0cd8730aff 100644
--- a/apps/desktop/src/renderer/locales/settings-web-search-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-web-search-copy.ts
@@ -38,7 +38,7 @@ export type WebSearchSettingsCopy = {
};
const SETTINGS_WEB_SEARCH_COPY = {
- zh: {
+ 'zh-CN': {
saveFailed: '保存联网搜索设置失败', saveStatusFailed: '保存联网搜索状态失败', keySaved: '已保存 Tavily 密钥', keySavedDetail: '可点击「测试」做一次真实请求验证。',
credentialsCleared: '已清空 Tavily 凭据', credentialsClearedDetail: '联网搜索已自动关闭。', credentialValid: 'Tavily 凭据可用', resultCount: (count) => `返回 ${count} 条结果。`,
testFailed: '联网搜索测试失败', testError: '联网搜索测试出错', enabled: '启用联网搜索', enabledHelp: '启用后,Maka 可以在需要最新外部信息时调用所选搜索来源。',
@@ -55,6 +55,23 @@ const SETTINGS_WEB_SEARCH_COPY = {
sources: { model: '来源:当前模型连接', envWithSaved: '来源:环境变量(已保存密钥备用)', env: '来源:环境变量', saved: '来源:本机已保存密钥', none: '来源:未配置' },
errors: { invalid_query: '请输入有效的搜索内容。', incognito_active: '无痕模式下无法使用联网搜索。', not_configured: '所选搜索来源尚未配置完成。', invalid_credentials: '搜索来源拒绝了当前凭据,请更新后重试。', rate_limited: '搜索请求过于频繁,请稍后重试。', network_error: '网络请求失败,请检查网络后重试。', timeout: '搜索请求超时,请重试。', unsupported_provider: '当前模型不支持服务端搜索,或 Maka 尚未实现它的协议;可改用 Tavily。', experimental_disabled: '联网搜索实验功能当前已关闭。' },
},
+ 'zh-TW': {
+ saveFailed: '儲存聯網搜尋設定失敗', saveStatusFailed: '儲存聯網搜尋狀態失敗', keySaved: '已儲存 Tavily 金鑰', keySavedDetail: '可點選「測試」做一次真實請求驗證。',
+ credentialsCleared: '已清空 Tavily 憑據', credentialsClearedDetail: '聯網搜尋已自動關閉。', credentialValid: 'Tavily 憑據可用', resultCount: (count) => `返回 ${count} 條結果。`,
+ testFailed: '聯網搜尋測試失敗', testError: '聯網搜尋測試出錯', enabled: '啟用聯網搜尋', enabledHelp: '啟用後,Maka 可以在需要最新外部資訊時呼叫所選搜尋來源。',
+ provider: '搜尋來源', providerHelp: '優先複用目前模型的服務端搜尋;不支援時可顯式改用 Tavily。', providerModel: '目前模型', providerTavily: 'Tavily',
+ modelCredential: '主模型原生搜尋', modelCredentialHelp: 'Maka 會在每個任務回合開始時,根據目前連線與精確模型決定是否把原生 web_search 注入同一次模型請求。不儲存第二份搜尋金鑰,也不會從設定頁另發一次模型呼叫。',
+ statusAria: '聯網搜尋憑據狀態', lastTest: '最近測試 ', enabledAria: '啟用聯網搜尋', key: 'Tavily 金鑰',
+ envKeyHelp: '目前使用環境變數 TAVILY_API_KEY / MAKA_TAVILY_API_KEY;如需改用儲存的金鑰,請移除環境變數後重啟。', savedKeyHelp: '金鑰只儲存在本機。申請地址:',
+ envPlaceholder: '由環境變數提供', storedPlaceholder: '已儲存(輸入新金鑰可替換)', keyPlaceholder: 'tvly-xxxxxxxx', keyAria: 'Tavily 金鑰',
+ actions: '憑據操作', actionsHelp: '儲存後可以測試一次真實請求;清空憑據會同步關閉聯網搜尋。', saving: '儲存中…', saveKey: '儲存金鑰', testing: '測試中…', testKey: '測試憑據', clearing: '清空中…', clearKey: '清空金鑰',
+ testSearch: '測試搜尋', testSearchHelp: '發一條真實查詢,確認所選聯網搜尋來源是否設定可用。結果只顯示在這裡,不寫入任務。', queryPlaceholder: '例如:本週 AI 產品釋出動態',
+ searching: '搜尋中…', search: '搜尋', queryFailed: (error) => `查詢失敗:${error}`, noResults: '沒有結果。', resultsAria: '聯網搜尋真實查詢結果',
+ disabledReasons: { noKey: '先設定所選搜尋來源', disabled: '先啟用聯網搜尋', noQuery: '輸入查詢後再搜尋' },
+ statuses: { valid: '已驗證', invalid_credentials: '金鑰無效', rate_limited: '服務限流', timeout: '測試超時', network_error: '網路異常', not_configured: '等待設定', untested: '未測試', validEnabled: '已驗證 · 已啟用', validDisabled: '已驗證 · 未啟用', unknownEnabled: '未測試 · 已啟用', modelEnabled: '已啟用 · 按任務模型判定', modelDisabled: '目前模型來源 · 未啟用' },
+ sources: { model: '來源:目前模型連線', envWithSaved: '來源:環境變數(已儲存金鑰備用)', env: '來源:環境變數', saved: '來源:本機已儲存金鑰', none: '來源:未設定' },
+ errors: { invalid_query: '請輸入有效的搜尋內容。', incognito_active: '無痕模式下無法使用聯網搜尋。', not_configured: '所選搜尋來源尚未設定完成。', invalid_credentials: '搜尋來源拒絕了目前憑據,請更新後重試。', rate_limited: '搜尋請求過於頻繁,請稍後重試。', network_error: '網路請求失敗,請檢查網路後重試。', timeout: '搜尋請求超時,請重試。', unsupported_provider: '目前模型不支援服務端搜尋,或 Maka 尚未實現它的協議;可改用 Tavily。', experimental_disabled: '聯網搜尋實驗功能目前已關閉。' },
+ },
en: {
saveFailed: 'Failed to save web search settings', saveStatusFailed: 'Failed to save web search status', keySaved: 'Tavily key saved', keySavedDetail: 'Select Test credentials to verify it with a real request.',
credentialsCleared: 'Tavily credentials cleared', credentialsClearedDetail: 'Web search was disabled automatically.', credentialValid: 'Tavily credentials work', resultCount: (count) => `Returned ${count} ${count === 1 ? 'result' : 'results'}.`,
diff --git a/apps/desktop/src/renderer/locales/shell-copy.ts b/apps/desktop/src/renderer/locales/shell-copy.ts
index ed545587d2..3c6c50b745 100644
--- a/apps/desktop/src/renderer/locales/shell-copy.ts
+++ b/apps/desktop/src/renderer/locales/shell-copy.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { generalizedErrorMessage, generalizedErrorMessageChinese } from '@maka/core/redaction';
+import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
import { type UiCatalog, type UiLocale } from '@maka/core/ui-locale';
@@ -758,7 +758,7 @@ const EN_SETTINGS_SECTIONS: Record<SettingsSection, string> = {
};
const SHELL_COPY_BY_LOCALE = {
- zh: {
+ 'zh-CN': {
navigation: { settings: '设置' },
actions: { retry: '重试' },
paths: {
@@ -1260,6 +1260,508 @@ const SHELL_COPY_BY_LOCALE = {
resizeWorkbar: '调整任务工作栏宽度',
},
},
+ 'zh-TW': {
+ navigation: { settings: '設定' },
+ actions: { retry: '重試' },
+ paths: {
+ workspace: '工作區資料夾',
+ project: '專案目錄',
+ skills: 'Skills 資料夾',
+ },
+ errors: {
+ messageRead: '任務內容暫時無法讀取,請稍後重試。',
+ messageRefresh: '任務內容暫時無法重新整理,請稍後重試。',
+ openPath: (path: string) => `無法開啟${path},請稍後重試。`,
+ workspaceUnavailableTitle: '工作目錄不可用',
+ workspaceUnavailableDescription: '工作目錄不存在或無法存取。請選擇有效目錄建立新任務。',
+ },
+ chatActions: {
+ newConversation: '建立任務',
+ sendFailedTitle: '傳送失敗',
+ sendFailedFallback: '訊息暫時無法傳送,請稍後重試。',
+ skillInvocationBlockedTitle: 'Skill 呼叫失敗,訊息未傳送',
+ skillInvocationBlockedDescription: (items) => `${items.join('、')}。請調整選擇後重試。`,
+ skillInvocationFailedTitle: '部分 Skill 未能呼叫',
+ skillInvocationFailedDescription: (items) => `${items.join('、')}。其餘 Skill 已正常呼叫。`,
+ skillInvocationFailureReason: {
+ invalid_name: '名稱無效',
+ not_found: '未找到',
+ disabled: '已停用',
+ host_incompatible: '目前環境缺少依賴',
+ resolution_failed: '解析失敗',
+ too_many_requests: 'Skill 呼叫請求超過 50 個上限',
+ },
+ responseFailedTitle: '響應失敗',
+ responseFailedFallback: '任務操作失敗,請稍後重試。',
+ refreshFailedTitle: '重新整理任務失敗',
+ sessionStartFailedTitle: '開始任務失敗',
+ sessionStartFailedFallback: '任務暫時無法開始,請稍後重試。',
+ },
+ projectActions: {
+ currentProject: '目前專案',
+ readPathFailedTitle: '讀取專案路徑失敗',
+ readPathFailedFallback: '專案路徑暫時無法讀取,請稍後重試。',
+ selectDirectoryFailedTitle: '選擇工作目錄失敗',
+ selectedPathUnreadable: '所選路徑不存在或不可讀。',
+ directorySwitchedTitle: '已切換工作目錄',
+ projectUpdateFailedTitle: '專案操作失敗',
+ projectUpdateFailedFallback: '暫時無法更新專案,請稍後重試。',
+ catalogUnavailable: 'Runtime Host 暫時不可用',
+ retryCatalog: '重試載入',
+ remoteDirectoryTitle: (host: string) => `在 ${host} 上新增專案`,
+ remoteDirectoryBreadcrumbs: '目前資料夾',
+ remoteDirectoryHome: '主目錄',
+ remoteDirectoryEmpty: '此資料夾中沒有子資料夾',
+ remoteDirectorySelect: '新增此資料夾',
+ remoteDirectoryCancel: '取消',
+ remoteDirectoryRetry: '重試',
+ remoteDirectoryLoading: '正在讀取資料夾…',
+ remoteDirectoryShowHidden: '顯示隱藏目錄',
+ remoteDirectoryHideHidden: '不顯示隱藏目錄',
+ runtimeHostReadiness: {
+ connecting: '連線中',
+ reconnecting: '正在重連',
+ unavailable: '不可用',
+ },
+ openFailedTitle: (path: string) => `無法開啟${path}`,
+ openPathLabels: {
+ workspace: '工作區目錄',
+ skills: 'Skills 目錄',
+ memory: '記憶目錄',
+ project: '專案目錄',
+ },
+ openPathFailures: {
+ 'unknown-key': '未知的工作區目錄。',
+ 'not-allowed': '路徑不在允許開啟的工作區範圍內。',
+ missing: '目錄不存在。',
+ 'not-a-directory': '目標不是目錄。',
+ 'open-failed': '系統沒有開啟該目錄。',
+ unknown: '無法開啟目錄。',
+ },
+ },
+ commandActions: {
+ connectionVerified: (name: string) => `連線已驗證 · ${name}`,
+ connectionLatency: (latency: number | string, model?: string) =>
+ `延遲 ${latency} ms${model ? ` · ${model}` : ''}`,
+ connectionTestFailed: (name: string) => `連線測試失敗 · ${name}`,
+ testErrorTitle: '測試出錯',
+ connectionUnavailable: '連線測試暫時不可用,請稍後重試。',
+ connectionFailures: {
+ rateLimit: '目前帳號或模型服務觸發速率限制,請稍後重試。',
+ timeout: '請求超時,請檢查網路或代理後重試。',
+ auth: '鑑權失敗,請檢查模型金鑰、訂閱帳號登入或憑據設定後重試。',
+ network: '網路錯誤,請檢查網路或代理後重試。',
+ provider: '模型服務返回錯誤,請稍後重試。',
+ unknown: '連線測試失敗,請稍後重試。',
+ },
+ setDefaultSuccess: (name: string) => `已設為預設 · ${name}`,
+ setDefaultFailedTitle: '切換預設失敗',
+ setDefaultFallback: '預設模型暫時無法切換,請稍後重試。',
+ newConversation: '建立任務',
+ conversationCopiedTitle: '已複製任務為 Markdown',
+ lineCount: (lines: number) => `${lines} 行 · 可貼上到 Notion / Obsidian / GitHub`,
+ copyFailedTitle: '複製失敗',
+ clipboardUnavailable: '剪貼簿不可用',
+ conversationSavedTitle: '已儲存目前任務',
+ saveSummary: (lines: number, fileName: string) => `${lines} 行 · 儲存為 ${fileName}`,
+ saveFailedTitle: '儲存失敗',
+ invalidExport: '匯出內容無效',
+ writeFailed: '無法寫入選擇的位置',
+ exportFallback: '匯出目前任務失敗,請稍後重試。',
+ memoryOpenFailedTitle: '無法開啟 MEMORY.md',
+ openFailedTitle: '開啟失敗',
+ memoryOpenFallback: '無法開啟 MEMORY.md,請稍後重試。',
+ today: '今天',
+ reviewCopiedTitle: '已複製今日回顧為 Markdown',
+ reviewSummary: (sessions: number, requests: number) => `${sessions} 個任務 · ${requests} 個請求`,
+ reviewCopyFallback: '今日回顧暫時不可用,或剪貼簿被系統拒絕。',
+ reviewPastedTitle: '已追加今日回顧到輸入框',
+ reviewCopied: (label: string) => `已複製${label}回顧`,
+ reviewPasted: (label: string) => `已追加${label}回顧到輸入框`,
+ reviewSaved: (label: string) => `已儲存${label}回顧`,
+ reviewSaveFallback: '儲存每日回顧失敗,請稍後重試。',
+ pasteFailedTitle: '貼上失敗',
+ reviewUnavailable: '今日回顧暫時不可用,請稍後重試。',
+ diagnosticsCopiedTitle: '已複製診斷資訊',
+ diagnosticsCopiedDescription: '檢查內容後,可直接貼上到問題報告',
+ clipboardDenied: '剪貼簿不可用或被系統拒絕',
+ networkPassedTitle: '網路代理測試透過',
+ networkFailedTitle: '網路代理測試失敗',
+ genericTestFailedTitle: '測試失敗',
+ networkTestFallback: '網路代理測試暫時不可用,請稍後重試。',
+ },
+ sessionRowActions: {
+ actionFallback: '任務操作失敗,請稍後重試。',
+ flagFailedTitle: '標記任務失敗',
+ unflagFailedTitle: '取消標記失敗',
+ archiveFailedTitle: '歸檔任務失敗',
+ unarchiveFailedTitle: '恢復任務失敗',
+ renameFailedTitle: '重新命名任務失敗',
+ deleteFailedTitle: '刪除任務失敗',
+ currentConversation: '目前任務',
+ deleteTitle: (name: string) => `刪除 "${name}"`,
+ deleteDescription: '任務和全部訊息會從磁碟上永久移除。該操作不可撤銷。',
+ deleteLabel: '刪除',
+ cancelLabel: '取消',
+ deletedTitle: (name: string) => `已刪除 ${name}`,
+ deleteRestoredTitle: (name: string) => `${name} 已被恢復,未刪除`,
+ deleteSubtaskNote: () => '其普通子任務不會被刪除,將保留並移入歸檔。',
+ deleteSubtaskNoteUncertain: () => '其普通子任務(如有)不會被刪除,將保留並移入歸檔。',
+ deletedSubtaskNote: (count: number) => `${count} 個子任務已移入歸檔`,
+ bulkArchiveDescription: '歸檔後可在「設定 › 活動 › 已歸檔任務」中找回。',
+ bulkArchivedTitle: (count: number) => `已歸檔 ${count} 個任務`,
+ bulkArchiveFailedTitle: '部分任務無法歸檔',
+ bulkFailedBody: (count: number) => `還有 ${count} 個未處理成功。`,
+ },
+ skillActions: {
+ refreshSkillsFailedTitle: '重新整理技能失敗',
+ refreshSkillsFallback: '重新整理技能失敗,請稍後重試。',
+ refreshSourcesFailedTitle: '重新整理來源庫失敗',
+ refreshSourcesFallback: '重新整理來源庫失敗,請稍後重試。',
+ refreshBundledFailedTitle: '重新整理內建技能失敗',
+ refreshBundledFallback: '重新整理內建技能失敗,請稍後重試。',
+ installBundledFailedTitle: '無法安裝內建 Skill',
+ installBundledFallback: '無法安裝內建 Skill,請稍後重試。',
+ installedBundledTitle: '已安裝內建 Skill',
+ installedDescription: (id: string) => `${id}/SKILL.md 已放到目前工作區。`,
+ importSourceFailedTitle: '無法匯入 Skill 來源',
+ importSourceFallback: '無法匯入 Skill 來源,請稍後重試。',
+ importedSourceTitle: '已匯入 Skill 來源',
+ installFailedTitle: '無法安裝 Skill',
+ installFallback: '無法安裝 Skill,請稍後重試。',
+ installedTitle: '已安裝 Skill',
+ previewFailedTitle: '無法預覽 Skill 更新',
+ previewFallback: '無法預覽 Skill 更新,請稍後重試。',
+ updateFailedTitle: '無法更新 Skill',
+ updateFallback: '無法更新 Skill,請稍後重試。',
+ updatedTitle: '已更新 Skill',
+ forceUpdatedTitle: '已覆蓋更新 Skill',
+ updatedDescription: (id: string) => `${id}/SKILL.md 已更新到來源庫版本。`,
+ toggleFailedTitle: '無法切換 Skill',
+ toggleFallback: '無法切換 Skill,請稍後重試。',
+ enabledTitle: '已啟用 Skill',
+ disabledTitle: '已停用 Skill',
+ pinnedTitle: '已固定到技能上下文',
+ unpinnedTitle: '已取消固定',
+ runtimeDescription: (name: string) => `${name} 已更新目前專案的執行狀態。`,
+ deleteFailedTitle: '無法刪除 Skill',
+ deleteFallback: '無法刪除 Skill,請稍後重試。',
+ deletedTitle: '已刪除 Skill',
+ deletedDescription: (id: string) => `${id} 已移除。`,
+ openFailedTitle: '無法開啟 Skill',
+ openFallback: '無法開啟 Skill,請稍後重試。',
+ openFailures: {
+ invalid_id: 'Skill 名稱不在允許範圍內。',
+ missing: '沒有找到對應的 SKILL.md。',
+ blocked_path: 'Skill 路徑不在工作區 skills 目錄內,已阻止開啟。',
+ not_file: '目標不是一個可開啟的 SKILL.md 檔案。',
+ not_directory: '目標不是一個可開啟的目錄。',
+ open_failed: '系統開啟檔案失敗。',
+ },
+ sourceFailures: {
+ invalid_skill: '請選擇有效的 SKILL.md 檔案。',
+ already_exists: '來源庫裡已經有同名 Skill。',
+ blocked_path: '該檔案路徑不允許匯入。',
+ write_failed: '寫入來源庫失敗,請檢查檔案權限。',
+ cancelled: '已取消。',
+ },
+ installFailures: {
+ not_found: '沒有找到這個 Skill 來源。',
+ already_exists: '目前工作區已經有同名 Skill。',
+ blocked_path: '目標路徑不允許寫入。',
+ write_failed: '寫入工作區失敗,請檢查檔案權限。',
+ },
+ updateFailures: {
+ not_managed: '這個 Skill 不是受管理來源。',
+ source_missing: '來源庫中找不到對應來源。',
+ local_modified: '工作區副本已經被修改。請開啟本地檔案和來原始檔手動比較後再更新。',
+ metadata_error: 'Skill 後設資料異常,不能安全更新。',
+ blocked_path: '目標路徑不允許寫入。',
+ write_failed: '寫入工作區失敗,請檢查檔案權限。',
+ },
+ previewFailures: {
+ not_managed: '這個 Skill 不是受管理來源。',
+ source_missing: '來源庫中找不到對應來源。',
+ metadata_error: 'Skill 後設資料異常,不能安全預覽。',
+ blocked_path: '目標路徑不允許讀取。',
+ read_failed: '讀取 Skill 內容失敗,請檢查檔案權限。',
+ },
+ deleteFailures: {
+ not_found: '目前工作區找不到這個 Skill。',
+ blocked_path: 'Skill 路徑不允許刪除。',
+ blocked_scope: '專案內的 Skill 由倉庫管理,請直接在專案裡刪除。',
+ delete_failed: '刪除 Skill 失敗,請檢查檔案權限。',
+ },
+ runtimeFailures: {
+ not_found: '目前工作區找不到這個 Skill。',
+ blocked_path: 'Skill 狀態路徑不允許寫入。',
+ state_error: '目前工作區的 Skill 狀態檔案異常,需要先修復。',
+ write_failed: '寫入目前專案的 Skill 狀態失敗,請檢查檔案權限。',
+ },
+ },
+ sessionSettingsActions: {
+ bypassConfirmTitle: '切換到完全權限?',
+ bypassConfirmDescription:
+ '本地工具將直接讀寫你的檔案並存取網路,不經 Maka 的保護層。僅用於你完全信任、或已在外部隔離環境中執行的任務。',
+ bypassConfirmLabel: '開啟完全權限',
+ bypassCancelLabel: '保持自動',
+ permissionFailedTitle: '切換權限模式失敗',
+ permissionFallback: '權限模式暫時無法切換,請稍後重試。',
+ modelFailedTitle: '切換模型失敗',
+ modelFallback: '模型暫時無法切換,請稍後重試。',
+ thinkingFailedTitle: '切換思考級別失敗',
+ thinkingFallback: '思考級別暫時無法切換,請稍後重試。',
+ },
+ goalDialog: {
+ title: '設定 Goal',
+ description: 'Goal 會在每輪結束後自動續行,直到達成、判定不可行,或觸及下面的預算。隨時可在輸入框上方停止。',
+ conditionLabel: '達成條件',
+ conditionDescription: '用一句話說明什麼算做完;Maka 每輪都據此判斷。',
+ conditionPlaceholder: '例如:所有測試透過,且 lint 無告警',
+ maxIterationsLabel: '最多輪數',
+ maxIterationsDescription: '留空使用預設值。',
+ maxIterationsInvalid: (max) => `請填 1 到 ${max} 之間的整數,或留空。`,
+ tokenBudgetLabel: 'Token 預算',
+ tokenBudgetDescription: '留空表示不設 token 上限。',
+ tokenBudgetInvalid: (min) => `請填不小於 ${min} 的整數,或留空。`,
+ cancel: '取消',
+ close: '關閉',
+ submit: '開始',
+ failedFallback: '無法設定 Goal,請稍後重試。',
+ statusLabels: {
+ active: '進行中',
+ waiting: '等待中',
+ paused: '已暫停',
+ achieved: '已達成',
+ impossible: '不可行',
+ cleared: '已清除',
+ stalled: '已停滯',
+ budget_limited: '已達到 Token 預算',
+ max_iterations: '已達到最多輪數',
+ },
+ reconciledMatching: (condition, status) =>
+ `已重新讀取目前 Goal:“${condition}”(${status})。它符合你的請求,但無法確認剛才的操作是否提交。`,
+ reconciledDifferent: (condition, status) =>
+ `已重新讀取目前 Goal:“${condition}”(${status})。它與本次請求不同。`,
+ reconciledNoGoal: '已重新讀取目前狀態:目前未讀到 Goal。',
+ reconciliationUnavailable: '連線中斷後暫時無法確認目前 Goal 狀態。請關閉後重新開啟再檢查;此視窗不會重複提交。',
+ },
+ errorBoundary: {
+ copyPending: '複製中…',
+ copied: '已複製',
+ copyFailed: '複製失敗',
+ copyReport: '複製診斷資訊',
+ title: 'Maka 渲染層崩潰了',
+ description:
+ '已捕捉一次未處理的 React 例外狀況。可以重試以清除這次崩潰,或重新載入整個視窗。需要交接時請先複製診斷資訊。',
+ retry: '重試',
+ reload: '重新載入',
+ clipboardFailure: '剪貼簿無法使用或遭系統拒絕,請稍後重試。',
+ },
+ commandPalette: {
+ label: '命令面板',
+ searchLabel: '命令面板搜尋',
+ placeholder: '搜尋命令、設定項或任務…',
+ closeLabel: '關閉命令面板',
+ resultsLabel: '命令面板結果',
+ emptyTitle: '沒有符合的命令',
+ emptyDescription: '換個關鍵詞,或按 Esc 關閉。',
+ selectHint: '選擇',
+ runHint: '執行',
+ closeHint: '關閉',
+ current: '目前',
+ groups: {
+ settings: '設定',
+ permissions: '權限',
+ connections: '連線',
+ conversations: '任務',
+ },
+ staticKeywords: STATIC_COMMAND_KEYWORDS,
+ commands: ZH_STATIC_COMMANDS,
+ settingsSections: ZH_SETTINGS_SECTIONS,
+ permissionModes: {
+ explore: { label: '權限 · 只讀', hint: '讀取和搜尋直通,寫入和網路仍需確認' },
+ ask: { label: '權限 · 自動', hint: '在 Maka 的保護層內執行;需要超出目前權限範圍時再詢問' },
+ bypass: {
+ label: '權限 · 完全權限',
+ hint: '不經 Maka 的保護層,直接存取你的檔案和網路',
+ },
+ },
+ settingsCommand: (section: string) => `設定 · ${section}`,
+ testDefaultConnection: (name: string) => `測試預設連線 · ${name}`,
+ setDefaultConnection: (name: string) => `設為預設 · ${name}`,
+ testConnection: (name: string) => `測試連線 · ${name}`,
+ settingsKeywords: (section: SettingsSection, label: string) => [section, label, 'settings', '設定'],
+ permissionKeywords: (mode: PermissionMode) => [mode, 'permission', 'mode', '權限', '模式'],
+ connectionKeywords: (action: 'default' | 'test', name: string, providerType: string) => [
+ action,
+ 'connection',
+ '連線',
+ '預設',
+ '測試',
+ name,
+ providerType,
+ ],
+ },
+ keyboardHelp: {
+ title: '鍵盤快捷鍵',
+ sections: [
+ {
+ heading: '通用',
+ rows: [
+ {
+ keys: ['⌘', 'K'],
+ description: '開啟命令面板(跳任務 / 設定 / 主題等)',
+ },
+ { keys: ['?'], description: '開啟 / 關閉此快捷鍵面板' },
+ { keys: ['⌘', 'N'], description: '建立任務' },
+ { keys: ['⌘', ','], description: '開啟設定' },
+ {
+ keys: ['⌘', 'Shift', 'D'],
+ description: '複製目前上下文的診斷資訊',
+ },
+ { keys: ['Esc'], description: '關閉目前模態框' },
+ ],
+ },
+ {
+ heading: 'Composer 輸入',
+ rows: [
+ { keys: ['Enter'], description: '傳送訊息' },
+ { keys: ['Shift', 'Enter'], description: '插入換行' },
+ { keys: ['Alt', 'Enter'], description: '插入換行(備用)' },
+ ],
+ },
+ {
+ heading: '任務列表',
+ rows: [
+ { keys: ['Tab'], description: '在任務與導航之間移動焦點' },
+ { keys: ['↑', '↓'], description: '上下移動聚焦的任務' },
+ { keys: ['Home', 'End'], description: '跳到列表頂部 / 底部' },
+ { keys: ['Enter'], description: '開啟聚焦的任務' },
+ { keys: ['Delete'], description: '彈出刪除確認(永遠不靜默刪除)' },
+ { keys: ['F'], description: '聚焦任務列表搜尋框(按 Esc 清空)' },
+ ],
+ },
+ {
+ heading: '聊天區',
+ rows: [
+ { keys: ['Tab'], description: '聚焦工具活動 / 複製按鈕' },
+ { keys: ['Space', 'Enter'], description: '展開 / 摺疊工具呼叫' },
+ ],
+ },
+ {
+ heading: '面板調整',
+ rows: [
+ { keys: ['Tab'], description: '聚焦左右分割條' },
+ { keys: ['←', '→'], description: '微調任務列表寬度(±10 px)' },
+ { keys: ['Shift', '←', '→'], description: '快速調整(±50 px)' },
+ { keys: ['Home', 'End'], description: '直接拉到最小 / 最大寬度' },
+ ],
+ },
+ ],
+ },
+ chrome: {
+ windowActions: '視窗快捷操作',
+ searchConversations: '搜尋任務',
+ expandSidebar: '展開側邊欄',
+ collapseSidebar: '收起側邊欄',
+ newTask: '新任務',
+ expandWorkbar: '展開任務工作欄',
+ collapseWorkbar: '收起任務工作欄',
+ workspaceActions: '工作區輔助操作',
+ },
+ app: {
+ loadingWorkbarLabel: '正在載入任務工作欄',
+ loadingWorkbar: '正在載入任務工作欄…',
+ useSkillPrompt: (skillName: string) => `使用 ${skillName} 技能:`,
+ newConversation: '建立任務',
+ compactSuccessTitle: '上下文已壓縮',
+ compactSuccessDescription: '較早的上下文已替換為檢查點摘要。',
+ compactStartedTitle: '正在壓縮上下文',
+ compactStartedDescription: '正在將較早的上下文整理為檢查點摘要。',
+ compactUnchangedTitle: '無需壓縮',
+ compactUnchangedDescription: '任務已使用最新的檢查點。',
+ compactErrorTitle: '壓縮失敗',
+ compactErrorFallback: '任務暫時無法壓縮,請稍後重試。',
+ slashCommands: {
+ compact: { name: '壓縮上下文', description: '壓縮舊歷史並保留目前任務' },
+ graph: { name: '使用 Graph', description: '檢視、切換或單次執行 Graph' },
+ side: { name: '開啟側聊', description: '在右側開始一個具體話題' },
+ swarm: { name: '使用 Swarm', description: '檢視、切換或單次執行 Swarm' },
+ },
+ sideChatUnavailableTitle: '暫時無法開啟側邊對話',
+ sideChatUnavailableDescription: '請先在主任務中傳送一條訊息,再使用 /side。',
+ sideChatContextPendingTitle: '先處理待發送的上下文',
+ sideChatContextPendingDescription:
+ '目前 Composer 還有附件、引用或檔案 mention。請先發送或移除它們,再使用 /side。',
+ resumeStartedTitle: '已開始安全恢復',
+ resumeStartedDescription: '正在從最後一個完整執行邊界繼續',
+ resumeFailedTitle: '恢復失敗',
+ resumeFailedFallback: '無法啟動安全恢復,請檢查任務狀態後重試。',
+ goalClearFailedTitle: '停止目標失敗',
+ goalClearFailedFallback: '目標仍可能繼續執行,請立即重試。',
+ goalPauseFailedTitle: '暫停目標失敗',
+ goalPauseFailedFallback: '目標可能仍在自動續行,請立即重試。',
+ goalResumeFailedTitle: '恢復目標失敗',
+ goalResumeFailedFallback: '目標仍處於暫停狀態,請重試。',
+ appearanceLoadErrorTitle: '載入外觀設定失敗',
+ appearanceLoadErrorFallback: '外觀設定暫時無法載入,請稍後重試。',
+ memoryRefreshErrorTitle: '重新整理本地記憶狀態失敗',
+ memoryLoadErrorTitle: '載入本地記憶狀態失敗',
+ memoryErrorFallback: '本地記憶狀態暫時無法重新整理,請稍後重試。',
+ openModelSettings: '開啟設定 · 模型',
+ configureModelsOnHost: (hostName: string) =>
+ `請先在 ${hostName} 上設定模型連線。`,
+ sidebarCollapsed: '側邊欄已收起',
+ resizeConversationList: '調整任務列表寬度',
+ skipErrorTitle: '跳過失敗',
+ tryAgainLater: '請稍後重試。',
+ updateInstallFailedTitle: '無法安裝更新',
+ updateInstallFailedFallback: '請稍後重試。',
+ updateInstallManualFallback: '請稍後重試,或手動下載最新版本。',
+ updateActiveTasksTitle: '仍有任務正在執行',
+ updateActiveTasksDescription: '仍有任務正在執行。更新會中斷這些任務,是否繼續?',
+ updateActiveTasksConfirm: '仍然更新',
+ updateActiveTasksCancel: '取消',
+ updateRetryFailedTitle: '無法重新下載更新',
+ updateRetryFailedFallback: '請稍後重試,或手動下載最新版本。',
+ loading: '載入中',
+ goToModels: '去模型',
+ boundaryUnreadableTitle: '暫時讀不到這個任務的權限',
+ boundaryUnreadableDetail: '在讀到之前,這裡暫時不能輸入。可以重試,或先切換到別的任務。',
+ boundaryUnreadableRetry: '重試',
+ boundaryUnreadableRetrying: '重試中…',
+ permissionModeStreaming: '目前任務正在流式輸出,等結束後再切換權限模式。',
+ permissionModeRunning: '目前任務正在執行,等結束後再切換權限模式。',
+ permissionModeWaiting: '目前有工具呼叫正在等待確認,處理後再切換權限模式。',
+ modeChangeLoading: '會話還在載入,稍候即可切換模式。',
+ modeChanging: '模式正在切換,完成後再繼續操作。',
+ modeChangeStreaming: '目前任務正在流式輸出,等結束後再切換模式。',
+ modeChangeRunning: '目前任務正在執行,等結束後再切換模式。',
+ modeChangeWaiting: '目前有工具呼叫正在等待確認,處理後再切換模式。',
+ goalTurnActive: 'Goal 從下一輪開始生效。等目前這一輪結束後再設定。',
+ planModeFailedTitle: '切換 Plan 模式失敗',
+ planModeFallback: 'Plan 模式暫時無法切換,請稍後重試。',
+ orchestrationModeFailedTitle: '切換編排模式失敗',
+ orchestrationModeFallback: '編排模式暫時無法切換,請稍後重試。',
+ planModeExitPendingTitle: '放棄目前方案?',
+ planModeExitPendingDescription: (title: string) =>
+ `「${title}」尚未審批。退出 Plan Mode 後,該方案會標記為已放棄,但歷史記錄仍會保留。`,
+ planModeExitConfirm: '放棄並退出',
+ planModeExitCancel: '繼續規劃',
+ planModeExecutionActiveTitle: '計劃仍在執行',
+ planModeExecutionActiveDescription: '請先中斷目前執行,再進入 Plan Mode 調整方案。',
+ swarmModeEnabledTitle: 'Swarm Mode 已開啟',
+ swarmModeDisabledTitle: 'Swarm Mode 未開啟',
+ swarmModeStatusDescription: '使用 /swarm on、/swarm off,或 /swarm <任務> 單次執行。',
+ graphModeEnabledTitle: 'Graph Mode 已開啟',
+ graphModeDisabledTitle: 'Graph Mode 未開啟',
+ graphModeStatusDescription: '使用 /graph on、/graph off,或 /graph <任務> 單次執行。',
+ graphHistoryTitle: 'Graph 歷史',
+ graphHistoryDescription: '請在 Agent Graph 面板的執行輪次選單中檢視歷史記錄。',
+ resizeWorkbar: '調整任務工作欄寬度',
+ },
+ },
en: {
navigation: { settings: 'Settings' },
actions: { retry: 'Retry' },
@@ -1818,7 +2320,7 @@ export function getShellCopy(locale: UiLocale): ShellCopy {
}
export function localizedShellErrorMessage(error: unknown, fallback: string, locale: UiLocale): string {
- return locale === 'zh' ? generalizedErrorMessageChinese(error, fallback) : generalizedErrorMessage(error, fallback);
+ return generalizedErrorMessageForLocale(error, fallback, locale);
}
export function sessionSettingFailureCopy(
diff --git a/apps/desktop/src/renderer/locales/shell-remaining-copy.ts b/apps/desktop/src/renderer/locales/shell-remaining-copy.ts
index 75ace87c6e..17fb805908 100644
--- a/apps/desktop/src/renderer/locales/shell-remaining-copy.ts
+++ b/apps/desktop/src/renderer/locales/shell-remaining-copy.ts
@@ -97,6 +97,78 @@ const zhCopy = {
intentSeparator: " — ",
},
} as const;
+const zhTwCopy = {
+ scheduledTaskActions: {
+ refreshFailed: "重新整理計劃失敗",
+ refreshFallback: "重新整理定時任務失敗,請稍後重試。",
+ created: "已建立定時任務",
+ createFailed: "建立計劃失敗",
+ createFallback: "建立定時任務失敗,請稍後重試。",
+ createIncognitoBlocked: "隱身模式開啟時不能建立定時任務。",
+ saved: "已儲存定時任務",
+ saveFailed: "儲存計劃失敗",
+ saveFallback: "儲存定時任務失敗,請稍後重試。",
+ enabled: "已啟用任務",
+ paused: "已暫停任務",
+ updateFailed: "更新計劃失敗",
+ updateFallback: "更新定時任務失敗,請稍後重試。",
+ triggered: "已觸發定時任務",
+ triggerFailed: "觸發計劃失敗",
+ triggerFallback: "觸發定時任務失敗,請稍後重試。",
+ snoozed: "已延後 10 分鐘",
+ snoozeFailed: "延後計劃失敗",
+ snoozeFallback: "延後定時任務失敗,請稍後重試。",
+ task: "定時任務",
+ clearTitle: (name: string) => `清空 “${name}” 的執行記錄`,
+ clearDescription: "定時任務本身會保留;只清空最近執行記錄和最近狀態。",
+ clear: "清空記錄",
+ cancel: "取消",
+ cleared: "已清空執行記錄",
+ clearFailed: "清空記錄失敗",
+ clearFallback: "清空定時任務記錄失敗,請稍後重試。",
+ deleteTitle: (name: string) => `刪除 “${name}”`,
+ deleteDescription: "該任務和最近執行記錄會被刪除。該操作不可撤銷。",
+ delete: "刪除",
+ deleted: "已刪除定時任務",
+ deleteFailed: "刪除計劃失敗",
+ deleteFallback: "刪除定時任務失敗,請稍後重試。",
+ },
+ dailyReview: {
+ yesterday: "昨天",
+ today: "今天",
+ followSettings: "跟隨設定",
+ unavailable: "每日回顧生成暫不可用",
+ historyUnavailable: "每日回顧歷史暫不可用",
+ archiveMissing: "找不到每日回顧報告",
+ settingsUnavailable: "每日回顧設定暫不可用",
+ },
+ connections: {
+ refreshFailed: "重新整理模型連線失敗",
+ refreshFallback: "模型連線暫時無法重新整理,請稍後重試。",
+ },
+ tasks: { loadFailed: "待辦載入失敗,請重試。" },
+ projects: { ungrouped: "未歸屬專案" },
+ models: { unavailable: "目前不可用" },
+ overlays: {
+ loadingSettings: "正在載入設定",
+ loadingSettingsProgress: "正在載入設定…",
+ },
+ notifications: {
+ scheduledTask: "定時任務",
+ viewScheduledTasks: "檢視定時任務",
+ },
+ previousMainProcessInterruption: {
+ title: "Maka 已恢復",
+ description: "上次退出未完成。",
+ copyDiagnostics: "複製報告",
+ },
+ conversationExport: {
+ exported: (date: string) => `由 Maka 於 ${date} 匯出。`,
+ you: "你",
+ toolCalls: "工具呼叫",
+ intentSeparator: " — ",
+ },
+} as const;
export type ShellRemainingCopy = WidenCopy<typeof zhCopy>;
@@ -179,7 +251,11 @@ const enCopy: ShellRemainingCopy = {
},
};
-const COPY = { zh: zhCopy, en: enCopy } satisfies UiCatalog<ShellRemainingCopy>;
+const COPY = {
+ 'zh-CN': zhCopy,
+ 'zh-TW': zhTwCopy,
+ en: enCopy,
+} satisfies UiCatalog<ShellRemainingCopy>;
export function getShellRemainingCopy(locale: UiLocale): ShellRemainingCopy {
return COPY[locale];
diff --git a/apps/desktop/src/renderer/mcp-catalog.ts b/apps/desktop/src/renderer/mcp-catalog.ts
index 417c4c001e..9d5b834ecf 100644
--- a/apps/desktop/src/renderer/mcp-catalog.ts
+++ b/apps/desktop/src/renderer/mcp-catalog.ts
@@ -240,9 +240,27 @@ const MCP_CATALOG_ENGLISH_COPY: Record<McpCatalogId, McpCatalogLocalizedCopy> =
'sequential-thinking': { name: 'Sequential thinking', description: 'Provide revisable, verifiable structured reasoning for complex problems.', category: 'Reasoning and planning', setupLabel: undefined },
};
+const MCP_CATALOG_TRADITIONAL_COPY: Record<McpCatalogId, McpCatalogLocalizedCopy> = {
+ dingtalk: { name: 'DingTalk', description: '管理聯絡人、行事曆、任務和協作資料。', category: '通訊', setupLabel: '需要 Client ID 和 Client Secret' },
+ feishu: { name: '飛書', description: '存取飛書文件、行事曆、訊息和 OpenAPI。', category: '通訊', setupLabel: '需要 App ID 和 App Secret' },
+ slack: { name: 'Slack', description: '傳送訊息、管理頻道,並在 Slack 工作區協作。', category: '通訊', setupLabel: '需要 Bot Token 和 Team ID' },
+ line: { name: 'LINE', description: '透過 LINE Bot Messaging API 傳送和管理訊息。', category: '通訊', setupLabel: '需要 Channel Access Token' },
+ notion: { name: 'Notion', description: '搜尋、讀取和更新 Notion 工作區。', category: '知識與文件', setupLabel: '需要登入授權' },
+ 'macos-apps': { name: 'macOS 應用程式', description: '透過原生系統權限連接「行事曆」與「提醒事項」。', category: '系統與生產力', setupLabel: undefined },
+ 'google-calendar': { name: 'Google 日曆', description: '管理活動、建立會議並檢查可用時間。', category: '系統與生產力', setupLabel: '需要 OAuth 憑證檔案' },
+ figma: { name: 'Figma', description: '讀取設計檔案、元件和開發交付資料。', category: '設計與開發', setupLabel: '需要 Personal Access Token' },
+ vercel: { name: 'Vercel', description: '檢查專案、部署、記錄和平台文件。', category: '設計與開發', setupLabel: '需要登入授權' },
+ supabase: { name: 'Supabase', description: '管理資料庫、專案設定、移轉和 Edge Functions。', category: '設計與開發', setupLabel: '需要登入授權' },
+ filesystem: { name: '本機檔案', description: '在選取的資料夾中安全地讀取、寫入和管理檔案。', category: '檔案與知識', setupLabel: '需要選擇允許存取的資料夾' },
+ memory: { name: '持久記憶', description: '使用結構化知識圖譜記住實體、關係和重要事實。', category: '檔案與知識', setupLabel: undefined },
+ playwright: { name: '瀏覽器自動化', description: '讓 Maka 透過 Playwright 讀取和操作實際網頁。', category: '設計與開發', setupLabel: undefined },
+ 'sequential-thinking': { name: '循序思考', description: '為複雜問題提供可修正、可驗證的結構化推理。', category: '推理與規劃', setupLabel: undefined },
+};
+
export function getMcpCatalog(locale: UiLocale): McpCatalogEntry[] {
- if (locale === 'zh') return MCP_CATALOG;
- return MCP_CATALOG.map((entry) => ({ ...entry, ...MCP_CATALOG_ENGLISH_COPY[entry.id] }));
+ if (locale === 'zh-CN') return MCP_CATALOG;
+ const copy = locale === 'zh-TW' ? MCP_CATALOG_TRADITIONAL_COPY : MCP_CATALOG_ENGLISH_COPY;
+ return MCP_CATALOG.map((entry) => ({ ...entry, ...copy[entry.id] }));
}
export function catalogEntryMatches(entry: McpCatalogEntry, normalizedQuery: string): boolean {
diff --git a/apps/desktop/src/renderer/model-catalog-choices.ts b/apps/desktop/src/renderer/model-catalog-choices.ts
index d1d8b09a46..fdd90df6c3 100644
--- a/apps/desktop/src/renderer/model-catalog-choices.ts
+++ b/apps/desktop/src/renderer/model-catalog-choices.ts
@@ -48,7 +48,7 @@ export function buildCatalogRecommendedDefaultModel(providerType: ProviderType):
export function buildCatalogDailyReviewModelOptions(
connections: readonly (LlmConnection & HostResolvedConnectionCatalog)[],
currentModelKey: string,
- locale: UiLocale = 'zh',
+ locale: UiLocale = 'zh-CN',
): Array<readonly [string, string]> {
const current = parseDailyReviewModelKey(currentModelKey);
const candidates: Array<{ key: string; label: string; safeSourceLabel: string }> = [];
diff --git a/apps/desktop/src/renderer/model-connection-errors.ts b/apps/desktop/src/renderer/model-connection-errors.ts
index fe3fdf5568..1581398750 100644
--- a/apps/desktop/src/renderer/model-connection-errors.ts
+++ b/apps/desktop/src/renderer/model-connection-errors.ts
@@ -46,7 +46,7 @@ export function noRealConnectionReasonFromEvent(event: Extract<SessionEvent, { t
).reason;
}
-export function noRealConnectionSetupDescription(reason: string | undefined, locale: UiLocale = 'zh'): string {
+export function noRealConnectionSetupDescription(reason: string | undefined, locale: UiLocale = 'zh-CN'): string {
const copy = getDesktopConversationCopy(locale).model;
return reason && Object.hasOwn(copy.configurationReason, reason)
? copy.configurationReason[reason as ChatConfigurationReason]
@@ -55,7 +55,7 @@ export function noRealConnectionSetupDescription(reason: string | undefined, loc
export function sessionEventErrorMessage(
event: Extract<SessionEvent, { type: 'error' }>,
- locale: UiLocale = 'zh',
+ locale: UiLocale = 'zh-CN',
): string {
if (isNoRealConnectionEvent(event)) {
return noRealConnectionSetupDescription(noRealConnectionReasonFromEvent(event), locale);
@@ -69,7 +69,7 @@ export function sessionEventErrorMessage(
export function modelSetupToastCopy(
reason: string | undefined,
fallback: string,
- locale: UiLocale = 'zh',
+ locale: UiLocale = 'zh-CN',
): { title: string; description: string } {
const copy = getDesktopConversationCopy(locale).model;
if (reason === 'connection_missing') {
diff --git a/apps/desktop/src/renderer/session-error-presentation.ts b/apps/desktop/src/renderer/session-error-presentation.ts
index f3bccf1e7b..9dd8985ab5 100644
--- a/apps/desktop/src/renderer/session-error-presentation.ts
+++ b/apps/desktop/src/renderer/session-error-presentation.ts
@@ -25,7 +25,7 @@ import { getDesktopConversationCopy } from './locales/conversation-copy.js';
* runtime. Unknown reasons intentionally return undefined so callers can use
* their existing safe fallback instead of displaying raw provider text.
*/
-export function describeSessionErrorReason(reason: string | undefined, locale: UiLocale = 'zh'): string | undefined {
+export function describeSessionErrorReason(reason: string | undefined, locale: UiLocale = 'zh-CN'): string | undefined {
const copy = getDesktopConversationCopy(locale).turnError;
switch (reason?.toLowerCase()) {
case 'context_overflow':
diff --git a/apps/desktop/src/renderer/session-status-presentation.ts b/apps/desktop/src/renderer/session-status-presentation.ts
index c039d73e30..99088d068f 100644
--- a/apps/desktop/src/renderer/session-status-presentation.ts
+++ b/apps/desktop/src/renderer/session-status-presentation.ts
@@ -97,7 +97,7 @@ export function normalizeSessionSummaryForDisplay<T extends SessionSummary>(sess
* the UI; they just fall through to the catch-all until the mapping
* is extended.
*/
-export function describeTurnErrorClass(errorClass: string | undefined, locale: UiLocale = 'zh'): string {
+export function describeTurnErrorClass(errorClass: string | undefined, locale: UiLocale = 'zh-CN'): string {
const copy = getDesktopConversationCopy(locale).turnError;
if (!errorClass) return copy.unknown;
const reasonDescription = describeSessionErrorReason(errorClass, locale);
@@ -169,7 +169,7 @@ export interface FailedTurnExecutionState {
*/
export function describeFailedTurnExecutionState(
state: FailedTurnExecutionState,
- locale: UiLocale = 'zh',
+ locale: UiLocale = 'zh-CN',
): string | undefined {
const copy = getDesktopConversationCopy(locale).turnError.executionState;
if (state.erroredToolCount > 0) return copy.erroredTool;
diff --git a/apps/desktop/src/renderer/settings/about-settings-page.tsx b/apps/desktop/src/renderer/settings/about-settings-page.tsx
index aad940a6a5..ad7aa8ed1d 100644
--- a/apps/desktop/src/renderer/settings/about-settings-page.tsx
+++ b/apps/desktop/src/renderer/settings/about-settings-page.tsx
@@ -125,7 +125,10 @@ export function AboutSettingsPage(props: { onOpenKeyboardHelp?(): void }) {
const status = await window.maka.app.checkForUpdates();
if (aboutPageMountedRef.current) setUpdateStatus(status);
if (status.state === 'error') {
- toast.error(copy.updateCheckFailed, copy.updateCheckFailedDetail(status.message));
+ toast.error(
+ copy.updateCheckFailed,
+ copy.updateCheckFailedDetail(settingsActionErrorMessage(status.message, locale)),
+ );
}
} catch (error) {
if (aboutPageMountedRef.current) {
@@ -198,7 +201,10 @@ export function AboutSettingsPage(props: { onOpenKeyboardHelp?(): void }) {
window floor the sentence needs the full width. */}
{isDevBuild ? null : (
<HStack gap={3} justify="between" wrap="wrap">
- <Text type="body">{aboutUpdateStatusDetail(updateStatus, copy)}</Text>
+ <Text type="body">{aboutUpdateStatusDetail(updateStatus, copy, {
+ isDevBuild,
+ errorDetail: (message) => settingsActionErrorMessage(message, locale),
+ })}</Text>
<Button
variant="secondary"
size="sm"
diff --git a/apps/desktop/src/renderer/settings/about-update-status.ts b/apps/desktop/src/renderer/settings/about-update-status.ts
index 6e39e3b532..dd7c71e93a 100644
--- a/apps/desktop/src/renderer/settings/about-update-status.ts
+++ b/apps/desktop/src/renderer/settings/about-update-status.ts
@@ -69,6 +69,10 @@ export function aboutChannelFacts(
export function aboutUpdateStatusDetail(
status: AppUpdateStatus | null,
copy: AboutCopy,
+ options: {
+ readonly isDevBuild: boolean;
+ readonly errorDetail?: (message: string) => string;
+ },
): string {
if (!status || status.state === 'idle') return copy.updateIdle;
if (status.state === 'checking') return copy.checkingForUpdates;
@@ -80,5 +84,7 @@ export function aboutUpdateStatusDetail(
if (status.state === 'verifying') return copy.updateVerifying(status.latestVersion);
if (status.state === 'downloaded') return copy.updateDownloaded(status.latestVersion);
if (status.state === 'installing') return copy.updateInstalling(status.latestVersion);
- return copy.updateCheckFailedDetail(status.message);
+ return copy.updateCheckFailedDetail(
+ options.errorDetail ? options.errorDetail(status.message) : status.message,
+ );
}
diff --git a/apps/desktop/src/renderer/settings/bot-chat-detail.tsx b/apps/desktop/src/renderer/settings/bot-chat-detail.tsx
index 2e443a042e..ae9ee1aeaf 100644
--- a/apps/desktop/src/renderer/settings/bot-chat-detail.tsx
+++ b/apps/desktop/src/renderer/settings/bot-chat-detail.tsx
@@ -305,7 +305,7 @@ export function BotChatChannelDetail(props: {
title={detailCopy.latestFailure}
description={(
<span className="settingsBotBannerDescription">
- {locale === 'zh' ? viewState.currentError : detailCopy.latestFailureDetail}
+ {locale === 'zh-CN' ? viewState.currentError : detailCopy.latestFailureDetail}
</span>
)} />
)}
@@ -415,7 +415,7 @@ export function BotChatChannelDetail(props: {
if (snapshot.warning) {
toast.warning(
detailCopy.credentialsSaved(providerPresentation.label),
- locale === 'zh' ? snapshot.warning : detailCopy.savedButNotConnected,
+ locale === 'zh-CN' ? snapshot.warning : detailCopy.savedButNotConnected,
);
return;
}
@@ -635,7 +635,7 @@ function BotAllowedUserIdsField(props: {
if (!same) props.onChange(next);
};
const warning = invalidEntries.length > 0
- ? `${copy.invalidUsers(invalidEntries.slice(0, 3).join(locale === 'zh' ? '、' : ', '))}${invalidEntries.length > 3 ? copy.moreInvalid(invalidEntries.length) : ''}`
+ ? `${copy.invalidUsers(invalidEntries.slice(0, 3).join(locale !== 'en' ? '、' : ', '))}${invalidEntries.length > 3 ? copy.moreInvalid(invalidEntries.length) : ''}`
: undefined;
return (
@@ -653,7 +653,7 @@ function BotAllowedUserIdsField(props: {
);
}
-function botConnectionLabel(connection: BotStatus['connection'], locale: 'zh' | 'en'): string {
+function botConnectionLabel(connection: BotStatus['connection'], locale: 'zh-CN' | 'zh-TW' | 'en'): string {
const copy = getBotSettingsCopy(locale).status;
switch (connection) {
case 'polling': return copy.polling;
diff --git a/apps/desktop/src/renderer/settings/bot-chat-overview.tsx b/apps/desktop/src/renderer/settings/bot-chat-overview.tsx
index f3f096fe99..b2e248a006 100644
--- a/apps/desktop/src/renderer/settings/bot-chat-overview.tsx
+++ b/apps/desktop/src/renderer/settings/bot-chat-overview.tsx
@@ -145,7 +145,7 @@ function botOverviewDetail(
currentError: string | undefined,
fallback: string,
liveOperational: boolean,
- locale: 'zh' | 'en',
+ locale: 'zh-CN' | 'zh-TW' | 'en',
): ReactNode {
const copy = getBotSettingsCopy(locale).overview;
const identity = status?.identity?.username ?? status?.identity?.displayName;
@@ -157,7 +157,7 @@ function botOverviewDetail(
</>
);
}
- if (currentError) return locale === 'zh' ? currentError : fallback;
+ if (currentError) return locale === 'zh-CN' ? currentError : fallback;
if (status?.reason) return botStatusDetail(status, locale);
return fallback;
}
diff --git a/apps/desktop/src/renderer/settings/bot-chat-shared.tsx b/apps/desktop/src/renderer/settings/bot-chat-shared.tsx
index 101d069aa7..0a8b7a7ff9 100644
--- a/apps/desktop/src/renderer/settings/bot-chat-shared.tsx
+++ b/apps/desktop/src/renderer/settings/bot-chat-shared.tsx
@@ -60,7 +60,7 @@ export const BOT_LABELS: Record<BotProvider, { support: 'runtime' | 'credentials
slack: { support: 'runtime' },
};
-export function botReadinessCopyForSupport(support: 'runtime' | 'credentials' | 'planned', readiness: BotReadinessState, locale: UiLocale = 'zh') {
+export function botReadinessCopyForSupport(support: 'runtime' | 'credentials' | 'planned', readiness: BotReadinessState, locale: UiLocale = 'zh-CN') {
const copy = getBotSettingsCopy(locale);
if (support === 'planned') return copy.planned;
return copy.readiness[readiness] ?? copy.readiness.scaffolded;
@@ -98,7 +98,7 @@ export function BotBrandLogo(props: { provider: BotProvider; size?: 'compact' |
export type BotPendingActionName = 'test' | 'connect' | 'restart' | 'disconnect';
export type BotPendingAction = { provider: BotProvider; action: BotPendingActionName };
-export function botStatusDetail(status: BotStatus, locale: UiLocale = 'zh'): string {
+export function botStatusDetail(status: BotStatus, locale: UiLocale = 'zh-CN'): string {
const copy = getBotSettingsCopy(locale).status;
switch (status.reason) {
case 'disabled': return copy.disabled;
diff --git a/apps/desktop/src/renderer/settings/bot-onboarding-modal.tsx b/apps/desktop/src/renderer/settings/bot-onboarding-modal.tsx
index 05d99c09af..90496c3070 100644
--- a/apps/desktop/src/renderer/settings/bot-onboarding-modal.tsx
+++ b/apps/desktop/src/renderer/settings/bot-onboarding-modal.tsx
@@ -251,7 +251,7 @@ function statusCopy(
starting: boolean,
error: string | null,
copy: BotSettingsCopy['onboarding']['providers'][BotOnboardingProvider],
- locale: 'zh' | 'en' = 'zh',
+ locale: 'zh-CN' | 'zh-TW' | 'en' = 'zh-CN',
): string {
const shared = getBotSettingsCopy(locale).onboarding;
if (starting) return shared.generating;
@@ -263,12 +263,12 @@ function statusCopy(
// PR1197 review (P0-3): honour the honest "saved but not connected" notice
// instead of claiming a healthy connection.
case 'connected': return snapshot.warning
- ? (locale === 'zh' ? snapshot.warning : shared.connectedWarning)
+ ? (locale === 'zh-CN' ? snapshot.warning : shared.connectedWarning)
: shared.connected(getBotSettingsCopy(locale).providers[snapshot.provider].label);
case 'expired': return shared.expired;
case 'denied': return shared.denied;
case 'cancelled': return shared.cancelled;
- case 'error': return locale === 'zh' ? (snapshot.error ?? shared.failed) : shared.failed;
+ case 'error': return locale === 'zh-CN' ? (snapshot.error ?? shared.failed) : shared.failed;
default: return shared.preparing;
}
}
diff --git a/apps/desktop/src/renderer/settings/daily-review-settings-page.tsx b/apps/desktop/src/renderer/settings/daily-review-settings-page.tsx
index 57fdc46fcf..d8e244ecc8 100644
--- a/apps/desktop/src/renderer/settings/daily-review-settings-page.tsx
+++ b/apps/desktop/src/renderer/settings/daily-review-settings-page.tsx
@@ -39,7 +39,7 @@ function buildDailyReviewModelOptions(
connections: readonly ProjectedLlmConnection[],
currentModelKey: string,
copy: DailyReviewSettingsCopy,
- locale: 'zh' | 'en',
+ locale: 'zh-CN' | 'zh-TW' | 'en',
): Array<{ value: string; label: string }> {
return [
{ value: DAILY_REVIEW_DEFAULT_MODEL_VALUE, label: copy.defaultModel },
diff --git a/apps/desktop/src/renderer/settings/data-settings-page.tsx b/apps/desktop/src/renderer/settings/data-settings-page.tsx
index 517563eb0f..7ab341d52d 100644
--- a/apps/desktop/src/renderer/settings/data-settings-page.tsx
+++ b/apps/desktop/src/renderer/settings/data-settings-page.tsx
@@ -220,7 +220,7 @@ export function DataSettingsPage(props: {
if (res.ok) {
toast.success(copy.imported, summarizeImportResult(res.result, copy));
} else if (res.reason !== 'canceled') {
- const detail = res.message && (locale === 'zh' || !/[\u3400-\u9fff]/u.test(res.message))
+ const detail = res.message && (locale === 'zh-CN' || !/[\u3400-\u9fff]/u.test(res.message))
? res.message
: copy.invalidFile;
toast.error(copy.importFailed, detail, undefined, diagnosticTarget);
diff --git a/apps/desktop/src/renderer/settings/memory-settings-view-model.ts b/apps/desktop/src/renderer/settings/memory-settings-view-model.ts
index 2359bd57cb..d64ef28e79 100644
--- a/apps/desktop/src/renderer/settings/memory-settings-view-model.ts
+++ b/apps/desktop/src/renderer/settings/memory-settings-view-model.ts
@@ -36,7 +36,7 @@ export function deriveMemorySettingsViewModel(input: {
query: string;
copy?: MemorySettingsCopy;
}) {
- const copy = input.copy ?? getMemorySettingsCopy('zh');
+ const copy = input.copy ?? getMemorySettingsCopy('zh-CN');
const effective = input.state ?? {
path: '',
enabled: input.localMemorySettings.enabled,
diff --git a/apps/desktop/src/renderer/settings/permission-center-page.tsx b/apps/desktop/src/renderer/settings/permission-center-page.tsx
index 0bf7a7c1fb..ec4c6517d7 100644
--- a/apps/desktop/src/renderer/settings/permission-center-page.tsx
+++ b/apps/desktop/src/renderer/settings/permission-center-page.tsx
@@ -705,7 +705,7 @@ function localizedCapabilityLabel(capability: CapabilitySnapshot, locale: UiLoca
}
function localizedSnapshotText(value: string | undefined, locale: UiLocale): string | undefined {
- if (!value || (locale === 'en' && /[\u3400-\u9fff]/u.test(value))) return undefined;
+ if (!value || (locale !== 'zh-CN' && /[\u3400-\u9fff]/u.test(value))) return undefined;
return value;
}
@@ -714,7 +714,7 @@ function localizedCapabilityGuidance(
locale: UiLocale,
copy: PermissionCenterCopy,
): readonly string[] {
- return capability.guidance.filter((item) => locale === 'zh' || !/[\u3400-\u9fff]/u.test(item));
+ return capability.guidance.filter((item) => locale === 'zh-CN' || !/[\u3400-\u9fff]/u.test(item));
}
function featureTone(state: CapabilitySnapshot['feature']['state']): StatusSemantic {
diff --git a/apps/desktop/src/renderer/settings/personalization-settings-section.tsx b/apps/desktop/src/renderer/settings/personalization-settings-section.tsx
index 1ba7fedee8..7328350266 100644
--- a/apps/desktop/src/renderer/settings/personalization-settings-section.tsx
+++ b/apps/desktop/src/renderer/settings/personalization-settings-section.tsx
@@ -28,13 +28,12 @@
// another. It is one section of someone else's page: named, filed, and shaped
// as one.
import { useEffect, useRef, useState } from 'react';
-import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core';
import { SettingsField, SettingsRow, SettingsSection } from './settings-section';
import { SettingsExpandableRow } from './settings-expandable-row';
import { getSettingsSharedCopy } from '../locales/settings-shared-copy';
import type { AppSettings, PersonalizationSettings, UpdateAppSettingsResult } from '@maka/core/settings';
import type { UiLocalePreference } from '@maka/core/ui-locale';
-import { TextArea, TextInput, useMountedRef, useToast, useUiLocale } from '@maka/ui';
+import { Selector, TextArea, TextInput, useMountedRef, useToast, useUiLocale } from '@maka/ui';
import { settingsActionErrorMessage } from './settings-error-copy';
import { getSettingsPreferencesCopy } from '../locales/settings-preferences-copy.js';
import { useOptionalRuntimeHostSettingsTarget } from './runtime-host-settings-target.js';
@@ -232,22 +231,22 @@ export function PersonalizationSettingsSection(props: {
) : null}
{/*
PR-LANG-PREF-0 (WAWQAQ msg `edc9cb41` + kenji `7e532892`
- acceptance criteria): 自动 / 中文 / English. User explicit
+ acceptance criteria): automatic / Simplified Chinese / Traditional Chinese / English. User explicit
choice wins over the temporary auto -> zh fallback;
e2e-fixture override wins over both (deterministic baselines).
*/}
<SettingsRow
label={copy.interfaceLanguage}
description={copy.interfaceLanguageHelp}
- end={<SegmentedControl
- value={uiLocale}
- onChange={(next) => persistLocale(next as UiLocalePreference)}
- label={copy.interfaceLanguage}
- >
- {copy.localeOptions.map(([value, label]) => (
- <SegmentedControlItem key={value} value={value} label={label} />
- ))}
- </SegmentedControl>}
+ end={
+ <Selector
+ label={copy.interfaceLanguage}
+ isLabelHidden
+ value={uiLocale}
+ options={copy.localeOptions.map(([value, label]) => ({ value, label }))}
+ onChange={(next) => persistLocale(next as UiLocalePreference)}
+ />
+ }
/>
{props.runtimeHostSettingsAvailable ? <SettingsField>
{/* No fixed height style: it lands as inline style on the wrapper,
diff --git a/apps/desktop/src/renderer/settings/provider-add-form.tsx b/apps/desktop/src/renderer/settings/provider-add-form.tsx
index dbab46b5bd..1588995847 100644
--- a/apps/desktop/src/renderer/settings/provider-add-form.tsx
+++ b/apps/desktop/src/renderer/settings/provider-add-form.tsx
@@ -181,7 +181,7 @@ export function AddProviderForm(props: {
if (issue.field === 'slug') {
return issue.reason === 'duplicate'
? copy.duplicateSlug
- : locale === 'zh'
+ : locale === 'zh-CN'
? issue.detail
: copy.invalidSlug;
}
diff --git a/apps/desktop/src/renderer/settings/provider-catalog-page.tsx b/apps/desktop/src/renderer/settings/provider-catalog-page.tsx
index a4ea2202c8..b910072874 100644
--- a/apps/desktop/src/renderer/settings/provider-catalog-page.tsx
+++ b/apps/desktop/src/renderer/settings/provider-catalog-page.tsx
@@ -34,6 +34,7 @@ import {
type ProviderType,
} from '@maka/core/provider-registry';
import { PROVIDER_REGISTRY, type LlmConnection } from '@maka/core/llm-connections';
+import type { UiLocale } from '@maka/core/ui-locale';
import { Button, TextInput, useUiLocale } from '@maka/ui';
import { AddProviderForm } from './provider-add-form';
import { ProviderLogo, providerDisplay } from './provider-display';
@@ -204,7 +205,7 @@ export function ProviderCatalogPage(props: {
*/
function ProviderCatalogRows(props: {
title?: string;
- locale: 'zh' | 'en';
+ locale: UiLocale;
cards: readonly OAuthCard[];
providers: readonly ProviderType[];
onPick(target: SetupTarget): void;
@@ -317,7 +318,7 @@ export function ProviderSetupPage(props: {
function providersMatching(
source: readonly ProviderType[],
query: string,
- locale: 'zh' | 'en',
+ locale: UiLocale,
group?: ProviderCatalogGroup,
): ProviderType[] {
const normalizedQuery = query.toLocaleLowerCase();
diff --git a/apps/desktop/src/renderer/settings/provider-connection-status.ts b/apps/desktop/src/renderer/settings/provider-connection-status.ts
index b74f2c3bab..e8f22c4cc6 100644
--- a/apps/desktop/src/renderer/settings/provider-connection-status.ts
+++ b/apps/desktop/src/renderer/settings/provider-connection-status.ts
@@ -62,7 +62,7 @@ export interface ConnectionChipStatus {
* readiness, fixed to credential-only language. Matches the doc warning
* at SettingsModal `验证通过 ≠ 运行可用`.
*/
-export function connectionChipStatus(connection: LlmConnection, locale: UiLocale = 'zh'): ConnectionChipStatus | null {
+export function connectionChipStatus(connection: LlmConnection, locale: UiLocale = 'zh-CN'): ConnectionChipStatus | null {
const copy = getProviderSettingsCopy(locale).shared.connectionStatuses;
// Ahead of every other branch, including needs_reauth: a retired provider
// has no sign-in left to return to, so any repair-shaped status here would
diff --git a/apps/desktop/src/renderer/settings/provider-display-copy.ts b/apps/desktop/src/renderer/settings/provider-display-copy.ts
index 077cf893e9..14de01deda 100644
--- a/apps/desktop/src/renderer/settings/provider-display-copy.ts
+++ b/apps/desktop/src/renderer/settings/provider-display-copy.ts
@@ -48,217 +48,270 @@ export interface ProviderCopy {
}
export const UNKNOWN_PROVIDER_DESCRIPTION = {
- zh: '该 provider 在当前版本未注册。',
+ 'zh-CN': '该 provider 在当前版本未注册。',
+ 'zh-TW': '該 provider 在目前版本未註冊。',
en: 'This provider is not registered in the current build.',
} satisfies UiCatalog<string>;
export const PROVIDER_DISPLAY_COPY = {
'kimi-coding-plan': {
- zh: { name: 'Kimi Coding Plan', description: '月之暗面 · Anthropic 兼容', badge: 'Coding' },
+ 'zh-CN': { name: 'Kimi Coding Plan', description: '月之暗面 · Anthropic 兼容', badge: 'Coding' },
+ 'zh-TW': { name: 'Kimi Coding Plan', description: '月之暗面 · Anthropic 相容', badge: 'Coding' },
en: { name: 'Kimi Coding Plan', description: 'Moonshot · Anthropic-compatible', badge: 'Coding' },
},
'minimax-coding-plan': {
- zh: { name: 'MiniMax Coding Plan', description: 'MiniMax Coding 套餐 · Anthropic 兼容', badge: 'Coding' },
+ 'zh-CN': { name: 'MiniMax Coding Plan', description: 'MiniMax Coding 套餐 · Anthropic 兼容', badge: 'Coding' },
+ 'zh-TW': { name: 'MiniMax Coding Plan', description: 'MiniMax Coding 套餐 · Anthropic 相容', badge: 'Coding' },
en: { name: 'MiniMax Coding Plan', description: 'MiniMax coding plan · Anthropic-compatible', badge: 'Coding' },
},
deepseek: {
- zh: { name: 'DeepSeek', description: 'DeepSeek 官方接入', badge: 'API' },
+ 'zh-CN': { name: 'DeepSeek', description: 'DeepSeek 官方接入', badge: 'API' },
+ 'zh-TW': { name: 'DeepSeek', description: 'DeepSeek 官方 API 連線', badge: 'API' },
en: { name: 'DeepSeek', description: 'Official DeepSeek API access.', badge: 'API' },
},
moonshot: {
- zh: { name: 'Moonshot', description: 'Moonshot 官方接入', badge: 'API' },
+ 'zh-CN': { name: 'Moonshot', description: 'Moonshot 官方接入', badge: 'API' },
+ 'zh-TW': { name: 'Moonshot', description: 'Moonshot 官方 API 連線', badge: 'API' },
en: { name: 'Moonshot', description: 'Official Moonshot API access.', badge: 'API' },
},
'zai-coding-plan': {
- zh: { name: 'Z.AI Coding Plan', description: '智谱 · OpenAI 兼容', badge: 'Coding' },
+ 'zh-CN': { name: 'Z.AI Coding Plan', description: '智谱 · OpenAI 兼容', badge: 'Coding' },
+ 'zh-TW': { name: 'Z.AI Coding Plan', description: '智譜 · OpenAI 相容', badge: 'Coding' },
en: { name: 'Z.AI Coding Plan', description: 'Zhipu · OpenAI-compatible', badge: 'Coding' },
},
MiniMax: {
- zh: { name: 'MiniMax', description: 'MiniMax · Anthropic 兼容', badge: 'API' },
+ 'zh-CN': { name: 'MiniMax', description: 'MiniMax · Anthropic 兼容', badge: 'API' },
+ 'zh-TW': { name: 'MiniMax', description: 'MiniMax · Anthropic 相容', badge: 'API' },
en: { name: 'MiniMax', description: 'MiniMax · Anthropic-compatible', badge: 'API' },
},
'MiniMax-cn': {
- zh: { name: 'MiniMax 中国站', description: 'MiniMax 中国站 · Anthropic 兼容', badge: 'API' },
+ 'zh-CN': { name: 'MiniMax 中国站', description: 'MiniMax 中国站 · Anthropic 兼容', badge: 'API' },
+ 'zh-TW': { name: 'MiniMax 中國站', description: 'MiniMax 中國站 · Anthropic 相容', badge: 'API' },
en: { name: 'MiniMax China', description: 'MiniMax China · Anthropic-compatible', badge: 'API' },
},
siliconflow: {
- zh: { name: 'SiliconFlow', description: '硅基流动多模型 API,支持精确模型 ID。', badge: '聚合' },
+ 'zh-CN': { name: 'SiliconFlow', description: '硅基流动多模型 API,支持精确模型 ID。', badge: '聚合' },
+ 'zh-TW': { name: 'SiliconFlow', description: '矽基流動多模型 API,支援精確模型 ID。', badge: '聚合' },
en: { name: 'SiliconFlow', description: 'Hosted multi-model API with exact upstream model ids.', badge: 'Aggregator' },
},
anthropic: {
- zh: { name: 'Anthropic', description: 'Anthropic 官方接入', badge: 'API' },
+ 'zh-CN': { name: 'Anthropic', description: 'Anthropic 官方接入', badge: 'API' },
+ 'zh-TW': { name: 'Anthropic', description: 'Anthropic 官方 API 連線', badge: 'API' },
en: { name: 'Anthropic', description: 'Official Anthropic API access.', badge: 'API' },
},
openai: {
- zh: { name: 'OpenAI', description: 'OpenAI 官方接入', badge: 'API' },
+ 'zh-CN': { name: 'OpenAI', description: 'OpenAI 官方接入', badge: 'API' },
+ 'zh-TW': { name: 'OpenAI', description: 'OpenAI 官方 API 連線', badge: 'API' },
en: { name: 'OpenAI', description: 'Official OpenAI API access.', badge: 'API' },
},
google: {
- zh: { name: 'Google Gemini', description: 'Google AI Studio 接入', badge: 'API' },
+ 'zh-CN': { name: 'Google Gemini', description: 'Google AI Studio 接入', badge: 'API' },
+ 'zh-TW': { name: 'Google Gemini', description: 'Google AI Studio API 連線', badge: 'API' },
en: { name: 'Google Gemini', description: 'Google AI Studio API access.', badge: 'API' },
},
xai: {
- zh: { name: 'xAI', description: 'xAI 官方接入,Grok 系列模型', badge: 'API' },
+ 'zh-CN': { name: 'xAI', description: 'xAI 官方接入,Grok 系列模型', badge: 'API' },
+ 'zh-TW': { name: 'xAI', description: 'xAI 官方 API 連線,支援 Grok 系列模型', badge: 'API' },
en: { name: 'xAI', description: 'Official xAI API access for Grok models.', badge: 'API' },
},
zai: {
- zh: { name: 'Z.AI', description: '智谱官方接入,GLM 系列模型', badge: 'API' },
+ 'zh-CN': { name: 'Z.AI', description: '智谱官方接入,GLM 系列模型', badge: 'API' },
+ 'zh-TW': { name: 'Z.AI', description: '智譜官方 API 連線,支援 GLM 系列模型', badge: 'API' },
en: { name: 'Z.AI', description: 'Official Z.AI API access for GLM models.', badge: 'API' },
},
xiaomi: {
- zh: { name: 'Xiaomi', description: '小米官方接入,MiMo 系列模型', badge: 'API' },
+ 'zh-CN': { name: 'Xiaomi', description: '小米官方接入,MiMo 系列模型', badge: 'API' },
+ 'zh-TW': { name: 'Xiaomi', description: '小米官方 API 連線,支援 MiMo 系列模型', badge: 'API' },
en: { name: 'Xiaomi', description: 'Official Xiaomi API access for MiMo models.', badge: 'API' },
},
'xiaomi-token-plan-cn': {
- zh: { name: 'Xiaomi Token Plan 中国', description: '小米 MiMo Token Plan 订阅 · 中国 · 编码工具', badge: 'Token' },
+ 'zh-CN': { name: 'Xiaomi Token Plan 中国', description: '小米 MiMo Token Plan 订阅 · 中国 · 编码工具', badge: 'Token' },
+ 'zh-TW': { name: 'Xiaomi Token Plan 中國', description: '小米 MiMo Token Plan 訂閱 · 中國 · 編碼工具', badge: 'Token' },
en: { name: 'Xiaomi Token Plan (China)', description: 'Xiaomi MiMo Token Plan subscription (China) for coding tools.', badge: 'Token' },
},
'xiaomi-token-plan-sgp': {
- zh: { name: 'Xiaomi Token Plan 新加坡', description: '小米 MiMo Token Plan 订阅 · 新加坡 · 编码工具', badge: 'Token' },
+ 'zh-CN': { name: 'Xiaomi Token Plan 新加坡', description: '小米 MiMo Token Plan 订阅 · 新加坡 · 编码工具', badge: 'Token' },
+ 'zh-TW': { name: 'Xiaomi Token Plan 新加坡', description: '小米 MiMo Token Plan 訂閱 · 新加坡 · 編碼工具', badge: 'Token' },
en: { name: 'Xiaomi Token Plan (Singapore)', description: 'Xiaomi MiMo Token Plan subscription (Singapore) for coding tools.', badge: 'Token' },
},
'xiaomi-token-plan-ams': {
- zh: { name: 'Xiaomi Token Plan 欧洲', description: '小米 MiMo Token Plan 订阅 · 欧洲 · 编码工具', badge: 'Token' },
+ 'zh-CN': { name: 'Xiaomi Token Plan 欧洲', description: '小米 MiMo Token Plan 订阅 · 欧洲 · 编码工具', badge: 'Token' },
+ 'zh-TW': { name: 'Xiaomi Token Plan 歐洲', description: '小米 MiMo Token Plan 訂閱 · 歐洲 · 編碼工具', badge: 'Token' },
en: { name: 'Xiaomi Token Plan (Europe)', description: 'Xiaomi MiMo Token Plan subscription (Europe) for coding tools.', badge: 'Token' },
},
cerebras: {
- zh: { name: 'Cerebras', description: '高速推理托管开源模型', badge: 'API' },
+ 'zh-CN': { name: 'Cerebras', description: '高速推理托管开源模型', badge: 'API' },
+ 'zh-TW': { name: 'Cerebras', description: '高速推理託管開源模型', badge: 'API' },
en: { name: 'Cerebras', description: 'Fast hosted open-model inference.', badge: 'API' },
},
mistral: {
- zh: { name: 'Mistral', description: 'Mistral 官方接入', badge: 'API' },
+ 'zh-CN': { name: 'Mistral', description: 'Mistral 官方接入', badge: 'API' },
+ 'zh-TW': { name: 'Mistral', description: 'Mistral 官方 API 連線', badge: 'API' },
en: { name: 'Mistral', description: 'Official Mistral API access.', badge: 'API' },
},
togetherai: {
- zh: { name: 'Together AI', description: '托管开源模型 API', badge: 'API' },
+ 'zh-CN': { name: 'Together AI', description: '托管开源模型 API', badge: 'API' },
+ 'zh-TW': { name: 'Together AI', description: '託管開源模型 API', badge: 'API' },
en: { name: 'Together AI', description: 'Hosted open models over one API.', badge: 'API' },
},
ollama: {
- zh: { name: 'Ollama', description: '本机运行 · 离线可用', badge: 'Local' },
+ 'zh-CN': { name: 'Ollama', description: '本机运行 · 离线可用', badge: 'Local' },
+ 'zh-TW': { name: 'Ollama', description: '本機執行 · 離線可用', badge: 'Local' },
en: { name: 'Ollama', description: 'Runs locally · works offline', badge: 'Local' },
},
'lm-studio': {
- zh: { name: 'LM Studio', description: '本机 LM Studio 服务 · 离线可用', badge: 'Local' },
+ 'zh-CN': { name: 'LM Studio', description: '本机 LM Studio 服务 · 离线可用', badge: 'Local' },
+ 'zh-TW': { name: 'LM Studio', description: '本機 LM Studio 服務 · 離線可用', badge: 'Local' },
en: { name: 'LM Studio', description: 'Local models served by LM Studio.', badge: 'Local' },
},
localai: {
- zh: { name: 'LocalAI', description: '本机 LocalAI 服务,可选密钥保护', badge: 'Local' },
+ 'zh-CN': { name: 'LocalAI', description: '本机 LocalAI 服务,可选密钥保护', badge: 'Local' },
+ 'zh-TW': { name: 'LocalAI', description: '本機 LocalAI 服務,可選金鑰保護', badge: 'Local' },
en: { name: 'LocalAI', description: 'Local models served by LocalAI, with optional API key.', badge: 'Local' },
},
'openai-compatible': {
- zh: { name: '自定义中转站(OpenAI Chat)', description: 'OpenAI Chat Completions 兼容中转站、代理服务或自部署网关。', badge: '中转' },
+ 'zh-CN': { name: '自定义中转站(OpenAI Chat)', description: 'OpenAI Chat Completions 兼容中转站、代理服务或自部署网关。', badge: '中转' },
+ 'zh-TW': { name: '自訂中轉站(OpenAI Chat)', description: 'OpenAI Chat Completions 相容中轉站、代理服務或自部署閘道器。', badge: '中轉' },
en: { name: 'Custom relay (OpenAI Chat)', description: 'OpenAI Chat Completions-compatible relay, proxy, or self-hosted gateway.', badge: 'Relay' },
},
'openai-responses-compatible': {
- zh: { name: '自定义中转站(OpenAI Responses)', description: 'OpenAI Responses API 兼容中转站、代理服务或自部署网关。', badge: 'Responses' },
+ 'zh-CN': { name: '自定义中转站(OpenAI Responses)', description: 'OpenAI Responses API 兼容中转站、代理服务或自部署网关。', badge: 'Responses' },
+ 'zh-TW': { name: '自訂中轉站(OpenAI Responses)', description: 'OpenAI Responses API 相容中轉站、代理服務或自部署閘道器。', badge: 'Responses' },
en: { name: 'Custom relay (OpenAI Responses)', description: 'OpenAI Responses-compatible relay, proxy, or self-hosted gateway.', badge: 'Responses' },
},
'anthropic-compatible': {
- zh: { name: '自定义中转站(Anthropic)', description: 'Anthropic Messages 兼容中转站、代理服务或自部署网关。', badge: 'Anthropic' },
+ 'zh-CN': { name: '自定义中转站(Anthropic)', description: 'Anthropic Messages 兼容中转站、代理服务或自部署网关。', badge: 'Anthropic' },
+ 'zh-TW': { name: '自訂中轉站(Anthropic)', description: 'Anthropic Messages 相容中轉站、代理服務或自部署閘道器。', badge: 'Anthropic' },
en: { name: 'Custom relay (Anthropic)', description: 'Anthropic Messages-compatible relay, proxy, or self-hosted gateway.', badge: 'Anthropic' },
},
'fireworks-ai': {
- zh: { name: 'Fireworks AI', description: 'Serverless 开源模型托管', badge: 'API' },
+ 'zh-CN': { name: 'Fireworks AI', description: 'Serverless 开源模型托管', badge: 'API' },
+ 'zh-TW': { name: 'Fireworks AI', description: 'Serverless 開源模型託管', badge: 'API' },
en: { name: 'Fireworks AI', description: 'Serverless open models with exact Fireworks model paths.', badge: 'API' },
},
nvidia: {
- zh: { name: 'NVIDIA', description: 'NVIDIA 官方托管模型接入', badge: 'API' },
+ 'zh-CN': { name: 'NVIDIA', description: 'NVIDIA 官方托管模型接入', badge: 'API' },
+ 'zh-TW': { name: 'NVIDIA', description: 'NVIDIA 官方託管模型服務', badge: 'API' },
en: { name: 'NVIDIA', description: 'NVIDIA-hosted models API access.', badge: 'API' },
},
'tencent-tokenhub': {
- zh: { name: 'Tencent TokenHub', description: '腾讯云 TokenHub 按量接入,混元等模型', badge: 'API' },
+ 'zh-CN': { name: 'Tencent TokenHub', description: '腾讯云 TokenHub 按量接入,混元等模型', badge: 'API' },
+ 'zh-TW': { name: 'Tencent TokenHub', description: '騰訊雲 TokenHub 按量計費模型服務,包含混元等模型', badge: 'API' },
en: { name: 'Tencent TokenHub', description: 'Tencent Cloud TokenHub pay-as-you-go access.', badge: 'API' },
},
stepfun: {
- zh: { name: 'StepFun 中国站', description: '阶跃星辰官方接入 · 中国站', badge: 'API' },
+ 'zh-CN': { name: 'StepFun 中国站', description: '阶跃星辰官方接入 · 中国站', badge: 'API' },
+ 'zh-TW': { name: 'StepFun 中國站', description: '階躍星辰官方 API 連線 · 中國站', badge: 'API' },
en: { name: 'StepFun (China)', description: 'Official StepFun China API access.', badge: 'API' },
},
'tencent-coding-plan': {
- zh: { name: 'Tencent Coding Plan', description: '腾讯云 Coding 套餐 · OpenAI 兼容', badge: 'Coding' },
+ 'zh-CN': { name: 'Tencent Coding Plan', description: '腾讯云 Coding 套餐 · OpenAI 兼容', badge: 'Coding' },
+ 'zh-TW': { name: 'Tencent Coding Plan', description: '騰訊雲 Coding 套餐 · OpenAI 相容', badge: 'Coding' },
en: { name: 'Tencent Coding Plan', description: 'Tencent Cloud coding plan · OpenAI-compatible', badge: 'Coding' },
},
'stepfun-ai': {
- zh: { name: 'StepFun 国际站', description: '阶跃星辰官方接入 · 国际站', badge: 'API' },
+ 'zh-CN': { name: 'StepFun 国际站', description: '阶跃星辰官方接入 · 国际站', badge: 'API' },
+ 'zh-TW': { name: 'StepFun 國際站', description: '階躍星辰官方 API 連線 · 國際站', badge: 'API' },
en: { name: 'StepFun (Global)', description: 'Official StepFun Global API access.', badge: 'API' },
},
'volcengine-ark': {
- zh: { name: '火山方舟', description: '火山引擎官方接入,豆包等模型', badge: 'API' },
+ 'zh-CN': { name: '火山方舟', description: '火山引擎官方接入,豆包等模型', badge: 'API' },
+ 'zh-TW': { name: '火山方舟', description: '火山引擎官方 API 連線,包含豆包等模型', badge: 'API' },
en: { name: 'Volcengine Ark (China)', description: 'Volcengine Ark direct API access in China.', badge: 'API' },
},
'volcengine-coding-plan': {
- zh: { name: '火山方舟 Coding Plan', description: '火山引擎 Coding 订阅 · OpenAI 兼容', badge: 'Coding' },
+ 'zh-CN': { name: '火山方舟 Coding Plan', description: '火山引擎 Coding 订阅 · OpenAI 兼容', badge: 'Coding' },
+ 'zh-TW': { name: '火山方舟 Coding Plan', description: '火山引擎 Coding 訂閱 · OpenAI 相容', badge: 'Coding' },
en: { name: 'Volcengine Ark Coding Plan (China)', description: 'Volcengine Ark coding subscription · OpenAI-compatible', badge: 'Coding' },
},
'volcengine-agent-plan': {
- zh: { name: '火山方舟 Agent Plan', description: '火山引擎 Agent 订阅 · Responses API', badge: 'Agent' },
+ 'zh-CN': { name: '火山方舟 Agent Plan', description: '火山引擎 Agent 订阅 · Responses API', badge: 'Agent' },
+ 'zh-TW': { name: '火山方舟 Agent Plan', description: '火山引擎 Agent 訂閱 · Responses API', badge: 'Agent' },
en: { name: 'Volcengine Ark Agent Plan (China)', description: 'Volcengine Ark agent subscription · Responses API', badge: 'Agent' },
},
'tencent-token-plan': {
- zh: { name: 'Tencent Token Plan', description: '腾讯云 Token 套餐,个人智能体与编码工具', badge: 'Token' },
+ 'zh-CN': { name: 'Tencent Token Plan', description: '腾讯云 Token 套餐,个人智能体与编码工具', badge: 'Token' },
+ 'zh-TW': { name: 'Tencent Token Plan', description: '騰訊雲 Token 套餐,個人智慧體與編碼工具', badge: 'Token' },
en: { name: 'Tencent Token Plan', description: 'Tencent Cloud token plan for personal agents and coding tools.', badge: 'Token' },
},
'stepfun-step-plan': {
- zh: { name: 'StepFun Step Plan 中国站', description: '阶跃星辰订阅套餐 · 中国站', badge: 'Plan' },
+ 'zh-CN': { name: 'StepFun Step Plan 中国站', description: '阶跃星辰订阅套餐 · 中国站', badge: 'Plan' },
+ 'zh-TW': { name: 'StepFun Step Plan 中國站', description: '階躍星辰訂閱套餐 · 中國站', badge: 'Plan' },
en: { name: 'StepFun Step Plan (China)', description: 'StepFun China subscription for coding and agent tools.', badge: 'Plan' },
},
deepinfra: {
- zh: { name: 'DeepInfra', description: '开源模型托管推理 · OpenAI 兼容', badge: 'API' },
+ 'zh-CN': { name: 'DeepInfra', description: '开源模型托管推理 · OpenAI 兼容', badge: 'API' },
+ 'zh-TW': { name: 'DeepInfra', description: '開源模型託管推理 · OpenAI 相容', badge: 'API' },
en: { name: 'DeepInfra', description: 'Hosted open-model inference · OpenAI-compatible', badge: 'API' },
},
cohere: {
- zh: { name: 'Cohere', description: 'Cohere 官方接入', badge: 'API' },
+ 'zh-CN': { name: 'Cohere', description: 'Cohere 官方接入', badge: 'API' },
+ 'zh-TW': { name: 'Cohere', description: 'Cohere 官方 API 連線', badge: 'API' },
en: { name: 'Cohere', description: 'Official Cohere Chat API access.', badge: 'API' },
},
vercel: {
- zh: { name: 'Vercel AI Gateway', description: '一个密钥接入多家托管模型', badge: '网关' },
+ 'zh-CN': { name: 'Vercel AI Gateway', description: '一个密钥接入多家托管模型', badge: '网关' },
+ 'zh-TW': { name: 'Vercel AI Gateway', description: '使用一組金鑰連線多家託管模型', badge: '閘道器' },
en: { name: 'Vercel AI Gateway', description: 'One API key for hosted models with exact creator/model ids.', badge: 'Gateway' },
},
'stepfun-ai-step-plan': {
- zh: { name: 'StepFun Step Plan 国际站', description: '阶跃星辰订阅套餐 · 国际站', badge: 'Plan' },
+ 'zh-CN': { name: 'StepFun Step Plan 国际站', description: '阶跃星辰订阅套餐 · 国际站', badge: 'Plan' },
+ 'zh-TW': { name: 'StepFun Step Plan 國際站', description: '階躍星辰訂閱套餐 · 國際站', badge: 'Plan' },
en: { name: 'StepFun Step Plan (Global)', description: 'StepFun Global subscription for coding and agent tools.', badge: 'Plan' },
},
'cloudflare-workers-ai': {
- zh: { name: 'Cloudflare Workers AI', description: 'Cloudflare 托管模型,账户级接入', badge: 'API' },
+ 'zh-CN': { name: 'Cloudflare Workers AI', description: 'Cloudflare 托管模型,账户级接入', badge: 'API' },
+ 'zh-TW': { name: 'Cloudflare Workers AI', description: 'Cloudflare 託管模型,使用帳號層級連線', badge: 'API' },
en: { name: 'Cloudflare Workers AI', description: 'Cloudflare-hosted models over the account-scoped API.', badge: 'API' },
},
huggingface: {
- zh: { name: 'Hugging Face', description: 'Inference Providers 路由,聚合多家托管模型', badge: '路由' },
+ 'zh-CN': { name: 'Hugging Face', description: 'Inference Providers 路由,聚合多家托管模型', badge: '路由' },
+ 'zh-TW': { name: 'Hugging Face', description: 'Inference Providers 路由,聚合多家託管模型', badge: '路由' },
en: { name: 'Hugging Face', description: 'Inference Providers router across hosted models.', badge: 'Router' },
},
'ollama-cloud': {
- zh: { name: 'Ollama Cloud', description: 'Ollama 官方云端托管模型', badge: 'API' },
+ 'zh-CN': { name: 'Ollama Cloud', description: 'Ollama 官方云端托管模型', badge: 'API' },
+ 'zh-TW': { name: 'Ollama Cloud', description: 'Ollama 官方雲端託管模型', badge: 'API' },
en: { name: 'Ollama Cloud', description: 'Ollama-hosted cloud models over the official remote API.', badge: 'API' },
},
zenmux: {
- zh: { name: 'ZenMux', description: '模型路由网关,一个密钥接入多家模型', badge: '网关' },
+ 'zh-CN': { name: 'ZenMux', description: '模型路由网关,一个密钥接入多家模型', badge: '网关' },
+ 'zh-TW': { name: 'ZenMux', description: '模型路由閘道器,使用一組金鑰連線多家模型', badge: '閘道器' },
en: { name: 'ZenMux', description: 'One API key for routed models with exact creator/model ids.', badge: 'Gateway' },
},
opencode: {
- zh: { name: 'OpenCode Zen', description: '面向编码智能体的按量模型精选', badge: 'Plan' },
+ 'zh-CN': { name: 'OpenCode Zen', description: '面向编码智能体的按量模型精选', badge: 'Plan' },
+ 'zh-TW': { name: 'OpenCode Zen', description: '面向編碼智慧體的按量模型精選', badge: 'Plan' },
en: { name: 'OpenCode Zen', description: 'Curated pay-as-you-go models for coding agents.', badge: 'Plan' },
},
'opencode-go': {
- zh: { name: 'OpenCode Go', description: '低价订阅制的开源编码模型精选', badge: 'Plan' },
+ 'zh-CN': { name: 'OpenCode Go', description: '低价订阅制的开源编码模型精选', badge: 'Plan' },
+ 'zh-TW': { name: 'OpenCode Go', description: '低價訂閱制的開源編碼模型精選', badge: 'Plan' },
en: { name: 'OpenCode Go', description: 'Low-cost subscription to curated open coding models.', badge: 'Plan' },
},
'opencode-free': {
- zh: { name: 'OpenCode Free', description: '免费匿名 OpenCode Zen 模型,无需密钥,按 IP 限速', badge: 'Free' },
+ 'zh-CN': { name: 'OpenCode Free', description: '免费匿名 OpenCode Zen 模型,无需密钥,按 IP 限速', badge: 'Free' },
+ 'zh-TW': { name: 'OpenCode Free', description: '免費匿名 OpenCode Zen 模型,無需金鑰,按 IP 限速', badge: 'Free' },
en: { name: 'OpenCode Free', description: 'Free anonymous OpenCode Zen models — no API key, IP-limited.', badge: 'Free' },
},
groq: {
- zh: { name: 'Groq', description: 'LPU 高速推理托管开源模型', badge: 'API' },
+ 'zh-CN': { name: 'Groq', description: 'LPU 高速推理托管开源模型', badge: 'API' },
+ 'zh-TW': { name: 'Groq', description: 'LPU 高速推理託管開源模型', badge: 'API' },
en: { name: 'Groq', description: 'Ultra-fast LPU-hosted open models.', badge: 'API' },
},
openrouter: {
- zh: { name: 'OpenRouter', description: '一个密钥接入各大模型厂商 · OpenAI 兼容', badge: '聚合' },
+ 'zh-CN': { name: 'OpenRouter', description: '一个密钥接入各大模型厂商 · OpenAI 兼容', badge: '聚合' },
+ 'zh-TW': { name: 'OpenRouter', description: '使用一組金鑰連線各大模型廠商 · OpenAI 相容', badge: '聚合' },
en: { name: 'OpenRouter', description: 'One API key across all major model labs · OpenAI-compatible', badge: 'Aggregator' },
},
alibaba: {
- zh: { name: 'Alibaba', description: '阿里云百炼接入,通义千问 Qwen 模型', badge: 'API' },
+ 'zh-CN': { name: 'Alibaba', description: '阿里云百炼接入,通义千问 Qwen 模型', badge: 'API' },
+ 'zh-TW': { name: 'Alibaba', description: '阿里雲百鍊 API 連線,支援通義千問 Qwen 模型', badge: 'API' },
en: { name: 'Alibaba', description: 'Alibaba Cloud API access for Qwen models.', badge: 'API' },
},
'alibaba-cn': {
- zh: { name: 'Alibaba 中国站', description: '阿里云百炼中国站接入,通义千问 Qwen 模型', badge: 'API' },
+ 'zh-CN': { name: 'Alibaba 中国站', description: '阿里云百炼中国站接入,通义千问 Qwen 模型', badge: 'API' },
+ 'zh-TW': { name: 'Alibaba 中國站', description: '阿里雲百鍊中國站 API 連線,支援通義千問 Qwen 模型', badge: 'API' },
en: {
name: 'Alibaba (China)',
description: 'Alibaba Cloud China-platform API access for Qwen models.',
@@ -266,37 +319,45 @@ export const PROVIDER_DISPLAY_COPY = {
},
},
'alibaba-coding-plan-cn': {
- zh: { name: 'Alibaba Coding Plan 中国站', description: '阿里云百炼 Coding Plan 订阅 · 中国站', badge: 'Plan' },
+ 'zh-CN': { name: 'Alibaba Coding Plan 中国站', description: '阿里云百炼 Coding Plan 订阅 · 中国站', badge: 'Plan' },
+ 'zh-TW': { name: 'Alibaba Coding Plan 中國站', description: '阿里雲百鍊 Coding Plan 訂閱 · 中國站', badge: 'Plan' },
en: { name: 'Alibaba Coding Plan (China)', description: 'Alibaba Cloud Model Studio Coding Plan for interactive coding tools · China.', badge: 'Plan' },
},
'alibaba-coding-plan': {
- zh: { name: 'Alibaba Coding Plan 国际站', description: '阿里云百炼 Coding Plan 订阅 · 国际站', badge: 'Plan' },
+ 'zh-CN': { name: 'Alibaba Coding Plan 国际站', description: '阿里云百炼 Coding Plan 订阅 · 国际站', badge: 'Plan' },
+ 'zh-TW': { name: 'Alibaba Coding Plan 國際站', description: '阿里雲百鍊 Coding Plan 訂閱 · 國際站', badge: 'Plan' },
en: { name: 'Alibaba Coding Plan', description: 'Alibaba Cloud Model Studio Coding Plan for interactive coding tools.', badge: 'Plan' },
},
'alibaba-token-plan-cn': {
- zh: { name: 'Alibaba Token Plan(团队版)', description: '阿里云百炼 Token Plan 订阅,交互式智能体与编码工具 · 北京', badge: 'Token' },
+ 'zh-CN': { name: 'Alibaba Token Plan(团队版)', description: '阿里云百炼 Token Plan 订阅,交互式智能体与编码工具 · 北京', badge: 'Token' },
+ 'zh-TW': { name: 'Alibaba Token Plan(團隊版)', description: '阿里雲百鍊 Token Plan 訂閱,互動式智慧體與編碼工具 · 北京', badge: 'Token' },
en: { name: 'Alibaba Token Plan (China)', description: 'Alibaba Cloud Model Studio Token Plan for interactive agents and coding tools, Beijing region.', badge: 'Token' },
},
'alibaba-token-plan': {
- zh: { name: 'Alibaba Token Plan(团队版)', description: '阿里云百炼 Token Plan 订阅,交互式智能体与编码工具 · 新加坡', badge: 'Token' },
+ 'zh-CN': { name: 'Alibaba Token Plan(团队版)', description: '阿里云百炼 Token Plan 订阅,交互式智能体与编码工具 · 新加坡', badge: 'Token' },
+ 'zh-TW': { name: 'Alibaba Token Plan(團隊版)', description: '阿里雲百鍊 Token Plan 訂閱,互動式智慧體與編碼工具 · 新加坡', badge: 'Token' },
en: { name: 'Alibaba Token Plan', description: 'Alibaba Cloud Model Studio Token Plan for interactive agents and coding tools, Singapore region.', badge: 'Token' },
},
// OAuth account providers (not in CATALOG_PROVIDER_TYPES; shown in the
// accounts section and on connection rows).
'github-copilot': {
- zh: { name: 'GitHub Copilot', description: 'GitHub Copilot 订阅接入,复用本机 GitHub 登录', badge: 'Account' },
+ 'zh-CN': { name: 'GitHub Copilot', description: 'GitHub Copilot 订阅接入,复用本机 GitHub 登录', badge: 'Account' },
+ 'zh-TW': { name: 'GitHub Copilot', description: '透過 GitHub Copilot 訂閱連線,沿用本機 GitHub 登入', badge: 'Account' },
en: { name: 'GitHub Copilot', description: 'GitHub Copilot subscription access using an existing GitHub login.', badge: 'Account' },
},
'claude-subscription': {
- zh: { name: 'Claude Subscription', description: 'Claude Pro / Max 订阅账号登录;登录后自动成为可用模型连接。' },
+ 'zh-CN': { name: 'Claude Subscription', description: 'Claude Pro / Max 订阅账号登录;登录后自动成为可用模型连接。' },
+ 'zh-TW': { name: 'Claude Subscription', description: 'Claude Pro / Max 訂閱帳號登入;登入後自動成為可用模型連線。' },
en: { name: 'Claude Subscription', description: 'Sign in with a Claude Pro / Max subscription; it becomes an available model connection once signed in.' },
},
'openai-codex': {
- zh: { name: 'OpenAI OAuth', description: 'ChatGPT / Codex 账号登录;登录后自动成为可用模型连接。' },
+ 'zh-CN': { name: 'OpenAI OAuth', description: 'ChatGPT / Codex 账号登录;登录后自动成为可用模型连接。' },
+ 'zh-TW': { name: 'OpenAI OAuth', description: 'ChatGPT / Codex 帳號登入;登入後自動成為可用模型連線。' },
en: { name: 'OpenAI OAuth', description: 'Sign in with a ChatGPT / Codex account; it becomes an available model connection once signed in.' },
},
'xai-oauth': {
- zh: { name: 'xAI OAuth', description: '使用 SuperGrok 或 X Premium 账号登录。', badge: 'Account' },
+ 'zh-CN': { name: 'xAI OAuth', description: '使用 SuperGrok 或 X Premium 账号登录。', badge: 'Account' },
+ 'zh-TW': { name: 'xAI OAuth', description: '使用 SuperGrok 或 X Premium 帳號登入。', badge: 'Account' },
en: { name: 'xAI OAuth', description: 'Sign in with SuperGrok or X Premium.', badge: 'Account' },
},
} satisfies Record<ProviderType, UiCatalog<ProviderCopy>>;
diff --git a/apps/desktop/src/renderer/settings/provider-oauth-section.tsx b/apps/desktop/src/renderer/settings/provider-oauth-section.tsx
index a5c16a7924..3f144ded6b 100644
--- a/apps/desktop/src/renderer/settings/provider-oauth-section.tsx
+++ b/apps/desktop/src/renderer/settings/provider-oauth-section.tsx
@@ -29,6 +29,7 @@ import {
} from '@maka/ui';
import {
getProviderSettingsCopy,
+ subscriptionResultMessage,
type ConnectionOAuthProviderBridge,
type ConnectionsBridge,
type ProviderSettingsCopy,
@@ -36,7 +37,6 @@ import {
import {
useOAuthLoginFlow,
subscriptionActionErrorMessage,
- subscriptionResultMessage,
type OAuthAuthorizationFlowBridge,
type OAuthConnectionIdentity,
type SubscriptionSnapshot,
diff --git a/apps/desktop/src/renderer/settings/providers-panel.tsx b/apps/desktop/src/renderer/settings/providers-panel.tsx
index 19f02b68a0..239125b90c 100644
--- a/apps/desktop/src/renderer/settings/providers-panel.tsx
+++ b/apps/desktop/src/renderer/settings/providers-panel.tsx
@@ -40,6 +40,7 @@ import {
type ProjectedLlmConnection,
type ProviderType,
} from '@maka/core/llm-connections';
+import type { UiLocale } from '@maka/core/ui-locale';
import { dotForStatus, useMountedRef, useUiLocale } from '@maka/ui';
import { settingsActionErrorMessage } from './settings-error-copy';
import { connectionChipStatus } from './provider-connection-status';
@@ -645,7 +646,7 @@ function ProvidersPanelContent({ bridge, apiKeyOnboardingBridge, initialPage = '
}
/** Provider · models · default model — the row's second line, and the detail's subtitle. */
-function connectionSubtitle(connection: IdentifiedLlmConnection, locale: 'zh' | 'en'): string {
+function connectionSubtitle(connection: IdentifiedLlmConnection, locale: UiLocale): string {
const copy = getProviderSettingsCopy(locale).panel;
const providerName = providerDisplay(connection.providerType, locale).name;
const enabledCount = connectionEnabledModelIds(connection).length;
diff --git a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
index 14e8316f7d..586eb442ad 100644
--- a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
+++ b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
@@ -155,6 +155,8 @@ export function RuntimeHostManagementDialog(props: {
useState(createWebRtcStunPolicyDraft);
const nextDirectoryRootId = useRef(1);
const logsRef = useRef<HTMLPreElement>(null);
+ const localizedError = (message: string): string =>
+ settingsActionErrorMessage(new Error(message), locale);
const target = props.target;
useEffect(() => {
@@ -188,7 +190,7 @@ export function RuntimeHostManagementDialog(props: {
reconcileDirectoryPolicy(response.service);
shouldLoadUpdatePolicy = response.service.state !== 'not_installed';
}
- else if (response.kind === 'error') setError(response.error.message);
+ else if (response.kind === 'error') setError(localizedError(response.error.message));
else setUninstalledRoot(response.retainedStateRoot);
} catch (failure) {
if (!disposed) setError(settingsActionErrorMessage(failure, locale));
@@ -255,9 +257,10 @@ export function RuntimeHostManagementDialog(props: {
setConfirmation({ kind: 'restart' });
return;
}
+ const message = settingsActionErrorMessage(new Error(response.error.message), locale);
setUpdatePolicy(undefined);
- setError(response.error.message);
- toast.error(copy.managementActionFailed, response.error.message);
+ setError(message);
+ toast.error(copy.managementActionFailed, message);
return;
}
if (response.kind === 'uninstalled') {
@@ -374,9 +377,10 @@ export function RuntimeHostManagementDialog(props: {
allowInterruptActiveTasks,
);
if (response.kind === 'error') {
+ const message = localizedError(response.error.message);
setUpdatePolicy(undefined);
- setError(response.error.message);
- toast.error(copy.managementActionFailed, response.error.message);
+ setError(message);
+ toast.error(copy.managementActionFailed, message);
return;
}
if (response.kind === 'uninstalled') {
@@ -461,8 +465,9 @@ export function RuntimeHostManagementDialog(props: {
allowInterruptActiveTasks,
);
if (response.kind === 'error') {
- setError(response.error.message);
- toast.error(copy.managementActionFailed, response.error.message);
+ const message = localizedError(response.error.message);
+ setError(message);
+ toast.error(copy.managementActionFailed, message);
return;
}
if (response.kind === 'uninstalled' || response.action !== 'configure') {
@@ -544,9 +549,10 @@ export function RuntimeHostManagementDialog(props: {
try {
const response = await window.maka.runtimeHostManagement.reconcileUpdate(target.id);
if (response.kind === 'error') {
+ const message = localizedError(response.error.message);
setUpdatePolicy(undefined);
- setUpdatePolicyError(response.error.message);
- toast.error(copy.managementActionFailed, response.error.message);
+ setUpdatePolicyError(message);
+ toast.error(copy.managementActionFailed, message);
return;
}
setLastUpdateOutcome(response.reconciliation);
@@ -586,9 +592,9 @@ export function RuntimeHostManagementDialog(props: {
function applyReconnectWarning(
reconnectError: { readonly message: string } | undefined,
): void {
- setReconnectWarning(reconnectError?.message);
+ setReconnectWarning(reconnectError ? localizedError(reconnectError.message) : undefined);
if (reconnectError) {
- toast.warning(copy.managementReconnectFailed, reconnectError.message);
+ toast.warning(copy.managementReconnectFailed, localizedError(reconnectError.message));
}
}
diff --git a/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx b/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx
index 73d8d2161e..49fbb8ac3f 100644
--- a/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx
+++ b/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx
@@ -59,6 +59,7 @@ import {
} from './runtime-host-management-dialog.js';
import {
PeerMeshPeerIdButton,
+ getRuntimeHostPeerMeshCopy,
RuntimeHostAddComputerMenu,
RuntimeHostConnectionCodeDialog,
RuntimeHostPairingRecoveryButton,
@@ -92,6 +93,7 @@ export function RuntimeHostProfilesSection(props: {
}) {
const locale = useUiLocale();
const copy = getSettingsProjectsCopy(locale).runtimeHost;
+ const peerMeshCopy = getRuntimeHostPeerMeshCopy(locale);
const pairingActionCopy: RuntimeHostPairingActionCopy = {
retry: copy.resolvePairingRecovery,
retryFailed: copy.resolvePairingRecoveryFailed,
@@ -195,7 +197,7 @@ export function RuntimeHostProfilesSection(props: {
if (result.kind === "unavailable") {
toast.error(
copy.selectFailed,
- result.message,
+ settingsActionErrorMessage(result.message, locale),
undefined,
{ profileId },
);
@@ -668,11 +670,9 @@ export function RuntimeHostProfilesSection(props: {
<PeerMeshPeerIdButton
peerId={profile.transport.reachability.lease.peerId}
displayValue={abbreviatePeerId(profile.transport.reachability.lease.peerId)}
- copyLabel={locale.startsWith('zh')
- ? `复制完整 Peer ID:${profile.transport.reachability.lease.peerId}`
- : `Copy full Peer ID: ${profile.transport.reachability.lease.peerId}`}
- copiedTitle={locale.startsWith('zh') ? 'Peer ID 已复制' : 'Peer ID copied'}
- failedTitle={locale.startsWith('zh') ? '无法复制 Peer ID' : 'Could not copy Peer ID'}
+ copyLabel={peerMeshCopy.copyPeerId(profile.transport.reachability.lease.peerId)}
+ copiedTitle={peerMeshCopy.peerIdCopied}
+ failedTitle={peerMeshCopy.peerIdCopyFailed}
errorMessage={(error) => settingsActionErrorMessage(error, locale)}
className="settingsRuntimeHostPeerId"
/>
@@ -695,11 +695,13 @@ export function RuntimeHostProfilesSection(props: {
<Badge variant="neutral" label={copy.unavailable} />
) : null}
{entry.peerPath ? (
- <Tooltip content={peerPathDetail(entry.peerPath, locale)}>
+ <Tooltip content={peerPathDetail(entry.peerPath, peerMeshCopy)}>
<span>
<Badge
variant="neutral"
- label={peerPathLabel(entry.peerPath, locale)}
+ label={entry.peerPath.kind === 'direct'
+ ? peerMeshCopy.peerPathDirect
+ : peerMeshCopy.peerPathTransit}
/>
</span>
</Tooltip>
@@ -842,12 +844,10 @@ export function RuntimeHostProfilesSection(props: {
function peerPathDetail(
path: RuntimeHostPeerConnectionPath,
- locale: string,
+ copy: ReturnType<typeof getRuntimeHostPeerMeshCopy>,
): string {
if (path.kind === 'transit') {
- return locale.startsWith('zh')
- ? `成员转发 · ${abbreviatePeerId(path.relayPeerId)}`
- : `Member transit · ${abbreviatePeerId(path.relayPeerId)}`;
+ return `${copy.peerPathTransit} · ${abbreviatePeerId(path.relayPeerId)}`;
}
const transport = path.transport === 'webrtc'
? 'WebRTC'
@@ -855,24 +855,10 @@ function peerPathDetail(
? 'QUIC'
: path.transport === 'tcp'
? 'TCP'
- : locale.startsWith('zh')
- ? '其他'
- : 'Other';
- return `${locale.startsWith('zh') ? '直连' : 'Direct'} · ${transport}`;
+ : copy.peerPathOther;
+ return `${copy.peerPathDirect} · ${transport}`;
}
-function peerPathLabel(
- path: RuntimeHostPeerConnectionPath,
- locale: string,
-): string {
- if (path.kind === 'transit') {
- return locale.startsWith('zh') ? '成员转发' : 'Member transit';
- }
- if (path.transport === 'webrtc') return 'WebRTC';
- if (path.transport === 'quic') return 'QUIC';
- if (path.transport === 'tcp') return 'TCP';
- return locale.startsWith('zh') ? '直连' : 'Direct';
-}
function abbreviatePeerId(peerId: string): string {
return peerId.length <= 20 ? peerId : `${peerId.slice(0, 10)}…${peerId.slice(-6)}`;
diff --git a/apps/desktop/src/renderer/settings/settings-error-copy.ts b/apps/desktop/src/renderer/settings/settings-error-copy.ts
index 5456695179..8e759e7b8f 100644
--- a/apps/desktop/src/renderer/settings/settings-error-copy.ts
+++ b/apps/desktop/src/renderer/settings/settings-error-copy.ts
@@ -17,19 +17,20 @@
* under the License.
*/
-import { generalizedErrorMessageChinese } from '@maka/core/redaction';
+import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
import { type UiLocale } from '@maka/core/ui-locale';
import { redactSecrets } from '@maka/ui';
import { getSettingsSharedCopy } from '../locales/settings-shared-copy.js';
-export function settingsActionErrorMessage(error: unknown, locale: UiLocale = 'zh'): string {
+export function settingsActionErrorMessage(error: unknown, locale: UiLocale = 'zh-CN'): string {
const raw = error instanceof Error
? error.message
: typeof error === 'string'
? error
: '';
- const classified = locale === 'zh' ? generalizedErrorMessageChinese(new Error(raw), '') : '';
+ const classified = generalizedErrorMessageForLocale(new Error(raw), '', locale);
if (classified) return classified;
const redacted = redactSecrets(raw).trim();
- return redacted || getSettingsSharedCopy(locale).unknownError;
+ if (locale === 'zh-CN' && redacted && /[\u4E00-\u9FFF]/.test(redacted)) return redacted;
+ return getSettingsSharedCopy(locale).unknownError;
}
diff --git a/apps/desktop/src/renderer/settings/use-memory-settings-controller.ts b/apps/desktop/src/renderer/settings/use-memory-settings-controller.ts
index 40bec015cd..2bfa9a4732 100644
--- a/apps/desktop/src/renderer/settings/use-memory-settings-controller.ts
+++ b/apps/desktop/src/renderer/settings/use-memory-settings-controller.ts
@@ -689,5 +689,5 @@ export function useMemoryDocumentController(props: MemoryDocumentControllerProps
}
function memoryResultMessage(message: string, locale: UiLocale, fallback: string): string {
- return locale === 'zh' || !/[\u3400-\u9fff]/u.test(message) ? message : fallback;
+ return locale === 'zh-CN' || !/[\u3400-\u9fff]/u.test(message) ? message : fallback;
}
diff --git a/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts b/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts
index fbfdc16031..f237429d4d 100644
--- a/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts
+++ b/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts
@@ -18,7 +18,6 @@
*/
import { useEffect, useRef, useState } from 'react';
-import { generalizedErrorMessage, generalizedErrorMessageChinese, redactSecrets } from '@maka/core/redaction';
import { type UiLocale } from '@maka/core/ui-locale';
import {
useMountedRef,
@@ -26,7 +25,7 @@ import {
useUiLocale,
} from '@maka/ui';
import { createOneShotActionGuard, teardownPendingAuthorization } from './oauth-login-flow-guard';
-import { getProviderSettingsCopy } from '../features/connection-settings';
+import { getProviderSettingsCopy, subscriptionResultMessage } from '../features/connection-settings';
import { useRuntimeHostSettingsErrorReporter } from './runtime-host-settings-target.js';
// Shared browser-assisted OAuth login-flow controller (device-code polling).
@@ -329,7 +328,7 @@ export function useOAuthLoginFlow(params: OAuthLoginFlowParams): OAuthLoginFlowC
};
}
-export function subscriptionActionErrorMessage(error: unknown, locale: UiLocale = 'zh'): string {
+export function subscriptionActionErrorMessage(error: unknown, locale: UiLocale = 'zh-CN'): string {
const message = error instanceof Error
? error.message
: typeof error === 'string'
@@ -337,35 +336,3 @@ export function subscriptionActionErrorMessage(error: unknown, locale: UiLocale
: '';
return subscriptionResultMessage(message, getProviderSettingsCopy(locale).oauthFlow.serviceUnavailable, locale);
}
-
-export function subscriptionResultMessage(message: string | undefined, fallback: string, locale: UiLocale = 'zh', reason?: string): string {
- const raw = redactSecrets(message ?? '').trim();
- // The Host refuses an enrollment this install has not opted into and says so
- // with a typed reason. Read the reason, not the English message: a reworded
- // string or an added locale must not silently disable this branch. The
- // message match stays only as a fallback for callers without a typed reason.
- if (reason === 'experimental_disabled' || /enrollment is disabled for this provider/i.test(raw)) {
- return locale === 'zh'
- ? '本机未启用该账号登录方式;可改用导入兼容凭据,或由管理员启用后重试。'
- : 'This sign-in is not enabled on this install. Import a compatible credential instead, or ask an operator to enable it.';
- }
- if (!raw) return fallback;
- // Host conflict / supersede copy before the coarse keyword classifier turns
- // "authorization" into a generic 鉴权失败 that does not tell the user what to do.
- // This is error-path copy: do not claim a new login already started.
- if (/already in progress|superseded by a new attempt/i.test(raw)) {
- return locale === 'zh'
- ? '上一轮浏览器登录仍在进行或已切换,请再点一次登录,或稍后再试。'
- : 'A previous browser login is still running or was superseded. Try logging in again shortly.';
- }
- if (/did not present OAuth|no matching OAuth presentation/i.test(raw)) {
- return locale === 'zh'
- ? '无法打开系统浏览器完成登录,请检查是否拦截了弹窗后重试。'
- : 'Could not open the system browser for login. Check popup blockers and try again.';
- }
- const classified = locale === 'zh'
- ? generalizedErrorMessageChinese(new Error(raw), '')
- : generalizedErrorMessage(new Error(raw), '');
- if (classified) return classified;
- return locale === 'zh' || !/[\u4e00-\u9fff]/.test(raw) ? raw : fallback;
-}
diff --git a/apps/desktop/src/renderer/turn-footer-actions.ts b/apps/desktop/src/renderer/turn-footer-actions.ts
index 3d60ef76f5..ffc02bf7ea 100644
--- a/apps/desktop/src/renderer/turn-footer-actions.ts
+++ b/apps/desktop/src/renderer/turn-footer-actions.ts
@@ -113,7 +113,7 @@ export interface TurnFooterContext {
*/
export function deriveTurnFooterActions(input: TurnFooterContext): TurnFooterAction[] {
const { status, hasContent, alreadyRegenerated, pendingActions, metaSummary } = input;
- const copyText = getDesktopConversationCopy(input.locale ?? 'zh').footer;
+ const copyText = getDesktopConversationCopy(input.locale ?? 'zh-CN').footer;
const actionLabel = copyText.labels;
const isPending = (id: TurnFooterActionId) => pendingActions?.has(id) ?? false;
const PENDING_TOOLTIP = copyText.pending;
diff --git a/apps/desktop/src/renderer/use-onboarding-snapshot.ts b/apps/desktop/src/renderer/use-onboarding-snapshot.ts
index da694b9cd9..8e00c44f20 100644
--- a/apps/desktop/src/renderer/use-onboarding-snapshot.ts
+++ b/apps/desktop/src/renderer/use-onboarding-snapshot.ts
@@ -33,7 +33,7 @@
*/
import { useCallback, useEffect, useRef, useState } from 'react';
-import { generalizedErrorMessage, generalizedErrorMessageChinese } from '@maka/core/redaction';
+import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
import { type LlmConnection } from '@maka/core/llm-connections';
import { type SessionSummary } from '@maka/core/session';
import { type UiLocale } from '@maka/core/ui-locale';
@@ -229,7 +229,7 @@ export function createOnboardingSnapshotPoller(
export function onboardingSnapshotErrorMessage(error: unknown, locale: UiLocale): string {
const fallback = getOnboardingCopy(locale).snapshotErrorFallback;
- return locale === 'zh' ? generalizedErrorMessageChinese(error, fallback) : generalizedErrorMessage(error, fallback);
+ return generalizedErrorMessageForLocale(error, fallback, locale);
}
/**
diff --git a/apps/desktop/src/renderer/workhub-surface.tsx b/apps/desktop/src/renderer/workhub-surface.tsx
index 8f9bf0532a..8a7cf93ad2 100644
--- a/apps/desktop/src/renderer/workhub-surface.tsx
+++ b/apps/desktop/src/renderer/workhub-surface.tsx
@@ -819,13 +819,17 @@ function SubmittedWorkView(props: {
}
export function workHubAmbiguousCommandPrompt(locale: UiLocale): string {
- return locale === 'zh'
- ? '没有开始新工作。如果需要我直接执行,请给出明确指令,例如“修复登录”。'
- : 'I did not start new work. If you want me to do it, give a direct instruction, for example “Fix login”.';
+ if (locale === 'zh-CN') {
+ return '没有开始新工作。如果需要我直接执行,请给出明确指令,例如“修复登录”。';
+ }
+ if (locale === 'zh-TW') {
+ return '沒有開始新工作。如果需要我直接執行,請給出明確指令,例如「修復登入」。';
+ }
+ return 'I did not start new work. If you want me to do it, give a direct instruction, for example “Fix login”.';
}
function workHubCopy(locale: UiLocale) {
- if (locale === 'zh') {
+ if (locale === 'zh-CN') {
return {
locale,
subtitle: '在一个入口里继续、创建和查看普通 Session',
@@ -886,6 +890,68 @@ function workHubCopy(locale: UiLocale) {
turnStates: { running: '进行中', completed: '已完成', aborted: '已中止', failed: '失败' },
} as const;
}
+ if (locale === 'zh-TW') {
+ return {
+ locale,
+ subtitle: '在一個入口繼續、建立和檢視一般 Session',
+ emptyTitle: '從這裡繼續所有工作',
+ emptyBody: (count: number) => count > 0
+ ? `WorkHub 會根據現有 ${count} 個 Session 判斷目標;不確定時會先詢問你。`
+ : '提出一個明確目標,WorkHub 會建立一般 Session 並將結果帶回這裡。',
+ workCount: (count: number) => `${count} 項工作`, clarification: '選擇工作',
+ chooseWork: '這則輸入可能與多項工作有關,請選擇目標:',
+ confirmCommand: workHubAmbiguousCommandPrompt(locale),
+ discussionStayed: '這則內容暫時保留在 WorkHub,沒有建立或變更 Session。',
+ discussionHint: '提出明確的執行目標後,我會將它交給對應的 Session。',
+ answering: '正在回答…',
+ choseWork: (name: string) => `選擇「${name}」`,
+ sentTo: '已交給:', createdWork: '已建立新工作:', accepted: '已接收', sessionFallback: '一般 Session',
+ stopTargetRequired: '請明確說出要停止的工作名稱,例如「停止 支付任務」。',
+ stopTargetAmbiguous: '這個名稱對應多項工作;請開啟具體的 Session 停止對應委派。',
+ stopTargetUnavailable: '這項工作現在沒有可以停止的單一 WorkHub 委派;請開啟該 Session 檢視。',
+ waitingForDecision: '這項工作正在等待你的決定。',
+ requestNotSent: '新請求尚未傳送;處理原 Session 中的互動後可以再次傳送。',
+ routing: '正在判斷應該交給哪個 Session…', loadFailed: '無法讀取現有工作。',
+ loading: '正在讀取現有工作…',
+ preparing: '正在準備 WorkHub…', unavailable: '暫時無法使用',
+ coordinationFailedTitle: 'WorkHub 暫時無法啟動',
+ coordinationFailedBody: '請檢查目前 Runtime Host 的預設模型設定,然後重試。',
+ retry: '重試',
+ stoppingWork: '正在請求停止:', stopping: '正在處理', stopRecorded: '結果已記錄',
+ openSessionToStop: '這個 Turn 不由該委派獨佔;請開啟 Session 處理',
+ stopOutcomes: {
+ cancelled_pending: '已取消尚未開始的工作:',
+ stop_delivered: '已向執行中的工作發出停止請求:',
+ already_terminal: '這項工作已經結束:',
+ not_owned: '未停止共享或使用者擁有的 Turn:',
+ },
+ submitFailures: {
+ candidates_changed: '工作清單已變更,請重新傳送以使用最新目標。',
+ linked_correction_unavailable: '跨 Session 更正將於持久委派關聯完成後開放;請先開啟原 Session 並停止目前工作。',
+ target_waiting: '目標 Session 正在等待你的處理;請先開啟並完成該互動。',
+ action_changed: '這次操作已變更,請重新傳送。',
+ delivery_failed: '輸入未能送達,請重試。',
+ },
+ scrollToBottom: '捲動到底部', archived: '已封存',
+ states: { active: '使用中', running: '進行中', waiting_for_user: '等待你', blocked: '受阻', aborted: '已中止' },
+ delegationStates: {
+ accepted: '已接收',
+ running: '進行中',
+ waiting_for_user: '等待你',
+ completed: '已完成',
+ failed: '失敗',
+ aborted: '已中止',
+ recovering: '正在恢復',
+ },
+ assignmentLinkStates: {
+ active: (execution: string) => `關聯有效 · ${execution}`,
+ superseded: '已被更正',
+ aborted: '更正已中止',
+ stopped: '已停止關聯',
+ },
+ turnStates: { running: '進行中', completed: '已完成', aborted: '已中止', failed: '失敗' },
+ } as const;
+ }
return {
locale,
subtitle: 'Continue, create, and review ordinary Sessions from one place',
diff --git a/apps/desktop/stories/agent-graph-panel.stories.tsx b/apps/desktop/stories/agent-graph-panel.stories.tsx
index f43088dd1e..cb60e7647c 100644
--- a/apps/desktop/stories/agent-graph-panel.stories.tsx
+++ b/apps/desktop/stories/agent-graph-panel.stories.tsx
@@ -39,7 +39,7 @@ import { withScopedMakaBridge } from './maka-bridge';
const ROOT_SESSION_ID = 'session-graph';
const GRAPH_ID = 'graph-storybook';
-const LOCALE = 'zh';
+const LOCALE = 'zh-CN';
const copy = getAgentGraphPanelCopy(LOCALE);
const meta = {
diff --git a/apps/desktop/stories/app-shell.stories.tsx b/apps/desktop/stories/app-shell.stories.tsx
index 3f61524c7e..66c84b7cdf 100644
--- a/apps/desktop/stories/app-shell.stories.tsx
+++ b/apps/desktop/stories/app-shell.stories.tsx
@@ -370,7 +370,7 @@ function ComposedShell(props: {
deriveAppShellTurnPresentation(turns, {
activeId: active?.id,
pendingTurnActions: new Set<string>(),
- uiLocale: 'zh',
+ uiLocale: 'zh-CN',
});
const projectGroups: SessionGroup[] = catalogProjects.map((item) => ({
id: `project:${item.id}`,
diff --git a/apps/desktop/stories/error-boundary.stories.tsx b/apps/desktop/stories/error-boundary.stories.tsx
index 3ca04fc13d..beb79f6ba8 100644
--- a/apps/desktop/stories/error-boundary.stories.tsx
+++ b/apps/desktop/stories/error-boundary.stories.tsx
@@ -38,7 +38,8 @@ const onCopyReport = fn();
const onReset = fn();
const onReload = fn();
-const resolveLocale = (globals: Record<string, unknown>) => (globals.locale === 'en' ? 'en' : 'zh');
+const resolveLocale = (globals: Record<string, unknown>) =>
+ globals.locale === 'en' ? 'en' : globals.locale === 'zh-TW' ? 'zh-TW' : 'zh-CN';
function fallback(copyState: ErrorBoundaryCopyState) {
return (_args: unknown, { globals }: { globals: Record<string, unknown> }) => (
diff --git a/apps/desktop/stories/settings/settings-pages.stories.tsx b/apps/desktop/stories/settings/settings-pages.stories.tsx
index e3be3bb49e..032e326f80 100644
--- a/apps/desktop/stories/settings/settings-pages.stories.tsx
+++ b/apps/desktop/stories/settings/settings-pages.stories.tsx
@@ -85,7 +85,7 @@ import { getUsageSettingsCopy } from '../../src/renderer/locales/settings-usage-
* function, so CI could not tell us. A story that drives the UI by its visible
* text has to source that text where the UI does.
*/
-const DAILY_REVIEW_DEFAULT_MODEL_LABEL = getDailyReviewSettingsCopy('zh').defaultModel;
+const DAILY_REVIEW_DEFAULT_MODEL_LABEL = getDailyReviewSettingsCopy('zh-CN').defaultModel;
/** A 1×1 transparent PNG: the picker needs a valid data URL, not real art. */
const STORY_ICON_PREVIEW =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
@@ -1852,7 +1852,7 @@ export const GeneralCachedRevalidation: Story = {
await expect(
canvas.getByRole('switch', { name: '完成时发送系统通知' }),
).toBeEnabled();
- await expect(canvas.getByRole('radio', { name: '中文' })).toBeEnabled();
+ await expect(canvas.getByRole('combobox', { name: '界面语言' })).toBeEnabled();
const mixedBoundary = canvasElement.querySelector<HTMLElement>(
'.settingsRuntimeHostInteractionBoundary',
);
@@ -1909,7 +1909,7 @@ export const GeneralHostGenerationRevalidation: Story = {
await expect(
canvas.getByRole('switch', { name: '完成时发送系统通知' }),
).toBeEnabled();
- await expect(canvas.getByRole('radio', { name: '中文' })).toBeEnabled();
+ await expect(canvas.getByRole('combobox', { name: '界面语言' })).toBeEnabled();
const mixedBoundary = canvasElement.querySelector<HTMLElement>(
'.settingsRuntimeHostInteractionBoundary',
);
@@ -1974,7 +1974,7 @@ export const GeneralBackgroundHostReconnectThenSelect: Story = {
await expect(
canvas.getByRole('switch', { name: '完成时发送系统通知' }),
).toBeEnabled();
- await expect(canvas.getByRole('radio', { name: '中文' })).toBeEnabled();
+ await expect(canvas.getByRole('combobox', { name: '界面语言' })).toBeEnabled();
},
};
// Error is a real signal rather than a loading state. Desktop-owned controls
@@ -2083,8 +2083,10 @@ export const UsageLongTail: Story = {
render: () => <SettingsStory section="usage" />,
play: async ({ canvasElement, globals }) => {
const canvas = within(canvasElement);
- const usageCopy = getUsageSettingsCopy(globals.locale === 'en' ? 'en' : 'zh');
- expect(
+ const usageCopy = getUsageSettingsCopy(
+ globals.locale === 'en' ? 'en' : globals.locale === 'zh-TW' ? 'zh-TW' : 'zh-CN',
+ );
+ expect(
await canvas.findByText(usageCopy.totalRequests, {
selector: '[data-slot="stat-tile-label"]',
}),
diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md
index e4e5f0f362..5b7e3fdb67 100644
--- a/docs/astryx-surface-file-inventory.md
+++ b/docs/astryx-surface-file-inventory.md
@@ -117,7 +117,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
| `apps/desktop/src/renderer/settings/memory-settings-sections.tsx` | settings-module | Button | aligned — uses Astryx (Button) | aligned |
| `apps/desktop/src/renderer/settings/password-input.tsx` | settings-module | IconButton, InputGroup, InputGroupText, TextInput | aligned — uses Astryx (IconButton, InputGroup, InputGroupText, TextInput) | aligned |
| `apps/desktop/src/renderer/settings/permission-center-page.tsx` | settings-page | Banner, Button, Collapsible, CollapsibleGroup, HStack, List, ListItem, MetadataList, MetadataListItem, StatusDot, Text, VStack | aligned — uses Astryx (Banner, Button, Collapsible, CollapsibleGroup, HStack, List, ListItem, MetadataList) | aligned |
-| `apps/desktop/src/renderer/settings/personalization-settings-section.tsx` | settings-module | SegmentedControl, SegmentedControlItem, TextArea, TextInput | aligned — uses Astryx (SegmentedControl, SegmentedControlItem, TextArea, TextInput) | aligned |
+| `apps/desktop/src/renderer/settings/personalization-settings-section.tsx` | settings-module | Selector, TextArea, TextInput | aligned — uses Astryx (Selector, TextArea, TextInput) | aligned |
| `apps/desktop/src/renderer/settings/projects-settings-page.tsx` | settings-page | Badge, Banner, Button, EmptyState, HStack, List, ListItem, MoreMenu, TextInput | aligned — uses Astryx (Badge, Banner, Button, EmptyState, HStack, List, ListItem, MoreMenu) | aligned |
| `apps/desktop/src/renderer/settings/provider-add-form.tsx` | settings-module | Banner, Button, CheckboxList, CheckboxListItem, Collapsible, EmptyState, FormLayout, HStack, Selector, Step, Stepper, Text, TextInput, VStack | aligned — uses Astryx (Banner, Button, CheckboxList, CheckboxListItem, Collapsible, EmptyState, FormLayout, HStack) | aligned |
| `apps/desktop/src/renderer/settings/provider-add-model-dialog.tsx` | settings-module | Button, Dialog, DialogHeader, FormLayout, HStack, Layout, LayoutContent, LayoutFooter, NumberInput, TextInput | aligned — uses Astryx (Button, Dialog, DialogHeader, FormLayout, HStack, Layout, LayoutContent, LayoutFooter) | aligned |
diff --git a/packages/cli/src/__tests__/cli-ui-locale.test.ts b/packages/cli/src/__tests__/cli-ui-locale.test.ts
index e92a26fb18..8c4e22d7b7 100644
--- a/packages/cli/src/__tests__/cli-ui-locale.test.ts
+++ b/packages/cli/src/__tests__/cli-ui-locale.test.ts
@@ -23,9 +23,13 @@ import { resolveCliUiLocale } from '../cli-ui-locale.js';
describe('CLI TUI locale authority', () => {
test('an explicit MAKA_LOCALE wins over the POSIX locale', () => {
- assert.deepEqual(resolveCliUiLocale({ MAKA_LOCALE: 'zh', LC_ALL: 'en_US.UTF-8' }), {
+ assert.deepEqual(resolveCliUiLocale({ MAKA_LOCALE: 'zh-CN', LC_ALL: 'en_US.UTF-8' }), {
ok: true,
- locale: 'zh',
+ locale: 'zh-CN',
+ });
+ assert.deepEqual(resolveCliUiLocale({ MAKA_LOCALE: 'ZH-TW', LC_ALL: 'zh_CN.UTF-8' }), {
+ ok: true,
+ locale: 'zh-TW',
});
assert.deepEqual(resolveCliUiLocale({ MAKA_LOCALE: 'EN', LANG: 'zh_CN.UTF-8' }), {
ok: true,
@@ -33,6 +37,21 @@ describe('CLI TUI locale authority', () => {
});
});
+ test('preserves legacy and POSIX-style Chinese aliases', () => {
+ assert.deepEqual(resolveCliUiLocale({ MAKA_LOCALE: 'zh' }), {
+ ok: true,
+ locale: 'zh-CN',
+ });
+ assert.deepEqual(resolveCliUiLocale({ MAKA_LOCALE: 'zh_CN' }), {
+ ok: true,
+ locale: 'zh-CN',
+ });
+ assert.deepEqual(resolveCliUiLocale({ MAKA_LOCALE: 'zh_TW' }), {
+ ok: true,
+ locale: 'zh-TW',
+ });
+ });
+
test('auto honors POSIX locale authority order', () => {
assert.deepEqual(
resolveCliUiLocale({
@@ -45,19 +64,34 @@ describe('CLI TUI locale authority', () => {
);
assert.deepEqual(resolveCliUiLocale({ LC_MESSAGES: 'zh_CN.UTF-8', LANG: 'en_US.UTF-8' }), {
ok: true,
- locale: 'zh',
+ locale: 'zh-CN',
});
assert.deepEqual(resolveCliUiLocale({ LANG: 'en_US.UTF-8' }), {
ok: true,
locale: 'en',
});
+ assert.deepEqual(resolveCliUiLocale({ LANG: 'zh_TW.UTF-8' }), {
+ ok: true,
+ locale: 'zh-TW',
+ });
assert.deepEqual(resolveCliUiLocale({}), { ok: true, locale: 'en' });
});
+ test('treats empty and whitespace-only MAKA_LOCALE as auto', () => {
+ assert.deepEqual(resolveCliUiLocale({ MAKA_LOCALE: '', LANG: 'zh_TW.UTF-8' }), {
+ ok: true,
+ locale: 'zh-TW',
+ });
+ assert.deepEqual(resolveCliUiLocale({ MAKA_LOCALE: ' \t ', LANG: 'zh_CN.UTF-8' }), {
+ ok: true,
+ locale: 'zh-CN',
+ });
+ });
+
test('rejects an unsupported explicit preference with a configuration error', () => {
assert.deepEqual(resolveCliUiLocale({ MAKA_LOCALE: 'fr' }), {
ok: false,
- message: 'Invalid MAKA_LOCALE "fr". Expected zh, en, or auto.',
+ message: 'Invalid MAKA_LOCALE "fr". Expected zh-CN, zh-TW, en, or auto.',
});
});
});
diff --git a/packages/cli/src/__tests__/cli.test.ts b/packages/cli/src/__tests__/cli.test.ts
index a4975f9409..09ed0228b9 100644
--- a/packages/cli/src/__tests__/cli.test.ts
+++ b/packages/cli/src/__tests__/cli.test.ts
@@ -142,7 +142,7 @@ describe('Maka CLI args', () => {
});
test('does not add a discoverable global locale flag', () => {
- assert.deepEqual(parseMakaCliArgs(['--locale', 'zh'], '0.1.0'), {
+ assert.deepEqual(parseMakaCliArgs(['--locale', 'zh-CN'], '0.1.0'), {
kind: 'error',
message: 'Unexpected argument: --locale',
exitCode: 2,
diff --git a/packages/cli/src/__tests__/pi-transcript.test.ts b/packages/cli/src/__tests__/pi-transcript.test.ts
index 6a8e205ed5..8cd0e2a40e 100644
--- a/packages/cli/src/__tests__/pi-transcript.test.ts
+++ b/packages/cli/src/__tests__/pi-transcript.test.ts
@@ -123,7 +123,7 @@ describe('Maka Pi TUI transcript', () => {
assert.match(english, /Type a message to start/);
assert.match(english, /\/session\s+Switch or resume a session/);
- const chinese = renderMakaPiTranscript(state, { ...meta(), uiLocale: 'zh' }, 100)
+ const chinese = renderMakaPiTranscript(state, { ...meta(), uiLocale: 'zh-CN' }, 100)
.map(stripAnsi)
.join('\n');
assert.match(chinese, /陪你把事做完/);
@@ -156,7 +156,7 @@ describe('Maka Pi TUI transcript', () => {
assert.equal(renderFor('darwin').at(-1), '⌥+↑ take queued messages back to re-edit');
assert.equal(renderFor('linux').at(-1), 'Alt+↑ take queued messages back to re-edit');
assert.equal(
- renderMakaPiPendingQueue(state, 80, 'linux', 'zh').map(stripAnsi).at(-1),
+ renderMakaPiPendingQueue(state, 80, 'linux', 'zh-CN').map(stripAnsi).at(-1),
'Alt+↑ 取回队列以重新编辑',
);
});
@@ -224,7 +224,7 @@ describe('Maka Pi TUI transcript', () => {
assert.equal(
stripAnsi(
renderMakaPiStatusLine(
- { ...meta(), uiLocale: 'zh', sideConversation: { view: 'side' } },
+ { ...meta(), uiLocale: 'zh-CN', sideConversation: { view: 'side' } },
200,
),
),
@@ -243,7 +243,7 @@ describe('Maka Pi TUI transcript', () => {
renderMakaPiStatusLine(
{
...meta(),
- uiLocale: 'zh',
+ uiLocale: 'zh-CN',
sideConversation: { view: 'side', parentStatus },
},
200,
@@ -255,7 +255,7 @@ describe('Maka Pi TUI transcript', () => {
assert.match(
stripAnsi(
renderMakaPiStatusLine(
- { ...meta(), uiLocale: 'zh', sideConversation: { view: 'parent' } },
+ { ...meta(), uiLocale: 'zh-CN', sideConversation: { view: 'parent' } },
200,
),
),
diff --git a/packages/cli/src/__tests__/pi-tui-mcp-status.test.ts b/packages/cli/src/__tests__/pi-tui-mcp-status.test.ts
index 9a0e12fbaf..cdb4cb6dae 100644
--- a/packages/cli/src/__tests__/pi-tui-mcp-status.test.ts
+++ b/packages/cli/src/__tests__/pi-tui-mcp-status.test.ts
@@ -64,7 +64,7 @@ describe('MCP management overlay', () => {
test('states the remote limitation instead of implying an empty local config', () => {
const overlay = new McpManagementOverlay({
- locale: 'zh',
+ locale: 'zh-CN',
viewportRows: () => 6,
onClose: () => undefined,
onChange: () => undefined,
@@ -137,7 +137,7 @@ describe('MCP management overlay', () => {
test('localizes manager states without changing their source values', () => {
const overlay = new McpManagementOverlay({
- locale: 'zh',
+ locale: 'zh-CN',
surface: surface({
initialization: 'ready',
configuration: 'ready',
@@ -164,6 +164,39 @@ describe('MCP management overlay', () => {
assert.doesNotMatch(text, /needs-auth/u);
});
+ test('renders Traditional Chinese without Simplified fallbacks', () => {
+ const overlay = new McpManagementOverlay({
+ locale: 'zh-TW',
+ surface: surface({
+ initialization: 'ready',
+ configuration: 'ready',
+ publication: 'not_published',
+ toolCount: 0,
+ servers: [
+ {
+ serverId: 'oauth',
+ configured: true,
+ synchronized: true,
+ state: 'disconnected',
+ transport: 'streamable-http',
+ toolCount: 0,
+ error: '连接失败。',
+ },
+ ],
+ }),
+ viewportRows: () => 7,
+ onClose: () => undefined,
+ onChange: () => undefined,
+ });
+
+ const text = overlay.render(100).map(stripAnsi).join('\n');
+ assert.match(text, /MCP 伺服器/u);
+ assert.match(text, /未發佈 · 0 個工具/u);
+ assert.match(text, /oauth 未連線/u);
+ assert.match(text, /MCP 伺服器連線失敗/u);
+ assert.doesNotMatch(text, /连接|发布/u);
+ });
+
test('subscribes only for the overlay lifetime', () => {
let subscribed = 0;
let disposed = 0;
@@ -251,73 +284,118 @@ describe('MCP management overlay', () => {
assert.match(text, /› ○ s4/u);
});
- test('routes the guided add flow through catalog headings, labels, and hints', () => {
- const locale = 'en';
- const expected = TUI_COPY_RESOURCES['mcp-status'][locale].editor;
- const overlay = new McpManagementOverlay({
- locale,
- tui: fakeTui(),
- surface: surface(listSnapshot()),
- viewportRows: () => 14,
- onClose: () => undefined,
- onChange: () => undefined,
- });
- const rendered = () => overlay.render(100).map(stripAnsi).join('\n');
-
- overlay.handleInput('a');
- assert.ok(rendered().includes(expected.addTitle));
- overlay.handleInput('g');
- let text = rendered();
- assert.ok(text.includes(expected.inputLabels.server_id));
- assert.ok(text.includes(expected.hints.submit));
- for (const char of 'demo') overlay.handleInput(char);
- overlay.handleInput('\r');
- assert.ok(rendered().includes(expected.transportTitle));
- overlay.handleInput('1');
- text = rendered();
- assert.ok(text.includes(expected.inputLabels.command));
- for (const char of 'echo') overlay.handleInput(char);
- overlay.handleInput('\r');
- text = rendered();
- assert.ok(text.includes(expected.inputLabels.args));
- assert.ok(text.includes(expected.hints.args));
- overlay.handleInput('\r');
- assert.ok(rendered().includes(expected.protocolTitle));
- overlay.handleInput('2');
- text = rendered();
- assert.ok(text.includes(expected.inputLabels.cwd));
- assert.ok(text.includes(expected.hints.optional));
- overlay.handleInput('\r');
- text = rendered();
- assert.ok(text.includes(expected.inputLabels.env));
- assert.ok(text.includes(expected.hints.map));
- overlay.handleInput('\r');
- text = rendered();
- assert.ok(text.includes(expected.confirmAddTitle));
- assert.ok(text.includes('demo · stdio · auto'));
- assert.ok(text.includes('echo'));
- assert.ok(text.includes(expected.confirmHint));
- });
+ const editorCopy = {
+ en: {
+ add: 'Add MCP server',
+ transport: 'Transport',
+ protocol: 'Protocol preference',
+ confirmAdd: 'Add this MCP server?',
+ confirmRemove: 'Remove filesystem?',
+ confirmHint: 'y Confirm · Esc cancel',
+ serverId: 'Server ID',
+ command: 'Command',
+ args: 'Arguments',
+ argsHint: 'JSON string array, optional',
+ cwd: 'Working directory',
+ optionalHint: 'optional',
+ env: 'Environment',
+ mapHint: 'JSON string map, optional',
+ submitHint: 'Enter submit · Esc back',
+ testing: 'Testing MCP server…',
+ reconnecting: 'Reconnecting MCP server…',
+ applying: 'Applying MCP configuration…',
+ published: 'Configuration saved and tools refreshed.',
+ managerFailed: 'The MCP connection action failed.',
+ },
+ 'zh-CN': {
+ add: '添加 MCP 服务器',
+ transport: '传输方式',
+ protocol: '协议偏好',
+ confirmAdd: '添加该 MCP 服务器?',
+ confirmRemove: '删除 filesystem?',
+ confirmHint: 'y 确认 · Esc 取消',
+ serverId: '服务器 ID',
+ command: '命令',
+ args: '参数',
+ argsHint: 'JSON string array, 可留空',
+ cwd: '工作目录',
+ optionalHint: '可留空',
+ env: '环境变量',
+ mapHint: 'JSON string map, 可留空',
+ submitHint: 'Enter 提交 · Esc 返回',
+ testing: '正在测试 MCP 服务器…',
+ reconnecting: '正在重连 MCP 服务器…',
+ applying: '正在应用 MCP 配置…',
+ published: '配置已保存,工具已刷新。',
+ managerFailed: 'MCP 连接操作失败。',
+ },
+ } as const;
- test('routes remove confirmation copy and interpolates the server id', () => {
- const locale = 'en';
- const expected = TUI_COPY_RESOURCES['mcp-status'][locale].editor;
- const overlay = new McpManagementOverlay({
- locale,
- surface: surface(listSnapshot()),
- viewportRows: () => 8,
- onClose: () => undefined,
- onChange: () => undefined,
+ for (const locale of ['en', 'zh-CN'] as const) {
+ const expected = editorCopy[locale];
+
+ test(`walks the guided add flow with ${locale} headings, labels, and hints`, () => {
+ const overlay = new McpManagementOverlay({
+ locale,
+ tui: fakeTui(),
+ surface: surface(listSnapshot()),
+ viewportRows: () => 14,
+ onClose: () => undefined,
+ onChange: () => undefined,
+ });
+ const rendered = () => overlay.render(100).map(stripAnsi).join('\n');
+
+ overlay.handleInput('a');
+ assert.ok(rendered().includes(expected.add));
+ overlay.handleInput('g');
+ let text = rendered();
+ assert.ok(text.includes(expected.serverId));
+ assert.ok(text.includes(expected.submitHint));
+ for (const char of 'demo') overlay.handleInput(char);
+ overlay.handleInput('\r');
+ assert.ok(rendered().includes(expected.transport));
+ overlay.handleInput('1');
+ text = rendered();
+ assert.ok(text.includes(expected.command));
+ for (const char of 'echo') overlay.handleInput(char);
+ overlay.handleInput('\r');
+ text = rendered();
+ assert.ok(text.includes(expected.args));
+ assert.ok(text.includes(expected.argsHint));
+ overlay.handleInput('\r');
+ assert.ok(rendered().includes(expected.protocol));
+ overlay.handleInput('2');
+ text = rendered();
+ assert.ok(text.includes(expected.cwd));
+ assert.ok(text.includes(expected.optionalHint));
+ overlay.handleInput('\r');
+ text = rendered();
+ assert.ok(text.includes(expected.env));
+ assert.ok(text.includes(expected.mapHint));
+ overlay.handleInput('\r');
+ text = rendered();
+ assert.ok(text.includes(expected.confirmAdd));
+ assert.ok(text.includes('demo · stdio · auto'));
+ assert.ok(text.includes('echo'));
+ assert.ok(text.includes(expected.confirmHint));
});
- overlay.render(100);
- overlay.handleInput('d');
- const text = overlay.render(100).map(stripAnsi).join('\n');
- assert.ok(text.includes(expected.confirmRemoveTitle.replace('{serverId}', 'filesystem')));
- assert.ok(text.includes(expected.confirmHint));
- });
+ test(`renders the ${locale} remove confirmation with the server id`, () => {
+ const overlay = new McpManagementOverlay({
+ locale,
+ surface: surface(listSnapshot()),
+ viewportRows: () => 8,
+ onClose: () => undefined,
+ onChange: () => undefined,
+ });
+ overlay.render(100);
+
+ overlay.handleInput('d');
+ const text = overlay.render(100).map(stripAnsi).join('\n');
+ assert.ok(text.includes(expected.confirmRemove));
+ assert.ok(text.includes(expected.confirmHint));
+ });
- for (const locale of ['en', 'zh'] as const) {
test(`labels the busy phase per action kind in ${locale}`, () => {
const expected = TUI_COPY_RESOURCES['mcp-status'][locale].editor;
const mcp = surface(listSnapshot());
@@ -369,24 +447,52 @@ describe('MCP management overlay', () => {
[{ status: 'tested', test: testResult(true), effect: 'pending_host' }, 'test_pending_host'],
];
- test('routes every action result to its catalog notice', async () => {
- for (const [result, code] of RESULT_CASES) {
- const mcp = surface(listSnapshot());
- mcp.execute = async () => result;
- const overlay = new McpManagementOverlay({
- locale: 'en',
- surface: mcp,
- viewportRows: () => 8,
- onClose: () => undefined,
- onChange: () => undefined,
- });
- overlay.render(100);
+ for (const locale of ['en', 'zh-CN'] as const) {
+ test(`routes every action result to its ${locale} catalog notice`, async () => {
+ const results = TUI_COPY_RESOURCES['mcp-status'][locale].editor.results;
+ for (const [result, code] of RESULT_CASES) {
+ const mcp = surface(listSnapshot());
+ mcp.execute = async () => result;
+ const overlay = new McpManagementOverlay({
+ locale,
+ surface: mcp,
+ viewportRows: () => 8,
+ onClose: () => undefined,
+ onChange: () => undefined,
+ });
+ overlay.render(100);
+
+ overlay.handleInput(' ');
+ await new Promise<void>((resolve) => setImmediate(resolve));
+ const text = overlay.render(100).map(stripAnsi).join('\n');
+ assert.ok(text.includes(results[code as keyof typeof results]), `${code} (${locale})`);
+ }
+ });
+ }
- overlay.handleInput(' ');
- await new Promise<void>((resolve) => setImmediate(resolve));
- const text = overlay.render(100).map(stripAnsi).join('\n');
- assert.ok(text.includes(resultCopy[code]), code);
- }
+ test('renders an unknown runtime result code as the raw code', async () => {
+ const mcp = surface(listSnapshot());
+ mcp.execute = async () =>
+ ({ status: 'failed', reason: 'future-code' }) as unknown as TuiMcpActionResult;
+ const overlay = new McpManagementOverlay({
+ locale: 'en',
+ surface: mcp,
+ viewportRows: () => 8,
+ onClose: () => undefined,
+ onChange: () => undefined,
+ });
+ overlay.render(100);
+
+ overlay.handleInput(' ');
+ await new Promise<void>((resolve) => setImmediate(resolve));
+ assert.ok(overlay.render(100).map(stripAnsi).join('\n').includes('future-code'));
+ });
+
+ test('keeps punctuation-sensitive catalog strings byte-for-byte', () => {
+ const editor = (locale: 'en' | 'zh-CN') => TUI_COPY_RESOURCES['mcp-status'][locale].editor;
+ assert.equal(editor('zh-CN').results.stale_import, '导入项已变化,请重新预览。');
+ assert.equal(editor('en').confirmImportTitle, 'Import MCP servers?');
+ assert.equal(editor('zh-CN').confirmImportTitle, '导入 MCP 服务器?');
});
});
diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts
index 2c633b4642..1253daf81f 100644
--- a/packages/cli/src/__tests__/pi-tui-runner.test.ts
+++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts
@@ -246,7 +246,7 @@ describe('Maka Pi TUI runner', () => {
const driver = new SlashCommandDriver();
const run = runMakaPiTui({
title: 'Maka',
- locale: 'zh',
+ locale: 'zh-CN',
driver,
cwd: '/repo',
model: 'claude-sonnet-4-5',
@@ -325,7 +325,7 @@ describe('Maka Pi TUI runner', () => {
const driver = new UserCommandDriver();
const run = runMakaPiTui({
title: 'Maka',
- locale: 'zh',
+ locale: 'zh-CN',
driver,
cwd: '/repo',
model: 'claude-sonnet-4-5',
@@ -799,7 +799,7 @@ describe('Maka Pi TUI runner', () => {
model: 'claude-sonnet-4-5',
connectionSlug: 'claude-subscription',
permissionMode: 'bypass',
- locale: 'zh',
+ locale: 'zh-CN',
terminal,
onboarding: fakeOnboardingSurface({
verify: async (input) => {
@@ -868,7 +868,7 @@ describe('Maka Pi TUI runner', () => {
model: 'claude-sonnet-4-5',
connectionSlug: 'claude-subscription',
permissionMode: 'bypass',
- locale: 'zh',
+ locale: 'zh-CN',
terminal,
onboarding: fakeOnboardingSurface({
save: async () => {
@@ -4119,7 +4119,7 @@ describe('Maka Pi TUI runner', () => {
models: ['gpt-5', 'gpt-5-mini'],
connectionSlug: 'openai',
permissionMode: 'ask',
- locale: 'zh',
+ locale: 'zh-CN',
terminal,
});
@@ -4141,7 +4141,7 @@ describe('Maka Pi TUI runner', () => {
model: 'gpt-5',
connectionSlug: 'openai',
permissionMode: 'ask',
- locale: 'zh',
+ locale: 'zh-CN',
modelChoices: [
{
connectionId: 'connection-openai',
@@ -6746,7 +6746,7 @@ describe('Maka Pi TUI runner', () => {
connectionSlug: 'claude-subscription',
permissionMode: 'ask',
terminal,
- locale: 'zh',
+ locale: 'zh-CN',
listSkills: async () => [],
});
@@ -8235,7 +8235,7 @@ describe('Maka Pi TUI runner', () => {
model: 'claude-sonnet-4-5',
connectionSlug: 'claude-subscription',
permissionMode: 'ask',
- locale: 'zh',
+ locale: 'zh-CN',
terminal,
});
diff --git a/packages/cli/src/__tests__/tui-copy-catalog.test.ts b/packages/cli/src/__tests__/tui-copy-catalog.test.ts
index 289fbb16f2..c233121b96 100644
--- a/packages/cli/src/__tests__/tui-copy-catalog.test.ts
+++ b/packages/cli/src/__tests__/tui-copy-catalog.test.ts
@@ -19,7 +19,7 @@
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
-import { formatUiMessage } from '@maka/core/ui-locale';
+import { formatUiMessage, UI_LOCALES } from '@maka/core/ui-locale';
import { getTuiPickerCopy, onboardingFailureMessage } from '../pi-tui-pickers.js';
import { TUI_COPY_RESOURCES } from '../tui-copy-catalog.js';
@@ -45,33 +45,37 @@ const MESSAGE_VALUES = {
describe('TUI copy resources', () => {
test('registers every domain without a locale-specific getter branch', () => {
for (const [domain, catalog] of Object.entries(TUI_COPY_RESOURCES)) {
- assert.ok(catalog.en, `${domain}/en`);
- assert.ok(catalog.zh, `${domain}/zh`);
+ for (const locale of UI_LOCALES) assert.ok(catalog[locale], `${domain}/${locale}`);
}
});
- test('keeps Chinese coverage, variables, and ICU formatting aligned with English', () => {
+ test('keeps translated coverage, variables, and ICU formatting aligned with English', () => {
for (const [domain, catalog] of Object.entries(TUI_COPY_RESOURCES)) {
const enLeaves = new Map(leafEntries(catalog.en));
- const zhLeaves = new Map(leafEntries(catalog.zh));
-
- assert.deepEqual([...zhLeaves.keys()].sort(), [...enLeaves.keys()].sort(), `${domain}/zh`);
- for (const [path, enTemplate] of enLeaves) {
- const zhTemplate = zhLeaves.get(path)!;
- const enVariables = messageVariables(enTemplate);
- const zhVariables = messageVariables(zhTemplate);
- assert.deepEqual(zhVariables, enVariables, `${domain}/zh/${path}`);
- if (enVariables.length === 0) continue;
- assert.notEqual(
- formatUiMessage(enTemplate, MESSAGE_VALUES, 'en'),
- enTemplate,
- `${domain}/en/${path}`,
- );
- assert.notEqual(
- formatUiMessage(zhTemplate, MESSAGE_VALUES, 'zh'),
- zhTemplate,
- `${domain}/zh/${path}`,
+ for (const locale of ['zh-CN', 'zh-TW'] as const) {
+ const translatedLeaves = new Map(leafEntries(catalog[locale]));
+ assert.deepEqual(
+ [...translatedLeaves.keys()].sort(),
+ [...enLeaves.keys()].sort(),
+ `${domain}/${locale}`,
);
+ for (const [path, enTemplate] of enLeaves) {
+ const translatedTemplate = translatedLeaves.get(path)!;
+ const enVariables = messageVariables(enTemplate);
+ const translatedVariables = messageVariables(translatedTemplate);
+ assert.deepEqual(translatedVariables, enVariables, `${domain}/${locale}/${path}`);
+ if (enVariables.length === 0) continue;
+ assert.notEqual(
+ formatUiMessage(enTemplate, MESSAGE_VALUES, 'en'),
+ enTemplate,
+ `${domain}/en/${path}`,
+ );
+ assert.notEqual(
+ formatUiMessage(translatedTemplate, MESSAGE_VALUES, locale),
+ translatedTemplate,
+ `${domain}/${locale}/${path}`,
+ );
+ }
}
}
});
@@ -79,11 +83,13 @@ describe('TUI copy resources', () => {
test('keeps model picker copy locale-specific', () => {
assert.equal(getTuiPickerCopy('en').modelPickerTitle, 'Select Model');
assert.equal(getTuiPickerCopy('en').searchLabel, 'Search');
- assert.equal(getTuiPickerCopy('zh').modelPickerTitle, '选择模型');
- assert.equal(getTuiPickerCopy('zh').searchLabel, '搜索');
- assert.equal(getTuiPickerCopy('zh').selectPickerHint, '↑↓ 选择 · Enter 确认 · Esc 关闭');
- assert.equal(getTuiPickerCopy('zh').currentMarker, '当前');
- assert.equal(getTuiPickerCopy('zh').defaultMarker, '默认');
+ assert.equal(getTuiPickerCopy('zh-CN').modelPickerTitle, '选择模型');
+ assert.equal(getTuiPickerCopy('zh-CN').searchLabel, '搜索');
+ assert.equal(getTuiPickerCopy('zh-TW').modelPickerTitle, '選擇模型');
+ assert.equal(getTuiPickerCopy('zh-TW').searchLabel, '搜尋');
+ assert.equal(getTuiPickerCopy('zh-TW').selectPickerHint, '↑↓ 選擇 · Enter 確認 · Esc 關閉');
+ assert.equal(getTuiPickerCopy('zh-CN').currentMarker, '当前');
+ assert.equal(getTuiPickerCopy('zh-TW').defaultMarker, '預設');
});
test('formats the English MCP count with ICU plural rules', () => {
@@ -93,23 +99,48 @@ describe('TUI copy resources', () => {
assert.equal(formatUiMessage(template, { count: 2 }, 'en'), '2 tools');
});
+ test('localizes rewind and skill notices in both locales', () => {
+ assert.equal(TUI_COPY_RESOURCES.rewind.en.noTargets, 'No turns to rewind to.');
+ assert.equal(TUI_COPY_RESOURCES.rewind['zh-CN'].noTargets, '没有可回退的轮次。');
+ assert.equal(
+ formatUiMessage(
+ TUI_COPY_RESOURCES.skills.en.failedItem,
+ { request: 'nope', reason: TUI_COPY_RESOURCES.skills.en.failureReasons.not_found },
+ 'en',
+ ),
+ '/skill:nope (not found)',
+ );
+ assert.equal(
+ formatUiMessage(
+ TUI_COPY_RESOURCES.skills['zh-CN'].failedItem,
+ { request: 'nope', reason: TUI_COPY_RESOURCES.skills['zh-CN'].failureReasons.not_found },
+ 'zh-CN',
+ ),
+ '/skill:nope(未找到)',
+ );
+ });
+
test('localizes stable onboarding failure codes at the TUI boundary', () => {
assert.equal(
onboardingFailureMessage({ kind: 'rejected', reason: 'connection_not_found' }, 'en'),
'This connection no longer exists. Reopen /setup and try again.',
);
assert.equal(
- onboardingFailureMessage({ kind: 'rejected', reason: 'connection_not_found' }, 'zh'),
+ onboardingFailureMessage({ kind: 'rejected', reason: 'connection_not_found' }, 'zh-CN'),
'该连接已不存在,请重新打开 /setup 后重试。',
);
assert.equal(
- onboardingFailureMessage({ kind: 'failed', errorClass: 'auth' }, 'zh'),
+ onboardingFailureMessage({ kind: 'failed', errorClass: 'auth' }, 'zh-CN'),
'服务商身份验证失败,请检查 API key 后重试。',
);
assert.equal(
- onboardingFailureMessage({ kind: 'unavailable' }, 'zh'),
+ onboardingFailureMessage({ kind: 'unavailable' }, 'zh-CN'),
'无法连接 Runtime Host,请重试。',
);
+ assert.equal(
+ onboardingFailureMessage({ kind: 'failed', errorClass: 'auth' }, 'zh-TW'),
+ '服務商身分驗證失敗,請檢查 API key 後重試。',
+ );
});
});
diff --git a/packages/cli/src/__tests__/tui-copy-command.test.ts b/packages/cli/src/__tests__/tui-copy-command.test.ts
index fa9dc3cd52..b62e181dc1 100644
--- a/packages/cli/src/__tests__/tui-copy-command.test.ts
+++ b/packages/cli/src/__tests__/tui-copy-command.test.ts
@@ -155,7 +155,7 @@ describe('serializeTranscriptText', () => {
describe('getTuiCopyCopy', () => {
test('resolves localized copy for each locale', () => {
assert.equal(typeof getTuiCopyCopy('en').nothingToCopy, 'string');
- assert.equal(typeof getTuiCopyCopy('zh').nothingToCopy, 'string');
+ assert.equal(typeof getTuiCopyCopy('zh-CN').nothingToCopy, 'string');
assert.ok(getTuiCopyCopy('en').copiedLast.includes('{count'));
const tooLarge = getTuiCopyCopy('en').tooLarge;
assert.ok(tooLarge.includes('{bytes}') && tooLarge.includes('{limit}'));
diff --git a/packages/cli/src/__tests__/tui-primary-guidance.test.ts b/packages/cli/src/__tests__/tui-primary-guidance.test.ts
index bb6330b9cf..01d49a06ff 100644
--- a/packages/cli/src/__tests__/tui-primary-guidance.test.ts
+++ b/packages/cli/src/__tests__/tui-primary-guidance.test.ts
@@ -28,26 +28,26 @@ describe('TUI primary guidance catalog', () => {
.map((command) => command.id)
.sort();
- for (const locale of ['zh', 'en'] as const) {
+ for (const locale of ['zh-CN', 'en'] as const) {
assert.deepEqual(Object.keys(getTuiPrimaryGuidance(locale).commands).sort(), expected);
}
});
test('keeps keybinding tokens identical across locales', () => {
- const tokens = (locale: 'zh' | 'en') =>
+ const tokens = (locale: 'zh-CN' | 'en') =>
getTuiPrimaryGuidance(locale).help.keybindings.flatMap((line) => {
const match = /^\s*(.*?)\s*—/u.exec(line);
return match ? [match[1]!.replace(/\s*[((].*$/u, '')] : [];
});
- assert.deepEqual(tokens('zh'), tokens('en'));
+ assert.deepEqual(tokens('zh-CN'), tokens('en'));
});
test('renders Option shortcut labels on macOS and Alt labels elsewhere', () => {
- const guidanceFor = (locale: 'zh' | 'en', platform: NodeJS.Platform) =>
+ const guidanceFor = (locale: 'zh-CN' | 'en', platform: NodeJS.Platform) =>
getTuiPrimaryGuidance(locale, platform);
- for (const locale of ['zh', 'en'] as const) {
+ for (const locale of ['zh-CN', 'en'] as const) {
const macKeybindings = guidanceFor(locale, 'darwin').help.keybindings.join('\n');
assert.match(macKeybindings, /⌥\+Enter/u);
assert.match(macKeybindings, /⌥\+↑/u);
diff --git a/packages/cli/src/__tests__/tui-session-status.test.ts b/packages/cli/src/__tests__/tui-session-status.test.ts
index 1336d599e6..dd2d59f33c 100644
--- a/packages/cli/src/__tests__/tui-session-status.test.ts
+++ b/packages/cli/src/__tests__/tui-session-status.test.ts
@@ -47,7 +47,7 @@ describe('TUI Session status badge', () => {
),
'needs permission',
);
- assert.equal(sessionStatusBadge(session({ status: 'waiting_for_user' }), 'zh'), '等你确认');
+ assert.equal(sessionStatusBadge(session({ status: 'waiting_for_user' }), 'zh-CN'), '等你确认');
});
test('marks only actionable blocked reasons and keeps resting rows unmarked', () => {
@@ -74,7 +74,7 @@ describe('TUI Session status badge', () => {
}
assert.equal(sessionStatusBadge(session({ status: 'active' }), 'en'), undefined);
assert.equal(sessionStatusBadge(session({ status: 'aborted' }), 'en'), 'stopped');
- assert.equal(sessionStatusBadge(session({ status: 'aborted' }), 'zh'), '已中止');
+ assert.equal(sessionStatusBadge(session({ status: 'aborted' }), 'zh-CN'), '已中止');
});
});
diff --git a/packages/cli/src/cli-ui-locale.ts b/packages/cli/src/cli-ui-locale.ts
index 21dcf979dd..a32cad9412 100644
--- a/packages/cli/src/cli-ui-locale.ts
+++ b/packages/cli/src/cli-ui-locale.ts
@@ -33,11 +33,17 @@ export type CliUiLocaleResolution =
/** Resolve the interactive TUI locale without changing the top-level CLI grammar. */
export function resolveCliUiLocale(environment: CliEnvironment): CliUiLocaleResolution {
const rawPreference = environment.MAKA_LOCALE?.trim();
- const preference = rawPreference ? rawPreference.toLowerCase() : 'auto';
+ const normalizedPreference = rawPreference?.replaceAll('_', '-').toLowerCase() || 'auto';
+ const preference =
+ normalizedPreference === 'zh' || normalizedPreference === 'zh-cn'
+ ? 'zh-CN'
+ : normalizedPreference === 'zh-tw'
+ ? 'zh-TW'
+ : normalizedPreference;
if (!isUiLocalePreference(preference)) {
return {
ok: false,
- message: `Invalid MAKA_LOCALE ${JSON.stringify(rawPreference)}. Expected zh, en, or auto.`,
+ message: `Invalid MAKA_LOCALE ${JSON.stringify(rawPreference)}. Expected zh-CN, zh-TW, en, or auto.`,
};
}
diff --git a/packages/cli/src/pi-tui-mcp-status.ts b/packages/cli/src/pi-tui-mcp-status.ts
index d9dbff2fed..ff61bcae81 100644
--- a/packages/cli/src/pi-tui-mcp-status.ts
+++ b/packages/cli/src/pi-tui-mcp-status.ts
@@ -28,6 +28,7 @@ import {
type TUI,
} from '@earendil-works/pi-tui';
import type { McpProtocolPreference, McpServerConfig } from '@maka/core/mcp';
+import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
import {
defineUiMessageCatalog,
formatUiMessage,
@@ -67,6 +68,7 @@ interface TuiMcpStatusCopy {
readonly configuredOnly: string;
readonly configPending: string;
readonly toolCount: string;
+ readonly connectionFailed: string;
readonly editor: {
readonly addTitle: string;
readonly transportTitle: string;
@@ -732,7 +734,17 @@ function serverLines(server: TuiMcpServerSnapshot, locale: UiLocale, selected: b
const cursor = selected ? ansi.accent('›') : ' ';
return [
`${cursor} ${statusMarker(server.state)} ${ansi.bold(server.serverId)} ${details}`,
- ...(server.error ? [` ${ansi.red(server.error)}`] : []),
+ ...(server.error
+ ? [
+ ` ${ansi.red(
+ generalizedErrorMessageForLocale(
+ new Error(server.error),
+ locale === 'en' ? server.error : copy.connectionFailed,
+ locale,
+ ),
+ )}`,
+ ]
+ : []),
];
}
@@ -763,7 +775,7 @@ function actionNotice(
}
function resultCopy(locale: UiLocale, code: TuiMcpResultCode): string {
- return MCP_STATUS_COPY[locale].editor.results[code];
+ return MCP_STATUS_COPY[locale].editor.results[code] ?? code;
}
function confirmAddDocument(draft: GuidedDraft, locale: UiLocale): string[] {
diff --git a/packages/cli/src/tui-copy-catalog.ts b/packages/cli/src/tui-copy-catalog.ts
index 3cfdd9cf26..44f8efa249 100644
--- a/packages/cli/src/tui-copy-catalog.ts
+++ b/packages/cli/src/tui-copy-catalog.ts
@@ -33,7 +33,7 @@ export const TUI_COPY_RESOURCES = {
roleGoalContinuation: 'Goal continuation (autonomous):',
roleLegacyAutomation: 'Legacy automation (history only):',
},
- zh: {
+ 'zh-CN': {
copiedLast:
'已将最后一条回复发送到终端 · {count} 个字符——仅当终端(以及 tmux,如使用)允许写入剪贴板时才会真正复制。',
copiedAll:
@@ -47,6 +47,20 @@ export const TUI_COPY_RESOURCES = {
roleGoalContinuation: '目标续跑(自主):',
roleLegacyAutomation: '旧版自动化(仅历史):',
},
+ 'zh-TW': {
+ copiedLast:
+ '已將最後一則回覆傳送至終端機 · {count} 個字元——僅當終端機(以及 tmux,如有使用)允許寫入剪貼簿時才會真正複製。',
+ copiedAll:
+ '已將整段對話傳送至終端機 · {count} 個字元——僅當終端機(以及 tmux,如有使用)允許寫入剪貼簿時才會真正複製。',
+ tooLarge:
+ '內容過大,無法複製 · {bytes} 位元組超過 {limit} 位元組上限;更大的內容會被終端機直接捨棄。',
+ nothingToCopy: '目前沒有可複製的內容。',
+ usage: '用法:/copy [all]',
+ roleUser: '你:',
+ roleAssistant: 'Maka:',
+ roleGoalContinuation: 'Goal 繼續執行(自主):',
+ roleLegacyAutomation: '舊版自動化(僅歷史):',
+ },
},
'mcp-status': {
en: {
@@ -89,6 +103,7 @@ export const TUI_COPY_RESOURCES = {
configuredOnly: 'configured only',
configPending: 'config pending',
toolCount: '{count, plural, one {# tool} other {# tools}}',
+ connectionFailed: 'MCP server connection failed',
editor: {
addTitle: 'Add MCP server',
transportTitle: 'Transport',
@@ -154,7 +169,7 @@ export const TUI_COPY_RESOURCES = {
},
},
},
- zh: {
+ 'zh-CN': {
title: 'MCP 服务器',
footer: {
back: 'Esc 返回',
@@ -191,6 +206,7 @@ export const TUI_COPY_RESOURCES = {
configuredOnly: '仅已配置',
configPending: '配置待同步',
toolCount: '{count} 个工具',
+ connectionFailed: 'MCP 服务器连接失败',
editor: {
addTitle: '添加 MCP 服务器',
transportTitle: '传输方式',
@@ -252,6 +268,106 @@ export const TUI_COPY_RESOURCES = {
},
},
},
+ 'zh-TW': {
+ title: 'MCP 伺服器',
+ footer: {
+ back: 'Esc 返回',
+ readOnly: '↑/↓ 捲動 · q/Esc 關閉',
+ manage: 'a 新增 · Enter 編輯 · Space 啟用/停用 · t 測試 · r 重新連線 · d 刪除 · Esc 關閉',
+ managePublication:
+ 'a 新增 · Enter 編輯 · Space 啟用/停用 · t 測試 · r 重新連線 · d 刪除 · p 設定 Provider 認證資料 · x 刪除認證資料 · Esc 關閉',
+ },
+ unavailableTitle: '目前 TUI 未連線至本機 MCP 控制介面。',
+ unavailableDetail: '遠端 Runtime Host 的用戶端 MCP 工具關聯將於後續版本提供。',
+ loading: '正在讀取 mcp.json 並探索工具…',
+ loadError: '無法讀取或套用 MCP 設定;未向 Runtime Host 發佈任何工具。',
+ noServers: '尚未設定 MCP 伺服器。按 a 新增。',
+ publication: {
+ waiting: '等待發佈',
+ host_unavailable: 'Runtime Host 重新連線中',
+ credential_required: '需要 Provider 認證資料',
+ credential_rejected: 'Provider 認證資料遭拒',
+ provider_conflict: 'Provider 已在另一個 TUI 中執行',
+ target_mismatch: 'Provider 目標不符',
+ publishing: '正在發佈',
+ published: '已發佈',
+ not_published: '未發佈',
+ error: '發佈失敗',
+ },
+ serverState: {
+ disabled: '已停用',
+ disconnected: '未連線',
+ connecting: '連線中',
+ connected: '已連線',
+ 'needs-auth': '需要登入',
+ error: '錯誤',
+ },
+ configuredOnly: '僅已設定',
+ configPending: '設定待同步',
+ toolCount: '{count} 個工具',
+ connectionFailed: 'MCP 伺服器連線失敗',
+ editor: {
+ addTitle: '新增 MCP 伺服器',
+ transportTitle: '傳輸方式',
+ protocolTitle: '通訊協定偏好',
+ confirmAddTitle: '新增這個 MCP 伺服器?',
+ confirmImportTitle: '匯入 MCP 伺服器?',
+ confirmRemoveTitle: '刪除 {serverId}?',
+ confirmRemoveCredentialTitle: '移除遠端 Provider 認證資料?',
+ confirmRemoveCredentialDetail: '這將停止向所選 Runtime Host 發佈 MCP 工具。',
+ confirmHint: 'y 確認 · Esc 取消',
+ synchronizing: '設定同步中…',
+ outOfSync: '持久化設定與 MCP Manager 尚未同步。',
+ action: {
+ test: '正在測試 MCP 伺服器…',
+ reconnect: '正在重新連線 MCP 伺服器…',
+ apply: '正在套用 MCP 設定…',
+ },
+ inputLabels: {
+ server_id: '伺服器 ID',
+ command: '命令',
+ args: '參數',
+ url: 'Streamable HTTP URL',
+ cwd: '工作目錄',
+ env: '環境變數',
+ headers: '請求標頭',
+ edit: '編輯伺服器 JSON',
+ import: '貼上 MCP JSON',
+ publication_credential: 'Provider 認證資料',
+ },
+ hints: {
+ args: 'JSON 字串陣列,可留空',
+ map: 'JSON 字串對映,可留空',
+ optional: '可留空',
+ submit: 'Enter 送出 · Esc 返回',
+ credential:
+ '僅儲存於本機認證資料存放區;不會寫入設定檔、參數或聊天內容 · Enter 送出 · Esc 返回',
+ },
+ results: {
+ exists: '該伺服器 ID 已存在。',
+ stale_config: 'MCP 設定已變更,請重試。',
+ stale_edit: '該伺服器已變更,請重新開啟後編輯。',
+ stale_import: '匯入項目已變更,請重新預覽。',
+ missing: '該伺服器已不存在。',
+ closed: 'MCP 控制器已關閉。',
+ 'invalid-config': '伺服器設定無效。',
+ 'credential-cleanup-failed': '無法移除已儲存的認證資料;設定未變更。',
+ 'publication-credential-failed': '無法儲存或套用 Provider 認證資料。',
+ 'persist-failed': '無法儲存設定。',
+ 'manager-failed': 'MCP 連線操作失敗。',
+ turn_active: 'Turn 或其他控制操作執行期間無法修改 MCP。',
+ invalid: '請檢查輸入後重試。',
+ published: '設定已儲存,工具已重新整理。',
+ pending_host: '設定已儲存,正等待 Runtime Host 發佈。',
+ sync_failed: '設定已儲存,但 MCP Manager 尚未同步。',
+ publication_failed: '設定已儲存,但 capability 發佈失敗。',
+ test_ok: '連線測試通過。',
+ test_failed: '連線測試失敗。',
+ test_publication_failed: '連線測試通過,但 capability 發佈失敗。',
+ test_pending_host: '連線測試通過,正等待 Runtime Host 發佈。',
+ },
+ },
+ },
},
'form-interaction': {
en: {
@@ -280,7 +396,7 @@ export const TUI_COPY_RESOURCES = {
falseValue: 'false',
selectedCount: '{count} selected',
},
- zh: {
+ 'zh-CN': {
requestedBy: '由 {detail} 请求',
sensitiveWarning: '请勿输入密码、API 密钥、访问令牌或支付信息。',
required: '必填',
@@ -306,6 +422,32 @@ export const TUI_COPY_RESOURCES = {
falseValue: '否',
selectedCount: '已选择 {count} 项',
},
+ 'zh-TW': {
+ requestedBy: '由 {detail} 請求',
+ sensitiveWarning: '請勿輸入密碼、API 金鑰、存取權杖或付款資訊。',
+ required: '必填',
+ optional: '選填',
+ omitted: '未提供',
+ empty: '空',
+ invalid: '該值不符合欄位約束',
+ minimumLength: '至少 {minimum} 個字元',
+ maximumLength: '最多 {maximum} 個字元',
+ lengthRange: '長度 {minimum}–{maximum} 個字元',
+ minimumValue: '最小值 {minimum}',
+ maximumValue: '最大值 {maximum}',
+ valueRange: '範圍 {minimum}–{maximum}',
+ minimumItems: '至少選取 {minimum} 項',
+ maximumItems: '最多選取 {maximum} 項',
+ itemRange: '選取 {minimum}–{maximum} 項',
+ format: '格式:{format}',
+ reviewHint: '↑↓ 選取欄位 · Enter 編輯 · Space 提供/省略 · s 提交 · d 拒絕 · Esc 取消',
+ textHint: '輸入值 · Enter 返回檢查 · Esc 返回檢查',
+ choiceHint: '↑↓ 選取 · Enter 返回檢查 · Esc 返回檢查',
+ multiHint: '↑↓ 移動 · Space 切換 · Enter 返回檢查 · Esc 返回檢查',
+ trueValue: '是',
+ falseValue: '否',
+ selectedCount: '已選取 {count} 項',
+ },
},
pickers: {
en: {
@@ -409,7 +551,7 @@ export const TUI_COPY_RESOURCES = {
closeAction: 'Enter to close',
thinkingUnsupported: 'This model does not support changing the thinking level.',
},
- zh: {
+ 'zh-CN': {
modelPickerTitle: '选择模型',
modelSwitchCacheWarning: '⚠ 切换模型可能需要重建提示缓存;下一次请求可能更慢或成本更高。',
modelSearchHint: '搜索模型 / 服务商 / 连接 · ↑↓ 选择 · Enter 确认 · Esc 取消',
@@ -498,6 +640,95 @@ export const TUI_COPY_RESOURCES = {
closeAction: 'Enter 关闭',
thinkingUnsupported: '当前模型不支持思考级别切换。',
},
+ 'zh-TW': {
+ modelPickerTitle: '選擇模型',
+ modelSwitchCacheWarning: '⚠ 切換模型可能需要重建提示快取;下一次請求可能較慢或成本較高。',
+ modelSearchHint: '搜尋模型 / 服務商 / 連線 · ↑↓ 選擇 · Enter 確認 · Esc 取消',
+ searchLabel: '搜尋',
+ noMatchingModels: '沒有符合的模型',
+ selectPickerHint: '↑↓ 選擇 · Enter 確認 · Esc 關閉',
+ providerConfigured: '已設定',
+ addAccount: '新增帳號',
+ thinkingLevels: {
+ off: '關',
+ minimal: '最小',
+ low: '低',
+ medium: '中',
+ high: '高',
+ xhigh: '超高',
+ max: '最高',
+ },
+ defaultThinkingLevel: '預設',
+ thinkingPickerTitle: '選擇思考級別',
+ currentMarker: '目前',
+ defaultMarker: '預設',
+ setupTitle: '設定模型服務商',
+ baseUrlLabel: 'Base URL',
+ apiKeyLabel: 'API key',
+ connectionNameLabel: '名稱',
+ connectionSlugLabel: '識別碼(slug)',
+ identityHint: '為該連線起名,或保留預設值自動分配 · Enter 繼續 · Esc 返回',
+ identitySlugInvalid: '識別碼需為 2–64 位小寫字母、數字或連字號',
+ identityNameTooLong: '名稱不能超過 256 個字元',
+ onboardingUnavailable: 'Onboarding 無法使用:目前執行環境未提供設定入口。',
+ onboardingRequestFailed: '無法連線 Runtime Host,請重試。',
+ onboardingRejections: {
+ provider_unsupported: '此服務商不支援 API key 設定。',
+ connection_not_found: '此連線已不存在,請重新開啟 /setup 後重試。',
+ credential_not_configured: '需要填寫 API key。',
+ base_url_not_configured: '此服務商需要填寫 Base URL。',
+ catalog_full: '連線目錄已滿。',
+ model_unavailable: '所選模型已無法使用,請重新驗證並選擇。',
+ slug_taken: '該識別碼已被佔用。請更換,或清空後自動分配。',
+ superseded: '此連線已變更,請重新開啟 /setup 後重試。',
+ },
+ onboardingFailures: {
+ auth: '服務商身分驗證失敗,請檢查 API key 後重試。',
+ timeout: '服務商驗證逾時,請重試。',
+ provider_unavailable: '服務商目前無法使用,請稍後重試。',
+ network: '網路錯誤中斷了服務商驗證,請重試。',
+ invalid_response: '服務商傳回無效回應,請重試。',
+ unknown: '服務商驗證失敗,請重試。',
+ },
+ accountSavedRefreshFailed: '帳號已儲存,但模型清單尚未重新整理。重新啟動 Maka 後會重新載入。',
+ listProvidersFailed: '無法讀取已設定的連線。',
+ noConfigurableProviders: '沒有可設定的 API key 服務商。',
+ baseUrlRequired: '需要填寫 Base URL',
+ baseUrlInvalid: 'Base URL 不是有效的 URL',
+ baseUrlProtocol: 'Base URL 必須使用 http 或 https',
+ baseUrlCredentials: 'Base URL 不能包含帳號密碼',
+ baseUrlQuery: 'Base URL 不能包含查詢字串或片段',
+ baseUrlTooLong: 'Base URL 不能超過 2048 位元組',
+ selectModelBeforeSaving: '至少選擇一個模型再儲存',
+ reuseBaseUrlHint: '留空以重用已儲存的 Base URL,或輸入新位址替換 · Esc 返回選擇服務商',
+ enterBaseUrlHint: '輸入中轉站的 Base URL(http/https)· Esc 返回選擇服務商',
+ continueAction: 'Enter 繼續',
+ providerSearchHint: '搜尋服務商,↑↓ 選擇 · Enter 確認 · Esc 取消',
+ noMatchingProviders: '沒有符合的服務商',
+ keyHints: {
+ reuse: {
+ baseUrl: '留空以重用已儲存的 key,或輸入新 key 輪換 · Esc 返回 Base URL',
+ provider: '留空以重用已儲存的 key,或輸入新 key 輪換 · Esc 返回選擇服務商',
+ // identity 步驟只存在於新建流程,reuse 不會落到這裡;沿用 provider 文案保持記錄完整。
+ identity: '留空以重用已儲存的 key,或輸入新 key 輪換 · Esc 返回選擇服務商',
+ },
+ enter: {
+ baseUrl: '輸入 API key · 僅儲存在本機 · Esc 返回 Base URL',
+ provider: '輸入 API key · 僅儲存在本機 · Esc 返回選擇服務商',
+ identity: '輸入 API key · 僅儲存在本機 · Esc 返回命名連線',
+ },
+ },
+ submitAction: 'Enter 送出',
+ verifyingKey: '正在驗證 key…',
+ modelSelectionHint: '搜尋模型,↑↓ 選擇 · Space 切換 · Enter 儲存 · Esc 返回',
+ selectedModels: '已選 {count}',
+ selectedModelsAndSave: '已選 {count} · Enter 儲存',
+ saving: '正在儲存…',
+ complete: '完成',
+ enabledModels: '已啟用模型:{count}',
+ closeAction: 'Enter 關閉',
+ thinkingUnsupported: '目前模型不支援思考級別切換。',
+ },
},
'primary-guidance': {
en: {
@@ -551,7 +782,7 @@ export const TUI_COPY_RESOURCES = {
],
},
},
- zh: {
+ 'zh-CN': {
welcome: {
tagline: '陪你把事做完',
start: '输入消息开始对话',
@@ -602,6 +833,57 @@ export const TUI_COPY_RESOURCES = {
],
},
},
+ 'zh-TW': {
+ welcome: {
+ tagline: '陪你把事做完',
+ start: '輸入訊息開始對話',
+ session: '切換或恢復會話',
+ model: '切換模型',
+ setup: '設定模型服務商',
+ },
+ commands: {
+ compact: '壓縮會話上下文',
+ context: '檢視最近一次請求的上下文用量',
+ copy: '複製最後一則回覆(或 /copy all)到剪貼簿',
+ exit: '退出 Maka',
+ goal: '檢視自主目標狀態',
+ graph: '檢視、啟用、停用 Graph 模式,或執行一次 Graph 任務',
+ help: '檢視命令和快捷鍵',
+ mcp: '管理用戶端 MCP 伺服器與工具發佈狀態',
+ model: '選擇模型',
+ move: '將目前會話移到其他目錄',
+ new: '建立會話',
+ permissions: '設定會話權限',
+ recap: '用一句話總結目前會話',
+ rename: '重新命名目前會話',
+ resume: '從安全邊界恢復最近一次中斷的執行',
+ rewind: '回退到較早的對話輪次',
+ session: '切換或恢復會話',
+ setup: '設定模型服務商(API Key)',
+ side: '開啟臨時 Side Conversation',
+ skill: '呼叫 Skill(也可直接輸入 /skill:<name>)',
+ swarm: '檢視、啟用、停用 Swarm 模式,或執行一次 Swarm 任務',
+ thinking: '設定思考級別',
+ transcript: '在 Maka 內瀏覽完整對話記錄',
+ },
+ help: {
+ commandsHeading: '命令',
+ userCommand: ' !<command> — 執行一次僅使用者可見的 shell 命令',
+ keybindingsHeading: '快捷鍵',
+ keybindings: [
+ ' Ctrl+O — 展開或摺疊所有工具輸出',
+ ' Ctrl+T — 展開或摺疊檢視中的所有思考區塊',
+ ' 使用終端機或觸控板捲動對話記錄',
+ ' Enter(任務執行中)— 將訊息注入目前任務',
+ ' Alt+Enter(任務執行中)— 將訊息排入下一輪',
+ ' Alt+↑ — 將已排隊訊息取回編輯器重新編輯',
+ ' Esc Esc(任務執行中)— 中斷目前任務',
+ ' Esc Esc(空閒時)— 回退到較早的輪次',
+ ' Ctrl+C — 停止任務、清空輸入,或連續按兩次退出',
+ ' Ctrl+D — 輸入為空時退出',
+ ],
+ },
+ },
},
rewind: {
en: {
@@ -616,7 +898,7 @@ export const TUI_COPY_RESOURCES = {
pickerHint:
'Return to before the selected turn (discards it and later turns; the prompt refills the input box) · enter select / esc cancel',
},
- zh: {
+ 'zh-CN': {
pickerTitle: '回退',
pending: '正在回退到该轮之前…',
doneRefilled:
@@ -627,6 +909,17 @@ export const TUI_COPY_RESOURCES = {
busy: '无法回退:当前有正在进行的操作 — 请等待其完成,或中断(Esc)后重试。',
pickerHint: '回到选定轮次之前(丢弃该轮及之后,prompt 回填输入框) · enter 选择 / esc 取消',
},
+ 'zh-TW': {
+ pickerTitle: '回退',
+ pending: '正在回退到該輪之前…',
+ doneRefilled:
+ '已回退到該輪之前(分支為新任務,原任務保留),該輪 prompt 已回填輸入框,可修改後重新傳送。',
+ doneKeptDraft:
+ '已回退到該輪之前(分支為新任務,原任務保留)。輸入框已有未傳送內容,未覆蓋;該輪 prompt 已存入輸入歷史,可按 ↑ 找回。',
+ noTargets: '沒有可回退的輪次。',
+ busy: '無法回退:目前有正在進行的操作 — 請等待完成,或中斷(Esc)後重試。',
+ pickerHint: '回到選定輪次之前(捨棄該輪及之後,prompt 回填輸入框) · enter 選擇 / esc 取消',
+ },
},
skills: {
en: {
@@ -649,7 +942,7 @@ export const TUI_COPY_RESOURCES = {
too_many_requests: 'too many requests',
},
},
- zh: {
+ 'zh-CN': {
pickerTitle: '调用技能',
usage: 'Usage: /skill,或直接在消息中输入 /skill:<name>',
noneAvailable: '当前没有可调用的技能。',
@@ -669,6 +962,26 @@ export const TUI_COPY_RESOURCES = {
too_many_requests: '调用请求过多',
},
},
+ 'zh-TW': {
+ pickerTitle: '呼叫技能',
+ usage: 'Usage: /skill,或直接在訊息中輸入 /skill:<name>',
+ noneAvailable: '目前沒有可呼叫的技能。',
+ loaded: '已載入技能:{names}',
+ loadFailed: '無法載入技能 {failures};{outcome}',
+ outcomeNoRequest: '未發起模型請求。',
+ outcomeMarkersNotSent: '失敗的呼叫標記未傳送給模型。',
+ failedItem: '/skill:{request}({reason})',
+ tooManyRequestsItem: '請求超過 {limit} 個上限({reason})',
+ listSeparator: '、',
+ failureReasons: {
+ invalid_name: '名稱無效',
+ not_found: '找不到',
+ disabled: '已停用',
+ host_incompatible: '目前主機缺少其相依工具',
+ resolution_failed: '解析失敗',
+ too_many_requests: '呼叫請求過多',
+ },
+ },
},
'connection-identity': {
en: {
@@ -684,7 +997,7 @@ export const TUI_COPY_RESOURCES = {
accountDisabled:
'The original account is disabled. Enable it, or run /model and choose a new account and model.',
},
- zh: {
+ 'zh-CN': {
withRecovery: '{notice}{recovery}',
emptyChoiceRecovery:
'如果 /model 没有可选项,请先添加或启用连接(API Key 连接可运行 /setup)。',
@@ -693,6 +1006,15 @@ export const TUI_COPY_RESOURCES = {
identityMismatch: '任务保存的账号身份与当前连接不一致;运行 /model 重新选择账号和模型。',
accountDisabled: '原账号已停用;请启用该账号,或运行 /model 选择新账号和模型。',
},
+ 'zh-TW': {
+ withRecovery: '{notice}{recovery}',
+ emptyChoiceRecovery:
+ '如果 /model 沒有可選項,請先新增或啟用連線(API Key 連線可執行 /setup)。',
+ confirmAccount: '此任務來自舊版本,需要確認一次帳號。執行 /model 選擇現有帳號和模型。',
+ accountDeleted: '原帳號已刪除;執行 /model 選擇新帳號和模型後繼續。',
+ identityMismatch: '任務儲存的帳號身分與目前連線不一致;執行 /model 重新選擇帳號和模型。',
+ accountDisabled: '原帳號已停用;請啟用該帳號,或執行 /model 選擇新帳號和模型。',
+ },
},
'session-actions': {
en: {
@@ -700,10 +1022,14 @@ export const TUI_COPY_RESOURCES = {
newSessionFailed:
'Could not start a new session: a local command could not be stopped. Press Ctrl+C to stop it, then try again.',
},
- zh: {
+ 'zh-CN': {
foreignScanFailed: '读取外部对话失败:{detail}',
newSessionFailed: '无法开始新会话:无法停止本地命令。请按 Ctrl+C 停止命令后重试。',
},
+ 'zh-TW': {
+ foreignScanFailed: '讀取外部對話失敗:{detail}',
+ newSessionFailed: '無法開始新工作階段,請稍後重試。',
+ },
},
'pending-queue': {
en: {
@@ -711,11 +1037,16 @@ export const TUI_COPY_RESOURCES = {
queuedLabel: 'Queued:',
requeueHint: 'Alt+↑ take queued messages back to re-edit',
},
- zh: {
+ 'zh-CN': {
steeringLabel: '注入:',
queuedLabel: '已排队:',
requeueHint: 'Alt+↑ 取回队列以重新编辑',
},
+ 'zh-TW': {
+ steeringLabel: '調整方向:',
+ queuedLabel: '已排入佇列:',
+ requeueHint: 'Alt+↑ 取回佇列訊息重新編輯',
+ },
},
'session-status': {
en: {
@@ -726,7 +1057,7 @@ export const TUI_COPY_RESOURCES = {
signInRequired: 'needs sign-in',
stopped: 'stopped',
},
- zh: {
+ 'zh-CN': {
running: '进行中',
waitingForUser: '等你确认',
permissionRequired: '需要权限',
@@ -734,5 +1065,13 @@ export const TUI_COPY_RESOURCES = {
signInRequired: '需要重新登录',
stopped: '已中止',
},
+ 'zh-TW': {
+ running: '進行中',
+ waitingForUser: '等你確認',
+ permissionRequired: '需要權限',
+ connectionRequired: '需要連線',
+ signInRequired: '需要重新登入',
+ stopped: '已中止',
+ },
},
} as const;
diff --git a/packages/core/src/__tests__/redaction.test.ts b/packages/core/src/__tests__/redaction.test.ts
index 4e593790a5..57094a2c32 100644
--- a/packages/core/src/__tests__/redaction.test.ts
+++ b/packages/core/src/__tests__/redaction.test.ts
@@ -23,6 +23,8 @@ import { describe, test } from 'node:test';
import {
generalizedErrorMessage,
generalizedErrorMessageChinese,
+ generalizedErrorMessageForLocale,
+ generalizedErrorMessageTraditionalChinese,
redactSecrets,
} from '../redaction.js';
@@ -381,3 +383,38 @@ describe('generalizedErrorMessageChinese', () => {
);
});
});
+
+describe('localized generalized error messages', () => {
+ test('routes one shared classification through each locale catalog', () => {
+ const error = new Error('HTTP 503 from provider');
+ assert.equal(
+ generalizedErrorMessageForLocale(error, 'fallback', 'en'),
+ 'Provider returned an error',
+ );
+ assert.equal(generalizedErrorMessageForLocale(error, '後備', 'zh-CN'), '模型服务返回错误');
+ assert.equal(generalizedErrorMessageForLocale(error, '備援', 'zh-TW'), '模型服務傳回錯誤');
+ });
+
+ test('uses Taiwan terminology for Traditional Chinese categories', () => {
+ for (const [raw, expected] of [
+ ['Request timeout after 30s', '請求逾時'],
+ ['HTTP 429 Too Many Requests', '已達模型速率限制'],
+ ['401 Unauthorized', '驗證失敗'],
+ ['HTTP 500 Internal Server Error', '模型服務傳回錯誤'],
+ ['network unreachable', '網路錯誤'],
+ ['something weird happened', '操作失敗'],
+ ]) {
+ assert.equal(generalizedErrorMessageTraditionalChinese(new Error(raw)), expected);
+ }
+ });
+
+ test('routes each resolved locale without changing the supplied fallback', () => {
+ const error = new Error('something weird happened');
+ assert.equal(generalizedErrorMessageForLocale(error, '简中后备', 'zh-CN'), '简中后备');
+ assert.equal(generalizedErrorMessageForLocale(error, '繁中備援', 'zh-TW'), '繁中備援');
+ assert.equal(
+ generalizedErrorMessageForLocale(error, 'English fallback', 'en'),
+ 'English fallback',
+ );
+ });
+});
diff --git a/packages/core/src/__tests__/relative-time.test.ts b/packages/core/src/__tests__/relative-time.test.ts
index 2507f2c167..9cb6cc031d 100644
--- a/packages/core/src/__tests__/relative-time.test.ts
+++ b/packages/core/src/__tests__/relative-time.test.ts
@@ -37,13 +37,13 @@ describe('relative timestamp labels', () => {
resetRelativeTimeFormatters();
for (const ageMs of [0, 1_000, 30_000, 59_999]) {
- assert.equal(formatRelativeTimestamp(NOW - ageMs, NOW, 'zh'), '刚刚');
+ assert.equal(formatRelativeTimestamp(NOW - ageMs, NOW, 'zh-CN'), '刚刚');
assert.equal(formatRelativeTimestamp(NOW - ageMs, NOW, 'en'), 'just now');
- assert.equal(formatCompactTimestamp(NOW - ageMs, NOW, 'zh'), '刚刚');
- assert.equal(formatSidebarTimestamp(NOW - ageMs, NOW, 'zh'), '刚刚');
+ assert.equal(formatCompactTimestamp(NOW - ageMs, NOW, 'zh-CN'), '刚刚');
+ assert.equal(formatSidebarTimestamp(NOW - ageMs, NOW, 'zh-CN'), '刚刚');
}
- assert.equal(formatRelativeTimestamp(NOW - 60_000, NOW, 'zh'), '1分钟前');
+ assert.equal(formatRelativeTimestamp(NOW - 60_000, NOW, 'zh-CN'), '1分钟前');
assert.equal(formatRelativeTimestamp(NOW - 60_000, NOW, 'en'), '1 minute ago');
});
@@ -54,7 +54,7 @@ describe('relative timestamp labels', () => {
});
it('uses scan-friendly units for sidebar timestamps', () => {
- for (const locale of ['zh', 'en'] as const) {
+ for (const locale of ['zh-CN', 'en'] as const) {
for (const [ageMs, expected] of [
[60_000, '1min'],
[46 * 60_000, '46min'],
diff --git a/packages/core/src/__tests__/settings.test.ts b/packages/core/src/__tests__/settings.test.ts
index acc1106af7..b9eee9c4ac 100644
--- a/packages/core/src/__tests__/settings.test.ts
+++ b/packages/core/src/__tests__/settings.test.ts
@@ -100,6 +100,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,
+ },
+ });
+ assert.strictEqual(normalized.personalization.uiLocale, uiLocale);
+ }
+
+ const normalizedLegacy = normalizeSettings({
+ personalization: {
+ displayName: '',
+ assistantTone: '',
+ uiLocale: 'zh',
+ selectedPetId: null,
+ },
+ });
+ assert.strictEqual(normalizedLegacy.personalization.uiLocale, 'zh-CN');
+ });
+});
+
test('shell settings default, normalize, and merge through their shared boundary', () => {
const defaults = createDefaultSettings();
assert.deepStrictEqual(defaults.shell, { preference: 'auto', executable: '' });
diff --git a/packages/core/src/__tests__/tool-quiet-preview.test.ts b/packages/core/src/__tests__/tool-quiet-preview.test.ts
index 49037bf1e1..1404b67ad2 100644
--- a/packages/core/src/__tests__/tool-quiet-preview.test.ts
+++ b/packages/core/src/__tests__/tool-quiet-preview.test.ts
@@ -66,9 +66,19 @@ describe('formatToolInvocationLine', () => {
],
},
},
- 'zh',
+ 'zh-CN',
);
assert.equal(line, '选哪个方案? 等 2 问');
+ assert.equal(
+ formatToolInvocationLine(
+ {
+ toolName: 'AskUserQuestion',
+ args: { questions: [{ question: '選哪個方案?' }, { question: '繼續嗎?' }] },
+ },
+ 'zh-TW',
+ ),
+ '選哪個方案? 等 2 問',
+ );
});
it('keeps the ScheduledTask title headline', () => {
@@ -77,7 +87,7 @@ describe('formatToolInvocationLine', () => {
toolName: 'ScheduledTask',
args: { title: '每天 9:00 生成日报', schedule: { kind: 'cron' } },
},
- 'zh',
+ 'zh-CN',
);
assert.equal(line, '每天 9:00 生成日报');
});
@@ -170,7 +180,7 @@ describe('projectToolArgsPreview', () => {
size: { cols: 80, rows: 24 },
});
const preview = projectToolArgsPreview('WriteStdin', projected);
- const line = formatToolInvocationLine({ toolName: 'WriteStdin', args: preview }, 'zh');
+ const line = formatToolInvocationLine({ toolName: 'WriteStdin', args: preview }, 'zh-CN');
assert.ok(line !== undefined);
assert.match(line, /后台终端交互/);
assert.match(line, /80x24/);
diff --git a/packages/core/src/__tests__/ui-locale.test.ts b/packages/core/src/__tests__/ui-locale.test.ts
index 6fcc097723..3304657925 100644
--- a/packages/core/src/__tests__/ui-locale.test.ts
+++ b/packages/core/src/__tests__/ui-locale.test.ts
@@ -24,11 +24,61 @@ import {
defineUiMessageCatalog,
formatUiMessage,
isUiLocale,
+ isUiLocalePreference,
+ normalizeUiLocalePreference,
resolveSystemUiLocale,
+ resolveUiLocale,
resolveUiMessageCatalog,
uiLocaleToIntlLocale,
} from '../ui-locale.js';
+describe('UI locale', () => {
+ it('accepts only the supported resolved locales and preferences', () => {
+ assert.equal(['zh-CN', 'zh-TW', 'en'].every(isUiLocale), true);
+ assert.equal(isUiLocale('zh'), false);
+ assert.equal(['auto', 'zh-CN', 'zh-TW', 'en'].every(isUiLocalePreference), true);
+ });
+
+ it('normalizes the legacy persisted preference without widening the locale contract', () => {
+ assert.equal(normalizeUiLocalePreference('zh'), 'zh-CN');
+ assert.equal(normalizeUiLocalePreference('zh-TW'), 'zh-TW');
+ assert.equal(normalizeUiLocalePreference('unsupported'), 'auto');
+ });
+
+ 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) {
+ it(`resolves system languages ${languages.join(',')} to ${expected}`, () => {
+ assert.equal(resolveSystemUiLocale(languages), expected);
+ });
+ }
+
+ it('resolves explicit preferences and overrides before the system locale', () => {
+ assert.equal(resolveUiLocale('auto', 'zh-TW'), 'zh-TW');
+ assert.equal(resolveUiLocale('zh-CN', 'zh-TW'), 'zh-CN');
+ assert.equal(resolveUiLocale('zh-CN', 'zh-CN', 'en'), 'en');
+ });
+
+ it('keeps every locale guard and formatter in step with UI_LOCALES', () => {
+ for (const locale of UI_LOCALES) {
+ assert.ok(isUiLocale(locale), locale);
+ assert.equal(resolveSystemUiLocale([locale]), locale);
+ assert.equal(uiLocaleToIntlLocale(locale), locale);
+ }
+ const intlLocales = UI_LOCALES.map(uiLocaleToIntlLocale);
+ assert.equal(new Set(intlLocales).size, UI_LOCALES.length);
+ });
+});
+
describe('UI message catalogs', () => {
it('falls back to complete English copy for missing translations', () => {
const catalog = defineUiMessageCatalog<{
@@ -36,12 +86,13 @@ describe('UI message catalogs', () => {
detail: { ready: string; waiting: string };
}>()({
en: { title: 'Status', detail: { ready: 'Ready', waiting: 'Waiting' } },
- zh: { title: '状态', detail: { ready: '就绪' } },
+ 'zh-CN': { title: '状态', detail: { ready: '就绪' } },
});
assert.deepEqual(resolveUiMessageCatalog(catalog), {
en: { title: 'Status', detail: { ready: 'Ready', waiting: 'Waiting' } },
- zh: { title: '状态', detail: { ready: '就绪', waiting: 'Waiting' } },
+ 'zh-CN': { title: '状态', detail: { ready: '就绪', waiting: 'Waiting' } },
+ 'zh-TW': { title: 'Status', detail: { ready: 'Ready', waiting: 'Waiting' } },
});
});
@@ -56,13 +107,4 @@ describe('UI message catalogs', () => {
assert.equal(formatUiMessage('Hello {name}', {}, 'en'), 'Hello {name}');
assert.equal(formatUiMessage('{constructor}', {}, 'en'), '{constructor}');
});
-
- it('keeps every locale guard in step with UI_LOCALES', () => {
- for (const locale of UI_LOCALES) {
- assert.ok(isUiLocale(locale), locale);
- assert.equal(resolveSystemUiLocale([locale]), locale);
- }
- const intlLocales = UI_LOCALES.map(uiLocaleToIntlLocale);
- assert.equal(new Set(intlLocales).size, UI_LOCALES.length);
- });
});
diff --git a/packages/core/src/redaction.ts b/packages/core/src/redaction.ts
index 9165533c73..add461e25a 100644
--- a/packages/core/src/redaction.ts
+++ b/packages/core/src/redaction.ts
@@ -17,6 +17,8 @@
* under the License.
*/
+import type { UiCatalog, UiLocale } from './ui-locale.js';
+
const SENSITIVE_KEY_SUFFIXES = new Set([
'auth',
'authorization',
@@ -206,23 +208,62 @@ function isAssignmentSensitiveKey(key: string): boolean {
return suffix !== 'auth' && suffix !== 'authorization';
}
-export function generalizedErrorMessage(error: unknown, fallback = 'Operation failed'): string {
+type GeneralizedErrorCategory = 'timeout' | 'rateLimit' | 'authentication' | 'provider' | 'network';
+
+const GENERALIZED_ERROR_COPY: UiCatalog<Record<GeneralizedErrorCategory, string>> = {
+ 'zh-CN': {
+ timeout: '请求超时',
+ rateLimit: '触发模型速率限制',
+ authentication: '鉴权失败',
+ provider: '模型服务返回错误',
+ network: '网络错误',
+ },
+ 'zh-TW': {
+ timeout: '請求逾時',
+ rateLimit: '已達模型速率限制',
+ authentication: '驗證失敗',
+ provider: '模型服務傳回錯誤',
+ network: '網路錯誤',
+ },
+ en: {
+ timeout: 'Request timed out',
+ rateLimit: 'Rate limit exceeded',
+ authentication: 'Authentication failed',
+ provider: 'Provider returned an error',
+ network: 'Network error',
+ },
+};
+
+function classifyGeneralizedError(error: unknown): GeneralizedErrorCategory | null {
const message = error instanceof Error ? error.message : String(error);
const redacted = redactSecrets(message);
const lower = redacted.toLowerCase();
- if (lower.includes('timeout')) return 'Request timed out';
- if (lower.includes('429') || lower.includes('rate')) return 'Rate limit exceeded';
+ if (lower.includes('timeout')) return 'timeout';
+ if (lower.includes('429') || lower.includes('rate')) return 'rateLimit';
if (lower.includes('401') || lower.includes('403') || isAuthenticationErrorText(lower))
- return 'Authentication failed';
- if (lower.includes('5') && /\b5\d\d\b/.test(lower)) return 'Provider returned an error';
+ return 'authentication';
+ if (lower.includes('5') && /\b5\d\d\b/.test(lower)) return 'provider';
if (
lower.includes('network') ||
lower.includes('fetch') ||
lower.includes('econn') ||
lower.includes('enotfound')
)
- return 'Network error';
- return fallback;
+ return 'network';
+ return null;
+}
+
+function localizedGeneralizedErrorMessage(
+ error: unknown,
+ fallback: string,
+ locale: UiLocale,
+): string {
+ const category = classifyGeneralizedError(error);
+ return category ? GENERALIZED_ERROR_COPY[locale][category] : fallback;
+}
+
+export function generalizedErrorMessage(error: unknown, fallback = 'Operation failed'): string {
+ return localizedGeneralizedErrorMessage(error, fallback, 'en');
}
/**
@@ -239,22 +280,22 @@ export function generalizedErrorMessage(error: unknown, fallback = 'Operation fa
* UX when the classifier can't categorize.
*/
export function generalizedErrorMessageChinese(error: unknown, fallback = '操作失败'): string {
- const message = error instanceof Error ? error.message : String(error);
- const redacted = redactSecrets(message);
- const lower = redacted.toLowerCase();
- if (lower.includes('timeout')) return '请求超时';
- if (lower.includes('429') || lower.includes('rate')) return '触发模型速率限制';
- if (lower.includes('401') || lower.includes('403') || isAuthenticationErrorText(lower))
- return '鉴权失败';
- if (lower.includes('5') && /\b5\d\d\b/.test(lower)) return '模型服务返回错误';
- if (
- lower.includes('network') ||
- lower.includes('fetch') ||
- lower.includes('econn') ||
- lower.includes('enotfound')
- )
- return '网络错误';
- return fallback;
+ return localizedGeneralizedErrorMessage(error, fallback, 'zh-CN');
+}
+
+export function generalizedErrorMessageTraditionalChinese(
+ error: unknown,
+ fallback = '操作失敗',
+): string {
+ return localizedGeneralizedErrorMessage(error, fallback, 'zh-TW');
+}
+
+export function generalizedErrorMessageForLocale(
+ error: unknown,
+ fallback: string,
+ locale: UiLocale,
+): string {
+ return localizedGeneralizedErrorMessage(error, fallback, locale);
}
export function isAuthenticationErrorText(message: string): boolean {
diff --git a/packages/core/src/relative-time.ts b/packages/core/src/relative-time.ts
index 5ce6f475bd..76ab7511c8 100644
--- a/packages/core/src/relative-time.ts
+++ b/packages/core/src/relative-time.ts
@@ -48,7 +48,8 @@ const SIDEBAR_TIME_BUCKETS = [
] as const;
const JUST_NOW: UiCatalog<string> = {
- zh: '刚刚',
+ 'zh-CN': '刚刚',
+ 'zh-TW': '剛剛',
en: 'just now',
};
@@ -93,7 +94,7 @@ function getAbsoluteFormat(uiLocale: UiLocale): Intl.DateTimeFormat {
* reading a relative label falls back to and a tooltip shows; `@maka/ui` had
* its own uncached copy of the same `Intl` options until this became public.
*/
-export function formatAbsoluteTimestamp(ts: number, locale: UiLocale = 'zh'): string {
+export function formatAbsoluteTimestamp(ts: number, locale: UiLocale = 'zh-CN'): string {
return getAbsoluteFormat(locale).format(new Date(ts));
}
@@ -105,7 +106,7 @@ export function formatAbsoluteTimestamp(ts: number, locale: UiLocale = 'zh'): st
export function formatRelativeTimestamp(
ts: number,
now: number = Date.now(),
- locale: UiLocale = 'zh',
+ locale: UiLocale = 'zh-CN',
): string {
const diffMs = relativeAgeMs(ts, now);
if (diffMs < JUST_NOW_MS) {
@@ -158,7 +159,7 @@ function getCompactFormats(uiLocale: UiLocale): {
export function formatCompactTimestamp(
ts: number,
now: number = Date.now(),
- locale: UiLocale = 'zh',
+ locale: UiLocale = 'zh-CN',
): string {
const diffMs = relativeAgeMs(ts, now);
if (diffMs <= RELATIVE_HORIZON_MS) return formatRelativeTimestamp(ts, now, locale);
@@ -178,7 +179,7 @@ export function formatCompactTimestamp(
export function formatSidebarTimestamp(
ts: number,
now: number = Date.now(),
- locale: UiLocale = 'zh',
+ locale: UiLocale = 'zh-CN',
): string {
const diffMs = relativeAgeMs(ts, now);
if (diffMs < JUST_NOW_MS) return JUST_NOW[locale];
diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts
index b6d2daccec..0c02f0c50f 100644
--- a/packages/core/src/settings.ts
+++ b/packages/core/src/settings.ts
@@ -40,6 +40,7 @@ import type { UsageProvenance } from './usage-ledger-merge.js';
import {
UI_LOCALE_PREFERENCES,
isUiLocalePreference,
+ normalizeUiLocalePreference,
type UiLocalePreference,
} from './ui-locale.js';
import { normalizeSubagentSettings, type SubagentSettings } from './subagent-settings.js';
@@ -978,13 +979,12 @@ 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: normalizeUiLocalePreference(base.personalization.uiLocale),
selectedPetId: normalizeSelectedPetId(base.personalization.selectedPetId),
},
botChat: normalizeBotChatSettings(base.botChat, value.botChat),
diff --git a/packages/core/src/tool-quiet-preview.ts b/packages/core/src/tool-quiet-preview.ts
index e2adb0d7b6..f52d03b966 100644
--- a/packages/core/src/tool-quiet-preview.ts
+++ b/packages/core/src/tool-quiet-preview.ts
@@ -52,7 +52,7 @@ interface QuietPreviewStrings {
}
const STRINGS_BY_LOCALE: Record<UiLocale, QuietPreviewStrings> = {
- zh: {
+ 'zh-CN': {
backgroundTerminal: '后台终端交互',
empty: '(空)',
done: '已完成',
@@ -62,6 +62,16 @@ const STRINGS_BY_LOCALE: Record<UiLocale, QuietPreviewStrings> = {
bytes: (n) => `共 ${n} 字节`,
moreQuestions: (total) => (total > 1 ? ` 等 ${total} 问` : ''),
},
+ 'zh-TW': {
+ backgroundTerminal: '後臺終端互動',
+ empty: '(空)',
+ done: '已完成',
+ notDone: '未完成',
+ replacements: (n) => `${n} 處`,
+ written: '已寫入',
+ bytes: (n) => `共 ${n} 位元組`,
+ moreQuestions: (total) => (total > 1 ? ` 等 ${total} 問` : ''),
+ },
en: {
backgroundTerminal: 'Background terminal interaction',
empty: '(empty)',
@@ -75,7 +85,7 @@ const STRINGS_BY_LOCALE: Record<UiLocale, QuietPreviewStrings> = {
};
function strings(locale: UiLocale): QuietPreviewStrings {
- return STRINGS_BY_LOCALE[locale] ?? STRINGS_BY_LOCALE.zh;
+ return STRINGS_BY_LOCALE[locale] ?? STRINGS_BY_LOCALE['zh-CN'];
}
// ── Tool command extraction ──────────────────────────────────────────────
@@ -230,7 +240,7 @@ export interface ToolInvocationInput {
*/
export function formatToolInvocationLine(
item: ToolInvocationInput,
- locale: UiLocale = 'zh',
+ locale: UiLocale = 'zh-CN',
): string | undefined {
const s = strings(locale);
const args = asRecord(item.args);
@@ -518,7 +528,7 @@ export interface QuietPreview {
* Primary list/text fields become the main body; remaining fields (error, ok,
* truncated, …) are appended so diagnostics cannot vanish.
*/
-export function formatQuietJsonValue(value: unknown, locale: UiLocale = 'zh'): QuietPreview {
+export function formatQuietJsonValue(value: unknown, locale: UiLocale = 'zh-CN'): QuietPreview {
const s = strings(locale);
if (value === null || value === undefined) {
return { body: s.empty };
@@ -677,7 +687,7 @@ function formatArrayAsBody(values: unknown[], locale: UiLocale): string {
export function formatAsKeyValueLines(
record: Record<string, unknown>,
depth = 0,
- locale: UiLocale = 'zh',
+ locale: UiLocale = 'zh-CN',
): string {
const s = strings(locale);
if (depth > 3) return redactSecrets(String(record));
diff --git a/packages/core/src/ui-locale.ts b/packages/core/src/ui-locale.ts
index 3a7d161734..069254891c 100644
--- a/packages/core/src/ui-locale.ts
+++ b/packages/core/src/ui-locale.ts
@@ -20,7 +20,7 @@
import { IntlMessageFormat } from 'intl-messageformat';
/** 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];
@@ -122,19 +122,29 @@ function isMessageRecord(value: unknown): value is Readonly<Record<string, unkno
}
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 {
return value === 'auto' || isUiLocale(value);
}
+/** Decode persisted or externally supplied preferences into the closed locale vocabulary. */
+export function normalizeUiLocalePreference(value: unknown): UiLocalePreference {
+ if (value === 'zh') return 'zh-CN';
+ return isUiLocalePreference(value) ? value : 'auto';
+}
+
/** 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';
- if (/^en(?:[-_]|$)/iu.test(normalized)) return 'en';
+ 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';
}
@@ -156,6 +166,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;
}
diff --git a/packages/ui/src/__tests__/conversation-copy.test.ts b/packages/ui/src/__tests__/conversation-copy.test.ts
index f1d9c37e8f..11dc7c6f2c 100644
--- a/packages/ui/src/__tests__/conversation-copy.test.ts
+++ b/packages/ui/src/__tests__/conversation-copy.test.ts
@@ -22,12 +22,13 @@ import test from 'node:test';
import { getConversationCopy } from '../conversation-copy.js';
test('labels the Chinese default thinking level as default', () => {
- assert.equal(getConversationCopy('zh').model.defaultLevel, '默认');
+ assert.equal(getConversationCopy('zh-CN').model.defaultLevel, '默认');
+ assert.equal(getConversationCopy('zh-TW').model.defaultLevel, '預設');
});
test('explains why folder-reference messages cannot be edited and resent', () => {
assert.equal(
- getConversationCopy('zh').messages.editMessageDisabledDirectoryReferences,
+ getConversationCopy('zh-CN').messages.editMessageDisabledDirectoryReferences,
'包含文件夹引用的历史消息暂不支持编辑并重发',
);
assert.equal(
@@ -38,7 +39,7 @@ test('explains why folder-reference messages cannot be edited and resent', () =>
test('context usage explains missing data without exposing provider internals', () => {
assert.equal(
- getConversationCopy('zh').messages.systemNotes.contextUsageUnavailable,
+ getConversationCopy('zh-CN').messages.systemNotes.contextUsageUnavailable,
'暂无用量数据',
);
assert.equal(
@@ -49,7 +50,7 @@ test('context usage explains missing data without exposing provider internals',
test('context usage tooltip leads with the measured share', () => {
assert.equal(
- getConversationCopy('zh').messages.systemNotes.contextUsageShare(12_345, 128_000),
+ getConversationCopy('zh-CN').messages.systemNotes.contextUsageShare(12_345, 128_000),
'已用 12,345 / 128,000 token(10%)',
);
assert.equal(
@@ -60,7 +61,7 @@ test('context usage tooltip leads with the measured share', () => {
test('context usage tooltip keeps measured usage when the limit is unknown', () => {
assert.equal(
- getConversationCopy('zh').messages.systemNotes.contextUsageNoWindow(12_345),
+ getConversationCopy('zh-CN').messages.systemNotes.contextUsageNoWindow(12_345),
'已用 12,345 token;上下文上限未知',
);
assert.equal(
@@ -75,7 +76,8 @@ test('context usage tooltip keeps measured usage when the limit is unknown', ()
* five-digit second count that reads as a frozen hang (#3401).
*/
test('providerRetryScheduled humanizes hour-scale delays in both locales', () => {
- const zh = getConversationCopy('zh').messages.providerRetryScheduled;
+ const zh = getConversationCopy('zh-CN').messages.providerRetryScheduled;
+ const zhTw = getConversationCopy('zh-TW').messages.providerRetryScheduled;
const en = getConversationCopy('en').messages.providerRetryScheduled;
// Sub-second and zero inputs still read as one second (never "0秒后重试").
@@ -92,6 +94,7 @@ test('providerRetryScheduled humanizes hour-scale delays in both locales', () =>
assert.equal(zh(75, 2, 10), '1分 15秒后重试(2/10)');
assert.equal(en(75, 2, 10), 'Retrying in 1m 15s (2/10)');
assert.equal(zh(16_083, 2, 10), '4小时 28分 3秒后重试(2/10)');
+ assert.equal(zhTw(16_083, 2, 10), '4小時 28分 3秒後重試(2/10)');
assert.equal(en(16_083, 2, 10), 'Retrying in 4h 28m 3s (2/10)');
assert.equal(zh(90_061, 2, 10), '1天 1小时 1分 1秒后重试(2/10)');
assert.equal(en(90_061, 2, 10), 'Retrying in 1d 1h 1m 1s (2/10)');
diff --git a/apps/desktop/src/main/__tests__/permission-center-copy.test.ts b/packages/ui/src/__tests__/daily-review-copy.test.ts
similarity index 73%
copy from apps/desktop/src/main/__tests__/permission-center-copy.test.ts
copy to packages/ui/src/__tests__/daily-review-copy.test.ts
index 6333051056..7a7aa41164 100644
--- a/apps/desktop/src/main/__tests__/permission-center-copy.test.ts
+++ b/packages/ui/src/__tests__/daily-review-copy.test.ts
@@ -19,9 +19,8 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
-import { getPermissionCenterCopy } from '../../renderer/locales/permission-center-copy.js';
+import { getDailyReviewCopy } from '../daily-review-copy.js';
-test('presents a granted OS permission as a verified success', () => {
- assert.equal(getPermissionCenterCopy('zh').osStates.granted.tone, 'success');
- assert.equal(getPermissionCenterCopy('en').osStates.granted.tone, 'success');
+test('uses Taiwan wording for the Daily Review append action', () => {
+ assert.equal(getDailyReviewCopy('zh-TW').export.append, '貼到輸入框');
});
diff --git a/packages/ui/src/__tests__/live-turn-projection.test.ts b/packages/ui/src/__tests__/live-turn-projection.test.ts
index 63d788c85f..29fc03fc7b 100644
--- a/packages/ui/src/__tests__/live-turn-projection.test.ts
+++ b/packages/ui/src/__tests__/live-turn-projection.test.ts
@@ -70,7 +70,7 @@ describe('the unconfirmed claim an arm carries', () => {
describe('provider retry copy', () => {
it('describes capacity retries without collapsing them into generic unavailability', () => {
- assert.match(getConversationCopy('zh').messages.providerRetryReason.provider_capacity, /满载/);
+ assert.match(getConversationCopy('zh-CN').messages.providerRetryReason.provider_capacity, /满载/);
assert.match(
getConversationCopy('en').messages.providerRetryReason.provider_capacity,
/capacity/,
diff --git a/packages/ui/src/__tests__/markdown-body.test.ts b/packages/ui/src/__tests__/markdown-body.test.ts
index f824394dc0..62bb6c9d7e 100644
--- a/packages/ui/src/__tests__/markdown-body.test.ts
+++ b/packages/ui/src/__tests__/markdown-body.test.ts
@@ -263,7 +263,7 @@ it('does not force the single-line scrollbar layout on multiline code', () => {
it('gives collapsible plaintext code a localized accessible name', () => {
const code = Array.from({ length: 10 }, (_, index) => `line ${index + 1}`);
- for (const [locale, label] of [['en', 'Code'], ['zh', '代码']] as const) {
+ for (const [locale, label] of [['en', 'Code'], ['zh-CN', '代码']] as const) {
const markup = renderToStaticMarkup(createElement(LocaleProvider, {
locale,
children: createElement(AstryxLocaleProvider, {
diff --git a/packages/ui/src/__tests__/materialize.test.ts b/packages/ui/src/__tests__/materialize.test.ts
index 179be9ea18..a7a2a76a3a 100644
--- a/packages/ui/src/__tests__/materialize.test.ts
+++ b/packages/ui/src/__tests__/materialize.test.ts
@@ -209,11 +209,11 @@ describe("materializeChat message metadata", () => {
"Context compacted to keep this session within the model window.",
);
assert.equal(
- materializeChat(messages, "zh")[0]?.text,
+ materializeChat(messages, "zh-CN")[0]?.text,
"已压缩较早的对话内容,以适应模型上下文窗口。",
);
assert.equal(
- materializeTurns(messages, "zh")[0]?.notes[0]?.text,
+ materializeTurns(messages, "zh-CN")[0]?.notes[0]?.text,
"已压缩较早的对话内容,以适应模型上下文窗口。",
);
});
diff --git a/packages/ui/src/__tests__/runtime-resume-copy.test.ts b/packages/ui/src/__tests__/runtime-resume-copy.test.ts
index 6fa9eb9909..f1618df9de 100644
--- a/packages/ui/src/__tests__/runtime-resume-copy.test.ts
+++ b/packages/ui/src/__tests__/runtime-resume-copy.test.ts
@@ -48,8 +48,8 @@ test('resolves English park copy for every known reason with no Chinese (#4489)'
}
});
-test('keeps the Chinese copy for zh', () => {
- const copy = resumeParkToastCopy(['pending_permission'], 'zh');
+test('keeps the Chinese copy for zh-CN', () => {
+ const copy = resumeParkToastCopy(['pending_permission'], 'zh-CN');
assert.equal(copy.title, '暂时无法继续这一轮');
assert.equal(copy.description, '上次执行仍在等待权限确认。');
@@ -60,7 +60,7 @@ test('resolves the missing-candidate special case per locale', () => {
title: 'Nothing to resume',
description: 'This task is already up to date.',
});
- assert.deepEqual(resumeParkToastCopy(['resume_candidate_missing'], 'zh'), {
+ assert.deepEqual(resumeParkToastCopy(['resume_candidate_missing'], 'zh-CN'), {
title: '没有可恢复的任务',
description: '任务已是最新状态。',
});
@@ -71,6 +71,6 @@ test('falls back to the generic description for unknown reasons and dedupes repe
assert.equal(unknown.title, 'This round cannot be resumed yet');
assert.equal(unknown.description, 'This task does not currently meet the conditions to continue.');
- const deduped = resumeParkToastCopy(['pending_permission', 'pending_permission'], 'zh');
+ const deduped = resumeParkToastCopy(['pending_permission', 'pending_permission'], 'zh-CN');
assert.equal(deduped.description, '上次执行仍在等待权限确认。');
});
diff --git a/packages/ui/src/__tests__/session-hover-card-copy.test.ts b/packages/ui/src/__tests__/session-hover-card-copy.test.ts
index 948a7dfeb4..89391207da 100644
--- a/packages/ui/src/__tests__/session-hover-card-copy.test.ts
+++ b/packages/ui/src/__tests__/session-hover-card-copy.test.ts
@@ -22,7 +22,7 @@ import test from 'node:test';
import { getSessionHoverCardCopy } from '../session-hover-card-copy.js';
test('localizes task and project hover card summaries', () => {
- const zh = getSessionHoverCardCopy('zh');
+ const zh = getSessionHoverCardCopy('zh-CN');
const en = getSessionHoverCardCopy('en');
assert.equal(zh.sessionDetailsLabel('发布说明'), '发布说明 任务详情');
diff --git a/packages/ui/src/__tests__/tool-activity-presentation.test.ts b/packages/ui/src/__tests__/tool-activity-presentation.test.ts
index 97f234d4d8..e0f2877b90 100644
--- a/packages/ui/src/__tests__/tool-activity-presentation.test.ts
+++ b/packages/ui/src/__tests__/tool-activity-presentation.test.ts
@@ -32,7 +32,7 @@ import {
isComputerTool,
} from '../tool-activity/computer-action-label.js';
-function renderToStaticMarkup(node: ReactNode, locale: 'zh' | 'en' = 'zh'): string {
+function renderToStaticMarkup(node: ReactNode, locale: 'zh-CN' | 'en' = 'zh-CN'): string {
return renderReactToStaticMarkup(createElement(LocaleProvider, {
locale,
children: node,
@@ -126,7 +126,7 @@ describe('tool activity presentation', () => {
};
assert.equal(isComputerTool(item), true);
- assert.equal(computerActionLabel(item, 'zh'), '观察「计算器」窗口');
+ assert.equal(computerActionLabel(item, 'zh-CN'), '观察「计算器」窗口');
const markup = renderToStaticMarkup(
createElement(ToolTrow, { items: [item] }),
);
@@ -166,7 +166,7 @@ describe('tool activity presentation', () => {
assert.match(markup, /「计算器」窗口/);
assert.match(markup, />7\/11</);
assert.equal(
- computerRunningLabel([observed, sequence], 'zh'),
+ computerRunningLabel([observed, sequence], 'zh-CN'),
'正在操作「计算器」窗口 · 连续操作第 7/11 步',
);
});
diff --git a/apps/desktop/src/main/__tests__/health-center-copy.test.ts b/packages/ui/src/__tests__/tool-format.test.ts
similarity index 65%
copy from apps/desktop/src/main/__tests__/health-center-copy.test.ts
copy to packages/ui/src/__tests__/tool-format.test.ts
index e8f7a6aa5f..5bfd6d93b4 100644
--- a/apps/desktop/src/main/__tests__/health-center-copy.test.ts
+++ b/packages/ui/src/__tests__/tool-format.test.ts
@@ -19,15 +19,17 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
-import { getHealthCenterCopy } from '../../renderer/locales/settings-health-copy.js';
+import { describeLoadToolResult } from '../tool-format.js';
-test('labels blocker counts as global across filtered health views', () => {
- assert.equal(
- getHealthCenterCopy('zh').blockers.send(1, 6),
- '全部健康信号中,1/6 条会阻塞发送',
- );
- assert.equal(
- getHealthCenterCopy('en').blockers.send(1, 6),
- 'Across all health signals, 1 of 6 blocks sending',
+test('custom load-tool groups use Traditional Chinese action copy', () => {
+ const result = describeLoadToolResult(
+ { group: 'custom' },
+ {
+ activated: ['custom_tool'],
+ group: { id: 'custom', label: '自訂工具', description: '專案工具' },
+ },
+ 'zh-TW',
);
+ assert.equal(result?.actionLabel, '啟用 自訂工具');
+ assert.equal(result?.title, '自訂工具 已啟用');
});
diff --git a/packages/ui/src/__tests__/transcript-projection.test.ts b/packages/ui/src/__tests__/transcript-projection.test.ts
index 559ce849fe..86715a0963 100644
--- a/packages/ui/src/__tests__/transcript-projection.test.ts
+++ b/packages/ui/src/__tests__/transcript-projection.test.ts
@@ -86,7 +86,7 @@ describe('incremental transcript projection', () => {
const chinese = projection.project({
sessionId: SESSION,
messages,
- locale: 'zh',
+ locale: 'zh-CN',
});
assert.equal(
diff --git a/packages/ui/src/__tests__/turn-running-spinner.test.tsx b/packages/ui/src/__tests__/turn-running-spinner.test.tsx
index 9f8f4d185d..1c30024a0c 100644
--- a/packages/ui/src/__tests__/turn-running-spinner.test.tsx
+++ b/packages/ui/src/__tests__/turn-running-spinner.test.tsx
@@ -50,7 +50,7 @@ function statusHasSpinner(toolStatuses: readonly ('running' | 'completed')[]): b
return document.querySelector('.maka-turn-processing .astryx-spinner') !== null;
}
-function runningStatusText(locale: 'en' | 'zh'): string {
+function runningStatusText(locale: 'en' | 'zh-CN'): string {
const turn: TurnViewModel = {
turnId: 'turn-1',
status: 'running',
@@ -78,6 +78,6 @@ test('keeps the turn spinner when a collapsed group hides the running tool', ()
});
test('describes provider silence without inventing semantic progress', () => {
- assert.equal(runningStatusText('zh'), '等待模型输出…');
+ assert.equal(runningStatusText('zh-CN'), '等待模型输出…');
assert.equal(runningStatusText('en'), 'Waiting for model output…');
});
diff --git a/packages/ui/src/artifact-preview-registry.ts b/packages/ui/src/artifact-preview-registry.ts
index def9198076..cb724e2bd6 100644
--- a/packages/ui/src/artifact-preview-registry.ts
+++ b/packages/ui/src/artifact-preview-registry.ts
@@ -81,7 +81,7 @@ function exceedsImagePayloadCap(base64: string): boolean {
return base64.length > IMAGE_PAYLOAD_MAX_BASE64_LENGTH;
}
-export function formatPreviewSize(sizeBytes: number | undefined, locale: UiLocale = 'zh'): string {
+export function formatPreviewSize(sizeBytes: number | undefined, locale: UiLocale = 'zh-CN'): string {
if (sizeBytes === undefined || sizeBytes < 0 || !Number.isFinite(sizeBytes)) return getSharedUiCopy(locale).artifact.unknownSize;
if (sizeBytes < 1024) return `${sizeBytes} B`;
if (sizeBytes < 1024 * 1024) return `${(sizeBytes / 1024).toFixed(1)} KB`;
... 1138 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org