Skip to content

Commit 89c58cd

Browse files
fix(i18n): emit settings expected-result codes from producers
Settings-area producers (memory, data, permission center, connection test, computer-use health, dev dialogs) emitted zh prose that reached en users verbatim, and presenters sniffed CJK to decide whether to show it. Producers now return stable codes; each settings catalog maps its codes per locale with an explicit unknown fallback. Generated-by: Claude Code
1 parent ce10a0e commit 89c58cd

35 files changed

Lines changed: 497 additions & 179 deletions

apps/desktop/src/main/__tests__/computer-use-host.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,27 @@ describe('Computer Use host health', () => {
3636
it('does not report a binary-only executor as healthy before first use', () => {
3737
assert.deepEqual(computerUseServiceHealth('maka-cu', snapshot('idle')), {
3838
state: 'not_run',
39-
reason: 'maka-cu 已可用,将在首次调用时启动。',
39+
reason: 'cu_executor_lazy_start',
4040
});
4141
});
4242

4343
it('reports ready, recovery, and unavailable states', () => {
4444
assert.equal(computerUseServiceHealth('maka-cu', snapshot('ready')).state, 'healthy');
4545
assert.equal(
4646
computerUseServiceHealth('maka-cu', snapshot('backing_off')).reason,
47-
'maka-cu executor 正在启动或恢复。',
47+
'cu_executor_recovering',
4848
);
4949
assert.equal(
5050
computerUseServiceHealth('maka-cu', snapshot('starting')).state,
5151
'degraded',
5252
);
5353
assert.deepEqual(computerUseServiceHealth('maka-cu', snapshot('unavailable')), {
5454
state: 'not_available',
55-
reason: 'maka-cu executor 启动失败或已退出。',
55+
reason: 'cu_executor_start_failed',
5656
});
5757
assert.deepEqual(computerUseServiceHealth('maka-cu', snapshot('disposed')), {
5858
state: 'not_available',
59-
reason: 'maka-cu executor 已停止。',
59+
reason: 'cu_executor_stopped',
6060
});
6161
});
6262

apps/desktop/src/main/__tests__/github-copilot-local-credential.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ describe('importGitHubCopilotLocalCredential', () => {
7171
assert.equal(imported.result.ok, false);
7272
if (!imported.result.ok) {
7373
assert.equal(imported.result.reason, 'token_exchange_failed');
74-
assert.match(imported.result.message, / classic PAT/);
74+
assert.equal(imported.result.code, 'copilot_classic_pat_unsupported');
7575
assert.equal(imported.result.message.includes('ghp_classic_pat'), false);
7676
}
7777
assert.equal(imported.secret, undefined);
@@ -83,7 +83,7 @@ describe('importGitHubCopilotLocalCredential', () => {
8383
});
8484

8585
assert.equal(imported.result.ok, false);
86-
if (!imported.result.ok) assert.match(imported.result.message, //);
86+
if (!imported.result.ok) assert.equal(imported.result.code, 'copilot_credential_type_unsupported');
8787
assert.equal(imported.secret, undefined);
8888
});
8989

@@ -95,7 +95,7 @@ describe('importGitHubCopilotLocalCredential', () => {
9595
});
9696

9797
assert.equal(imported.result.ok, false);
98-
if (!imported.result.ok) assert.match(imported.result.message, //);
98+
if (!imported.result.ok) assert.equal(imported.result.code, 'copilot_local_credential_missing');
9999
assert.equal(imported.secret, undefined);
100100
});
101101
});

apps/desktop/src/main/__tests__/permission-center-copy.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,29 @@ test('presents a granted OS permission as a verified success', () => {
2525
assert.equal(getPermissionCenterCopy('zh').osStates.granted.tone, 'success');
2626
assert.equal(getPermissionCenterCopy('en').osStates.granted.tone, 'success');
2727
});
28+
29+
test('renders capability reason codes per locale', () => {
30+
const zh = getPermissionCenterCopy('zh');
31+
const en = getPermissionCenterCopy('en');
32+
assert.equal(zh.reasons['missing platform credentials'], '未配置平台凭据');
33+
assert.equal(en.reasons['missing platform credentials'], 'Platform credentials are not configured');
34+
assert.equal(zh.reasons.cu_executor_recovering, 'maka-cu executor 正在启动或恢复。');
35+
assert.equal(en.reasons.cu_executor_recovering, 'The maka-cu executor is starting or recovering.');
36+
});
37+
38+
test('composes the computer-use backend status from snapshot facts per locale', () => {
39+
const zh = getPermissionCenterCopy('zh');
40+
const en = getPermissionCenterCopy('en');
41+
assert.equal(
42+
zh.cuBackendStatus(['辅助功能', '屏幕录制'], 'healthy'),
43+
'maka-cu artifact 已通过本地完整性检查。等待辅助功能、屏幕录制权限。操作与截图 service 已就绪;按目标与动作类别授权后可操作本机应用。',
44+
);
45+
assert.equal(
46+
zh.cuBackendStatus([], 'not_run'),
47+
'maka-cu artifact 已通过本地完整性检查。service 将在首次调用时启动;按目标与动作类别授权后可操作本机应用。',
48+
);
49+
assert.equal(
50+
en.cuBackendStatus(['Accessibility'], 'degraded'),
51+
'The maka-cu artifact passed the local integrity check. Waiting for Accessibility permission. The maka-cu service is starting or recovering.',
52+
);
53+
});

apps/desktop/src/main/__tests__/runtime-host-artifacts-ipc-main.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ function attachmentReadHandler(
5252
): Handler {
5353
const handlers = new Map<string, Handler>();
5454
registerRuntimeHostArtifactsIpc({
55+
uiLocale: () => 'zh' as const,
5556
ipcMain: {
5657
handle: (channel, handler) => handlers.set(channel, handler as Handler),
5758
},
@@ -121,6 +122,7 @@ test("Runtime Host Artifact IPC preserves previews and streams complete exports"
121122

122123
try {
123124
registerRuntimeHostArtifactsIpc({
125+
uiLocale: () => 'zh' as const,
124126
ipcMain: {
125127
handle: (channel, handler) => handlers.set(channel, handler as Handler),
126128
},

apps/desktop/src/main/__tests__/runtime-host-memory-ipc-main.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ test('does not project or open remote Runtime Host file paths', async () => {
175175
assert.deepEqual(projected.backups.map(({ path }) => path), ['']);
176176
assert.deepEqual(opened, {
177177
ok: false,
178+
code: 'remote_host_owned',
178179
message: 'Memory files are owned by the remote Runtime Host',
179180
});
180181
});

apps/desktop/src/main/__tests__/settings-test-result-copy.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,13 @@ test("missing proxy credentials have actionable bilingual copy", () => {
3737
"Proxy authentication is enabled. Enter a proxy password before testing.",
3838
);
3939
});
40+
41+
test("renders the disabled-direct proxy code per locale", () => {
42+
const result = { ok: true, code: "proxy_disabled_direct", message: "direct" } as never;
43+
assert.equal(settingsTestResultMessage(result, "zh"), "代理未启用,当前会直接连接。");
44+
assert.equal(
45+
settingsTestResultMessage(result, "en"),
46+
"The proxy is disabled; connections go direct.",
47+
);
48+
});
49+

apps/desktop/src/main/browser-message-box.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* under the License.
1818
*/
1919

20+
import type { UiCatalog } from '@maka/core/ui-locale';
2021
import { randomUUID } from 'node:crypto';
2122
import { readFileSync } from 'node:fs';
2223
import { join } from 'node:path';
@@ -348,7 +349,7 @@ export function buildBrowserMessageBoxHtml(
348349

349350
function renderBrowserMessageBoxHtml(input: BrowserMessageBoxPresentation): string {
350351
const nonce = randomUUID().replaceAll('-', '');
351-
const closeLabel = input.locale === 'zh' ? '关闭' : 'Close';
352+
const closeLabel = CLOSE_LABEL[input.locale];
352353
const closeButton = `<button class="window-close" type="button" data-response="${input.cancelId}" aria-label="${closeLabel}">
353354
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>
354355
</button>`;
@@ -638,3 +639,5 @@ function escapeHtml(value: string): string {
638639
return entities[character] ?? character;
639640
});
640641
}
642+
643+
const CLOSE_LABEL = { zh: '关闭', en: 'Close' } satisfies UiCatalog<string>;

apps/desktop/src/main/capability-snapshot.ts

Lines changed: 33 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
type CapabilityFeatureSignal,
2828
type CapabilityMemoryAcceptanceSignal,
2929
type CapabilityPermissionRequirement,
30+
type CapabilityReasonCode,
3031
type CapabilityRuntimeProbeSignal,
3132
type CapabilitySnapshot,
3233
type CapabilitySnapshotCollection,
@@ -80,7 +81,7 @@ export function buildCapabilitySnapshotCollection(input: {
8081
feature: {
8182
state: 'partial',
8283
source: 'runtime',
83-
reason: 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
84+
reason: 'activity_recorder_partial',
8485
},
8586
requiredPermissions: [
8687
{ id: 'screen_recording', required: false, status: permissions.screen_recording.status },
@@ -90,7 +91,7 @@ export function buildCapabilitySnapshotCollection(input: {
9091
runtimeProbe: {
9192
state: 'not_run',
9293
source: 'runtime_probe',
93-
reason: '打开 Daily Review 可查看本地活动聚合结果',
94+
reason: 'activity_recorder_probe_hint',
9495
},
9596
}),
9697
staticCapability({
@@ -100,15 +101,15 @@ export function buildCapabilitySnapshotCollection(input: {
100101
feature: {
101102
state: 'partial',
102103
source: 'runtime',
103-
reason: '本地 MEMORY.md 已可见;自动抽取/写入仍需用户确认',
104+
reason: 'memory_partial',
104105
},
105106
requiredPermissions: [],
106107
actionApproval: { state: 'not_required', source: 'not_applicable' },
107108
memoryAcceptance: { state: 'draft_required', source: 'memory_contract' },
108109
runtimeProbe: {
109110
state: 'not_run',
110111
source: 'runtime_probe',
111-
reason: '透明本地记忆为文件读写能力,不做后台探测',
112+
reason: 'memory_no_probe',
112113
},
113114
}),
114115
...BOT_PROVIDERS.map((provider) =>
@@ -138,7 +139,7 @@ function computerUseCapability(
138139
feature: {
139140
state: artifactAvailable ? 'enabled' : 'not_available',
140141
source: 'runtime',
141-
reason: computerUseCapabilityReason(input, permissions),
142+
reason: computerUseCapabilityReason(input),
142143
},
143144
requiredPermissions: [
144145
{ id: 'accessibility', required: true, status: permissions.accessibility.status },
@@ -153,45 +154,24 @@ function computerUseCapability(
153154
state: input?.health.state ?? 'not_available',
154155
source: 'runtime_probe',
155156
lastCheckedAt: now,
156-
reason: input?.health.reason ?? 'Computer Use 后端当前不可用。',
157+
reason: input?.health.reason ?? 'cu_backend_unavailable',
157158
},
158159
});
159160
}
160161

162+
// The presenter composes the full 'cu_backend_status' sentence from data the
163+
// same snapshot already carries (required-permission statuses and the runtime
164+
// probe state), so the reason stays a bare code.
161165
function computerUseCapabilityReason(
162166
input: {
163167
backendId: CuBackendId | 'none';
164168
health: ReturnType<typeof computerUseServiceHealth>;
165169
} | undefined,
166-
permissions: PermissionSnapshot['permissions'],
167170
): string {
168171
if (input === undefined || input.backendId === 'none') {
169-
return '未找到通过完整性检查的 Computer Use 执行器 artifact。';
170-
}
171-
172-
const reasons = [`${input.backendId} artifact 已通过本地完整性检查。`];
173-
const missingPermissions = [
174-
['辅助功能', permissions.accessibility.status],
175-
['屏幕录制', permissions.screen_recording.status],
176-
].filter((entry) => entry[1] !== 'granted').map((entry) => entry[0]);
177-
if (missingPermissions.length > 0) {
178-
reasons.push(`等待${missingPermissions.join('、')}权限。`);
172+
return 'cu_artifact_missing';
179173
}
180-
switch (input.health.state) {
181-
case 'not_available':
182-
reasons.push(`${input.backendId} service 启动失败、已退出或已停止。`);
183-
break;
184-
case 'degraded':
185-
reasons.push(`${input.backendId} service 正在启动或恢复。`);
186-
break;
187-
case 'healthy':
188-
reasons.push('操作与截图 service 已就绪;按目标与动作类别授权后可操作本机应用。');
189-
break;
190-
case 'not_run':
191-
reasons.push('service 将在首次调用时启动;按目标与动作类别授权后可操作本机应用。');
192-
break;
193-
}
194-
return reasons.join('');
174+
return 'cu_backend_status';
195175
}
196176

197177
function staticCapability(input: {
@@ -243,7 +223,7 @@ function botCapability(
243223
};
244224
const configuration: CapabilityConfigurationSignal = hasConfig
245225
? { state: 'present', source: 'settings' }
246-
: { state: 'missing', source: 'settings', reason: '未配置平台凭据' };
226+
: { state: 'missing', source: 'settings', reason: 'missing platform credentials' };
247227
const runtimeProbe = runtimeProbeFromBotReadiness(
248228
status.readiness,
249229
channel.readinessUpdatedAt,
@@ -274,20 +254,20 @@ function botCapability(
274254
}
275255

276256
function accessibilitySnapshot(now: number, platform: NodeJS.Platform): OsPermissionSnapshot {
277-
if (platform !== 'darwin') return unsupportedPermission('accessibility', now, 'macOS TCC 权限适用');
257+
if (platform !== 'darwin') return unsupportedPermission('accessibility', now, 'macOS TCC only');
278258
try {
279259
const granted = systemPreferences.isTrustedAccessibilityClient(false);
280260
return {
281261
id: 'accessibility',
282262
status: granted ? 'granted' : 'not_determined',
283263
source: 'electron',
284264
checkedAt: now,
285-
reason: granted ? undefined : 'macOS 不区分辅助功能权限是未授权还是未申请',
265+
reason: granted ? undefined : 'accessibility_status_ambiguous',
286266
canOpenSettings: true,
287267
canRequest: false,
288268
};
289269
} catch (error) {
290-
return unknownPermission('accessibility', now, generalizedReason(error), true);
270+
return unknownPermission('accessibility', now, error, true);
291271
}
292272
}
293273

@@ -298,11 +278,7 @@ function mediaPermissionSnapshot(
298278
platform: NodeJS.Platform,
299279
): OsPermissionSnapshot {
300280
if (!supportsMediaPermissionProbe(id, platform)) {
301-
return unsupportedPermission(
302-
id,
303-
now,
304-
'屏幕录制权限状态仅能在 macOS 上读取',
305-
);
281+
return unsupportedPermission(id, now, 'screen_recording_status_mac_only');
306282
}
307283
try {
308284
const status = mapMediaAccessStatus(systemPreferences.getMediaAccessStatus(mediaType));
@@ -315,7 +291,7 @@ function mediaPermissionSnapshot(
315291
...actions,
316292
};
317293
} catch (error) {
318-
return unknownPermission(id, now, generalizedReason(error), platform === 'darwin');
294+
return unknownPermission(id, now, error, platform === 'darwin');
319295
}
320296
}
321297

@@ -328,9 +304,9 @@ function notificationSnapshot(now: number, platform: NodeJS.Platform): OsPermiss
328304
checkedAt: now,
329305
reason: supported
330306
? platform === 'darwin'
331-
? 'Electron 无法可靠读取 macOS 通知授权状态,请在系统设置中确认'
332-
: 'Electron 无法可靠读取当前系统的通知授权状态'
333-
: 'Electron 通知能力不可用',
307+
? 'notifications_status_unreadable_macos'
308+
: 'notifications_status_unreadable'
309+
: 'notifications_unsupported',
334310
canOpenSettings: platform === 'darwin',
335311
// Showing a Notification is not an authorization API and does not report
336312
// whether macOS delivered or suppressed it. Never present that probe as a
@@ -340,19 +316,23 @@ function notificationSnapshot(now: number, platform: NodeJS.Platform): OsPermiss
340316
}
341317

342318
function automationSnapshot(now: number, platform: NodeJS.Platform): OsPermissionSnapshot {
343-
if (platform !== 'darwin') return unsupportedPermission('automation', now, 'macOS TCC 权限适用');
319+
if (platform !== 'darwin') return unsupportedPermission('automation', now, 'macOS TCC only');
344320
return {
345321
id: 'automation',
346322
status: 'unknown',
347323
source: 'static',
348324
checkedAt: now,
349-
reason: 'Electron 暂不支持读取逐 App 的 Apple Events 授权状态',
325+
reason: 'no Electron API for per-target Apple Events TCC status',
350326
canOpenSettings: true,
351327
canRequest: false,
352328
};
353329
}
354330

355-
function unsupportedPermission(id: OsPermissionId, now: number, reason: string): OsPermissionSnapshot {
331+
function unsupportedPermission(
332+
id: OsPermissionId,
333+
now: number,
334+
reason: CapabilityReasonCode,
335+
): OsPermissionSnapshot {
356336
return {
357337
id,
358338
status: 'unsupported',
@@ -367,20 +347,19 @@ function unsupportedPermission(id: OsPermissionId, now: number, reason: string):
367347
function unknownPermission(
368348
id: OsPermissionId,
369349
now: number,
370-
reason: string,
350+
error: unknown,
371351
canOpenSettings: boolean,
372352
): OsPermissionSnapshot {
373353
return {
374354
id,
375355
status: 'unknown',
376356
source: 'electron',
377357
checkedAt: now,
378-
reason,
358+
reason: 'permission_probe_failed',
359+
// Raw probe error text passes through verbatim as diagnostic detail; the
360+
// presenter renders it beside the localized 'permission_probe_failed' copy.
361+
...(error instanceof Error && error.message ? { detail: error.message } : {}),
379362
canOpenSettings,
380363
canRequest: false,
381364
};
382365
}
383-
384-
function generalizedReason(error: unknown): string {
385-
return error instanceof Error ? error.message : 'permission probe failed';
386-
}

0 commit comments

Comments
 (0)