Skip to content

Commit adf4f4a

Browse files
fix(desktop): localize expected errors
Raw exception text no longer reaches the UI: expected failures carry machine-readable codes (with params where needed) that the renderer maps through locale catalogs, and unexpected failures show a localized fallback while redacted diagnostics go to the console.
1 parent e2e597c commit adf4f4a

36 files changed

Lines changed: 765 additions & 309 deletions

apps/desktop/renderer-architecture.json

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"src/renderer/desktop-slash-command.ts",
5757
"src/renderer/desktop-transcript-range-store.ts",
5858
"src/renderer/error-boundary.tsx",
59+
"src/renderer/expected-operation-error.ts",
5960
"src/renderer/follow-up-submit-routing.ts",
6061
"src/renderer/interrupted-resume.ts",
6162
"src/renderer/keyboard-help.tsx",
@@ -88,6 +89,7 @@
8889
"src/renderer/locales/settings-web-search-copy.ts",
8990
"src/renderer/locales/shell-copy.ts",
9091
"src/renderer/locales/shell-remaining-copy.ts",
92+
"src/renderer/locales/workhub-copy.ts",
9193
"src/renderer/main.tsx",
9294
"src/renderer/mcp-brand-contrast.ts",
9395
"src/renderer/mcp-brand-marks.tsx",
@@ -276,6 +278,7 @@
276278
"src/renderer/features/module-hub/controller/use-scheduled-tasks-controller.ts -> src/renderer/locales/shell-remaining-copy",
277279
"src/renderer/features/module-hub/controller/use-skills-controller.ts -> src/renderer/locales/shell-copy",
278280
"src/renderer/features/module-hub/ui/module-hub-host.tsx -> src/renderer/mcp-page",
281+
"src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx -> src/renderer/expected-operation-error",
279282
"src/renderer/features/session-navigation/controller/session-row-actions.ts -> src/renderer/locales/shell-copy",
280283
"src/renderer/features/session-navigation/controller/use-session-navigation-controller.ts -> src/renderer/use-external-store-selector",
281284
"src/renderer/features/session-navigation/controller/use-session-navigation-reads.ts -> src/renderer/use-external-store-selector",
@@ -473,14 +476,14 @@
473476
"unresolvedDependencies": 0,
474477
"actionFactories": [],
475478
"dependencyPaths": {
479+
"./expected-operation-error.js": 1,
476480
"./locales/shell-copy.js": 1,
477481
"@maka/core/llm-connections": 1,
478-
"@maka/core/redaction": 1,
479482
"@maka/core/text-file-import": 1,
480483
"@maka/core/ui-locale": 1
481484
},
482485
"importSpecifiers": 6,
483-
"nonTriviaTokens": 515
486+
"nonTriviaTokens": 372
484487
},
485488
"src/renderer/app-shell-detail-panel.tsx": {
486489
"importDeclarations": 1,
@@ -1661,6 +1664,17 @@
16611664
"react": 1
16621665
}
16631666
},
1667+
"src/renderer/expected-operation-error.ts": {
1668+
"bridgePaths": {},
1669+
"environmentCapabilities": {},
1670+
"hookCalls": {},
1671+
"lifecycleMethods": {},
1672+
"unresolvedDependencies": 0,
1673+
"actionFactories": [],
1674+
"dependencyPaths": {
1675+
"@maka/core/redaction": 1
1676+
}
1677+
},
16641678
"src/renderer/follow-up-submit-routing.ts": {
16651679
"bridgePaths": {},
16661680
"environmentCapabilities": {},
@@ -1788,6 +1802,7 @@
17881802
"unresolvedDependencies": 0,
17891803
"actionFactories": [],
17901804
"dependencyPaths": {
1805+
"../../shared/work-board-ipc.js": 1,
17911806
"@maka/core/connection-readiness": 1,
17921807
"@maka/core/model-call-attempt": 1,
17931808
"@maka/core/session-send-projection": 1,
@@ -2046,10 +2061,10 @@
20462061
"unresolvedDependencies": 0,
20472062
"actionFactories": [],
20482063
"dependencyPaths": {
2064+
"../expected-operation-error.js": 1,
20492065
"@maka/core/goal": 1,
20502066
"@maka/core/model-thinking": 1,
20512067
"@maka/core/permission": 1,
2052-
"@maka/core/redaction": 1,
20532068
"@maka/core/settings": 1,
20542069
"@maka/core/slash-command-catalog": 1,
20552070
"@maka/core/ui-locale": 1
@@ -2066,6 +2081,17 @@
20662081
"@maka/core/ui-locale": 1
20672082
}
20682083
},
2084+
"src/renderer/locales/workhub-copy.ts": {
2085+
"bridgePaths": {},
2086+
"environmentCapabilities": {},
2087+
"hookCalls": {},
2088+
"lifecycleMethods": {},
2089+
"unresolvedDependencies": 0,
2090+
"actionFactories": [],
2091+
"dependencyPaths": {
2092+
"@maka/core/ui-locale": 1
2093+
}
2094+
},
20692095
"src/renderer/mcp-brand-contrast.ts": {
20702096
"bridgePaths": {},
20712097
"environmentCapabilities": {},
@@ -2359,6 +2385,7 @@
23592385
"unresolvedDependencies": 0,
23602386
"actionFactories": [],
23612387
"dependencyPaths": {
2388+
"./expected-operation-error.js": 1,
23622389
"./locales/plan-mode-copy.js": 1,
23632390
"@astryxdesign/core/Banner": 1,
23642391
"@astryxdesign/core/Collapsible": 1,
@@ -2399,6 +2426,7 @@
23992426
"actionFactories": [],
24002427
"dependencyPaths": {
24012428
"../preload/bridge-contract.js": 1,
2429+
"./expected-operation-error.js": 1,
24022430
"./locales/shell-copy.js": 1,
24032431
"@astryxdesign/core/Button": 1,
24042432
"@astryxdesign/core/Dialog": 1,
@@ -2467,6 +2495,7 @@
24672495
"unresolvedDependencies": 0,
24682496
"actionFactories": [],
24692497
"dependencyPaths": {
2498+
"./expected-operation-error.js": 1,
24702499
"./locales/session-collaboration-copy.js": 1,
24712500
"./session-turn-request-composer.js": 1,
24722501
"@astryxdesign/core": 1,
@@ -2604,6 +2633,7 @@
26042633
"unresolvedDependencies": 0,
26052634
"actionFactories": [],
26062635
"dependencyPaths": {
2636+
"./expected-operation-error.js": 1,
26072637
"./locales/session-collaboration-copy.js": 1,
26082638
"@maka/runtime-host/protocol": 1,
26092639
"@maka/ui": 1,
@@ -5213,6 +5243,7 @@
52135243
"actionFactories": [],
52145244
"dependencyPaths": {
52155245
"../shared/work-board-ipc.js": 1,
5246+
"./expected-operation-error.js": 1,
52165247
"@maka/core/work-board": 1,
52175248
"@maka/storage/work-board-store": 1,
52185249
"react": 1
@@ -5232,6 +5263,7 @@
52325263
"unresolvedDependencies": 0,
52335264
"actionFactories": [],
52345265
"dependencyPaths": {
5266+
"./expected-operation-error.js": 1,
52355267
"./locales/conversation-copy.js": 1,
52365268
"./use-work-board.js": 1,
52375269
"@astryxdesign/core": 1,
@@ -5251,6 +5283,7 @@
52515283
"unresolvedDependencies": 0,
52525284
"actionFactories": [],
52535285
"dependencyPaths": {
5286+
"./expected-operation-error.js": 1,
52545287
"./workhub-route-policy.js": 1,
52555288
"@maka/runtime-host/protocol": 1
52565289
}
@@ -5349,6 +5382,8 @@
53495382
"unresolvedDependencies": 0,
53505383
"actionFactories": [],
53515384
"dependencyPaths": {
5385+
"./expected-operation-error.js": 1,
5386+
"./locales/workhub-copy.js": 2,
53525387
"./workhub-controller.js": 1,
53535388
"./workhub-coordination-port.js": 1,
53545389
"./workhub-send-lease.js": 1,
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import assert from 'node:assert/strict';
21+
import test from 'node:test';
22+
import { getDesktopConversationCopy } from '../../renderer/locales/conversation-copy.js';
23+
import { getSessionCollaborationCopy } from '../../renderer/locales/session-collaboration-copy.js';
24+
import { ExpectedOperationError } from '../../renderer/expected-operation-error.js';
25+
import { sessionCollaborationImportErrorMessage } from '../../renderer/features/session-collaboration/testing.js';
26+
import { messageReadErrorMessage } from '../../renderer/app-shell-copy.js';
27+
import { localizedShellErrorMessage } from '../../renderer/locales/shell-copy.js';
28+
29+
test('routes Work Board codes through each locale catalog', () => {
30+
for (const locale of ['zh', 'en'] as const) {
31+
const copy = getDesktopConversationCopy(locale).workBoardPanel;
32+
const error: unknown = new ExpectedOperationError('not_found');
33+
assert.equal(
34+
error instanceof ExpectedOperationError && Object.hasOwn(copy.errors, error.code)
35+
? copy.errors[error.code as keyof typeof copy.errors]
36+
: copy.actionFailed,
37+
copy.errors.not_found,
38+
);
39+
}
40+
});
41+
42+
test('maps blocked session-control tokens per locale at the shared entry', () => {
43+
const blocked = new Error('session_control_blocked:permission_turn_running');
44+
assert.equal(
45+
localizedShellErrorMessage(blocked, 'fallback', 'zh'),
46+
'当前任务正在运行,等结束后再切换权限模式。',
47+
);
48+
assert.equal(
49+
localizedShellErrorMessage(blocked, 'fallback', 'en'),
50+
'A task is still running. Change the permission mode after it finishes.',
51+
);
52+
});
53+
54+
test('maps attachment-ingest tokens per locale at the shared entry', () => {
55+
const blocked = new Error("Error invoking remote method 'attachments': Error: attachment_ingest:count_limit");
56+
assert.equal(localizedShellErrorMessage(blocked, 'fallback', 'zh'), '一次最多添加 8 个附件。');
57+
assert.equal(
58+
localizedShellErrorMessage(blocked, 'fallback', 'en'),
59+
'At most 8 attachments per message.',
60+
);
61+
});
62+
63+
test('routes structured collaboration failures through each locale catalog', () => {
64+
const cases = [
65+
[{ kind: 'error', reason: 'invalid_code' } as const, 'invalidCode'],
66+
[{ kind: 'error', reason: 'peer_path_unavailable' } as const, 'directPathUnavailable'],
67+
[{ kind: 'error', reason: 'connection_failed' } as const, 'connectionFailed'],
68+
] as const;
69+
for (const locale of ['zh', 'en'] as const) {
70+
const copy = getSessionCollaborationCopy(locale);
71+
for (const [result, key] of cases) {
72+
assert.equal(sessionCollaborationImportErrorMessage(copy, result), copy[key]);
73+
}
74+
assert.equal(
75+
sessionCollaborationImportErrorMessage(copy, {
76+
kind: 'error',
77+
reason: 'mount_limit_reached',
78+
params: { max: 12 },
79+
}),
80+
copy.mountLimit(12),
81+
);
82+
}
83+
});
84+
85+
test('uses localized fallbacks instead of classifying raw exception text', (context) => {
86+
context.mock.method(console, 'error', () => undefined);
87+
const raw = new Error('timeout 401 网络失败 MAKA_SESSION_READ_MESSAGES_ERROR: 后端中文');
88+
assert.equal(
89+
messageReadErrorMessage(raw, 'en'),
90+
'Task content is temporarily unavailable. Try again later.',
91+
);
92+
assert.equal(messageReadErrorMessage(raw, 'zh'), '任务内容暂时无法读取,请稍后重试。');
93+
assert.equal(localizedShellErrorMessage(raw, 'English fallback', 'en'), 'English fallback');
94+
assert.equal(localizedShellErrorMessage(raw, '中文兜底', 'zh'), '中文兜底');
95+
});

apps/desktop/src/main/__tests__/plan-mode-copy.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ test('localizes Plan Mode chrome and abandon confirmation without rewriting plan
2929
assert.equal(en.proposal.statuses.approved, 'Approved');
3030
assert.equal(zh.execution.stepCount(2, 3), '2/3 步');
3131
assert.equal(en.execution.stepCount(1, 1), '1/1 step');
32+
assert.equal(zh.operationFailed, '计划操作失败,请稍后重试。');
33+
assert.equal(en.operationFailed, 'The plan action failed. Try again later.');
3234
assert.deepEqual(
3335
{
3436
title: en.abandonConfirmation.title,

apps/desktop/src/main/__tests__/runtime-host-guest-session-mounts.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,30 @@ test('removes failed activation desire instead of creating recoverable profile s
6969
});
7070

7171
const result = await mounts.importInvitation(invitation('guest-two'), false, 'import-two');
72-
assert.deepEqual(result.kind === 'error' ? result.reason : result.kind, 'peer_path_unavailable');
72+
assert.deepEqual(result, { kind: 'error', reason: 'peer_path_unavailable' });
7373
assert.deepEqual(await store.read(), []);
7474
assert.equal(unmounted.length, 1);
7575
});
7676

77+
test('logs unexpected activation failures without returning Host details', async (context) => {
78+
const diagnostics: string[] = [];
79+
context.mock.method(console, 'error', (...values: unknown[]) => {
80+
diagnostics.push(values.map(String).join(' '));
81+
});
82+
const mounts = service(memoryStore(), {
83+
mount: async () => {
84+
throw new Error('Authorization: Bearer very-secret-token');
85+
},
86+
});
87+
88+
const result = await mounts.importInvitation(invitation('guest-failed'), false, 'import-failed');
89+
90+
assert.deepEqual(result, { kind: 'error', reason: 'connection_failed' });
91+
assert.equal(diagnostics.length, 1);
92+
assert.match(diagnostics[0]!, /session-collaboration.*import failed/u);
93+
assert.doesNotMatch(diagnostics[0]!, /very-secret-token/u);
94+
});
95+
7796
test('settles admitted finalization before committing unmount desire', async () => {
7897
const store = memoryStore();
7998
let started!: () => void;

apps/desktop/src/main/__tests__/work-board-ipc-main.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,16 +164,18 @@ describe('Work Board IPC', () => {
164164
{ title: 'stale write' },
165165
{ expectedRevision: 1 },
166166
);
167-
assert.equal(staleRename.ok, false);
168-
if (!staleRename.ok) assert.equal(staleRename.code, 'operation_conflict');
167+
assert.deepEqual(staleRename, {
168+
ok: false,
169+
error: { code: 'operation_conflict' },
170+
});
169171

170172
const removedBeforeArchive = await ipc.invoke<WorkBoardIpcResult<null>>(
171173
'workBoard:remove',
172174
id,
173175
);
174176
assert.equal(removedBeforeArchive.ok, false);
175177
if (!removedBeforeArchive.ok) {
176-
assert.equal(removedBeforeArchive.code, 'must_archive_first');
178+
assert.equal(removedBeforeArchive.error.code, 'must_archive_first');
177179
}
178180

179181
const archived = await ipc.invoke<
@@ -194,14 +196,14 @@ describe('Work Board IPC', () => {
194196
{ titel: 'x' },
195197
);
196198
assert.equal(invalidPatch.ok, false);
197-
if (!invalidPatch.ok) assert.equal(invalidPatch.code, 'invalid_input');
199+
if (!invalidPatch.ok) assert.equal(invalidPatch.error.code, 'invalid_input');
198200

199201
const invalidCreate = await ipc.invoke<WorkBoardIpcResult<unknown>>(
200202
'workBoard:create',
201203
{ ...itemInput(), notes: null },
202204
);
203205
assert.equal(invalidCreate.ok, false);
204-
if (!invalidCreate.ok) assert.equal(invalidCreate.code, 'invalid_input');
206+
if (!invalidCreate.ok) assert.equal(invalidCreate.error.code, 'invalid_input');
205207

206208
await ipc.invoke('workBoard:archive', id);
207209
const removed = await ipc.invoke<WorkBoardIpcResult<null>>('workBoard:remove', id);

0 commit comments

Comments
 (0)