You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/09/05 14:13:53 UTC
(maka) branch main updated: chore(i18n): index catalogs instead of branching on locale (#4824)
This is an automated email from the ASF dual-hosted git repository.
Astro-Han 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 afa8a73a3d chore(i18n): index catalogs instead of branching on locale (#4824)
afa8a73a3d is described below
commit afa8a73a3d319067a28120bc1756b0cce3e38617
Author: NekoPunch <95...@users.noreply.github.com>
AuthorDate: Sat Sep 5 07:13:48 2026 -0700
chore(i18n): index catalogs instead of branching on locale (#4824)
Six presentation helpers still picked copy with `locale === '…'` comparisons, and `formatSkillLibraryDescription` decided whether a description was product copy by sniffing for CJK characters and then guessed a canned line from keywords, which mapped the only bundled skill (`computer-use`) to the wrong sentence.
Each helper now indexes a `UiCatalog`, so a new locale fails `tsc` instead of rendering the wrong language. Skill descriptions are chosen by provenance and identity: a bundled skill renders the localized description filed under its id, a rewritten bundled skill keeps the rewritten text, and a user-authored skill renders its own text in every locale. `resolveUiMessageCatalog` keeps its `en` fast path because `en` is the base every other locale merges over.
No wire or persistence change.
Refs #2672
Generated-by: Claude Code
---
.../src/main/client-settings-confirmation-copy.ts | 70 +++++++++++---------
apps/desktop/src/main/project-picker-copy.ts | 8 +--
packages/core/src/ui-locale.ts | 4 +-
packages/ui/src/__tests__/skill-status.test.ts | 76 ++++++++++++++++++++++
.../__tests__/tool-activity-presentation.test.ts | 18 ++++-
packages/ui/src/astryx-copy.ts | 6 +-
packages/ui/src/astryx-i18n.tsx | 40 ++++++------
packages/ui/src/conversation-copy.ts | 32 ++++-----
packages/ui/src/skill-status.ts | 24 +------
packages/ui/src/skills-copy.ts | 17 ++---
packages/ui/src/tool-activity.tsx | 5 +-
packages/ui/src/tool-activity/copy.ts | 4 ++
12 files changed, 185 insertions(+), 119 deletions(-)
diff --git a/apps/desktop/src/main/client-settings-confirmation-copy.ts b/apps/desktop/src/main/client-settings-confirmation-copy.ts
index 422e9aef92..57af295461 100644
--- a/apps/desktop/src/main/client-settings-confirmation-copy.ts
+++ b/apps/desktop/src/main/client-settings-confirmation-copy.ts
@@ -17,45 +17,53 @@
* under the License.
*/
-import type { UiLocale } from '@maka/core/ui-locale';
+import type { UiCatalog, UiLocale } from '@maka/core/ui-locale';
import type { ClientSettingsChange } from './client-settings-tools.js';
+type ConfirmationCopy = {
+ labels: Record<ClientSettingsChange['key'], string>;
+ on: string;
+ off: string;
+ message: string;
+ buttons: [string, string];
+};
+
+const COPY = {
+ 'zh-CN': {
+ labels: { theme: '主题', palette: '配色', uiLocale: '界面语言', runComplete: '回答完成通知', keepSystemAwake: '保持系统唤醒' },
+ on: '开启',
+ off: '关闭',
+ message: '允许 Maka 更新此客户端的设置吗?',
+ buttons: ['应用更改', '取消'],
+ },
+ 'zh-TW': {
+ labels: { theme: '主題', palette: '色彩配置', uiLocale: '介面語言', runComplete: '回答完成通知', keepSystemAwake: '保持系統喚醒' },
+ on: '開啟',
+ off: '關閉',
+ message: '允許 Maka 更新此用戶端的設定嗎?',
+ buttons: ['套用變更', '取消'],
+ },
+ en: {
+ labels: { theme: 'Theme', palette: 'Palette', uiLocale: 'UI language', runComplete: 'Run-complete notifications', keepSystemAwake: 'Keep system awake' },
+ on: 'true',
+ off: 'false',
+ message: "Allow Maka to update this client's settings?",
+ buttons: ['Apply changes', 'Cancel'],
+ },
+} satisfies UiCatalog<ConfirmationCopy>;
+
export function clientSettingsConfirmation(
changes: readonly ClientSettingsChange[],
locale: UiLocale,
): { message: string; detail: string; buttons: [string, string] } {
- 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 (locale === 'en') return String(input);
- if (input === true) return locale === 'zh-CN' ? '开启' : '開啟';
- if (input === false) return locale === 'zh-CN' ? '关闭' : '關閉';
- return String(input);
- };
+ const copy = COPY[locale];
+ const value = (input: string | boolean | undefined): string =>
+ input === true ? copy.on : input === false ? copy.off : String(input);
return {
- message:
- locale === 'en'
- ? "Allow Maka to update this client's settings?"
- : locale === 'zh-CN'
- ? '允许 Maka 更新此客户端的设置吗?'
- : '允許 Maka 更新此用戶端的設定嗎?',
+ message: copy.message,
detail: changes
- .map(
- (change) =>
- `${labels[change.key][localeIndex]}: ${value(change.current)} → ${value(change.next)}`,
- )
+ .map((change) => `${copy.labels[change.key]}: ${value(change.current)} → ${value(change.next)}`)
.join('\n'),
- buttons:
- locale === 'en'
- ? ['Apply changes', 'Cancel']
- : locale === 'zh-CN'
- ? ['应用更改', '取消']
- : ['套用變更', '取消'],
+ buttons: [...copy.buttons],
};
}
diff --git a/apps/desktop/src/main/project-picker-copy.ts b/apps/desktop/src/main/project-picker-copy.ts
index 45a2598914..1ce70aa251 100644
--- a/apps/desktop/src/main/project-picker-copy.ts
+++ b/apps/desktop/src/main/project-picker-copy.ts
@@ -17,10 +17,10 @@
* under the License.
*/
-import type { UiLocale } from '@maka/core/ui-locale';
+import type { UiCatalog, UiLocale } from '@maka/core/ui-locale';
+
+const TITLE = { 'zh-CN': '添加项目', 'zh-TW': '新增專案', en: 'Add project' } satisfies UiCatalog<string>;
export function projectPickerTitle(locale: UiLocale): string {
- if (locale === 'zh-CN') return '添加项目';
- if (locale === 'zh-TW') return '新增專案';
- return 'Add project';
+ return TITLE[locale];
}
diff --git a/packages/core/src/ui-locale.ts b/packages/core/src/ui-locale.ts
index 7c3ee8cd88..2272506d79 100644
--- a/packages/core/src/ui-locale.ts
+++ b/packages/core/src/ui-locale.ts
@@ -78,9 +78,7 @@ export function resolveUiMessageCatalog<T>(catalog: UiMessageCatalog<T>): UiCata
return Object.fromEntries(
UI_LOCALES.map((locale) => [
locale,
- locale === 'en'
- ? catalog.en
- : mergeUiMessages(catalog.en, catalog[locale] as DeepPartial<T> | undefined),
+ mergeUiMessages(catalog.en, catalog[locale] as DeepPartial<T> | undefined),
]),
) as UiCatalog<T>;
}
diff --git a/packages/ui/src/__tests__/skill-status.test.ts b/packages/ui/src/__tests__/skill-status.test.ts
new file mode 100644
index 0000000000..b858d838fa
--- /dev/null
+++ b/packages/ui/src/__tests__/skill-status.test.ts
@@ -0,0 +1,76 @@
+/*
+ * 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 { UI_LOCALES } from '@maka/core/ui-locale';
+import type { SkillEntry } from '../module-panel-types.js';
+import { formatSkillLibraryDescription } from '../skill-status.js';
+import { getSkillsCopy } from '../skills-copy.js';
+
+const COMPUTER_USE_DESCRIPTION =
+ 'Use when the user asks to inspect or operate a local desktop application UI; prefer Browser tools for web pages.';
+
+function skill(overrides: Partial<SkillEntry>): SkillEntry {
+ return {
+ id: 'computer-use',
+ name: 'Computer Use',
+ description: COMPUTER_USE_DESCRIPTION,
+ path: '/skills/computer-use',
+ enabled: true,
+ runtimeStatus: 'enabled',
+ ...overrides,
+ };
+}
+
+test('bundled skills render the product description for their id in every locale', () => {
+ for (const locale of UI_LOCALES) {
+ const copy = getSkillsCopy(locale);
+ const rendered = formatSkillLibraryDescription(skill({ sourceType: 'bundled' }), copy);
+ assert.equal(rendered, copy.bundledDescription['computer-use'], locale);
+ assert.notEqual(rendered, COMPUTER_USE_DESCRIPTION, locale);
+ assert.notEqual(rendered, undefined, locale);
+ }
+});
+
+test('a bundled skill the user edited keeps the edited description', () => {
+ assert.equal(
+ formatSkillLibraryDescription(
+ skill({ sourceType: 'bundled', userModified: true, description: '操作本机应用。' }),
+ getSkillsCopy('en'),
+ ),
+ '操作本机应用。',
+ );
+});
+
+test('skills the user authored keep their own description in every locale', () => {
+ for (const sourceType of ['workspace', 'managed', 'unknown', undefined] as const) {
+ assert.equal(
+ formatSkillLibraryDescription(skill({ id: 'docx', sourceType, description: 'Create and edit Word documents.' }), getSkillsCopy('zh-CN')),
+ 'Create and edit Word documents.',
+ );
+ }
+ assert.equal(
+ formatSkillLibraryDescription(
+ skill({ id: 'notes', sourceType: 'workspace', description: '整理会议纪要。' }),
+ getSkillsCopy('en'),
+ ),
+ '整理会议纪要。',
+ );
+});
diff --git a/packages/ui/src/__tests__/tool-activity-presentation.test.ts b/packages/ui/src/__tests__/tool-activity-presentation.test.ts
index e0f2877b90..349c60a1ad 100644
--- a/packages/ui/src/__tests__/tool-activity-presentation.test.ts
+++ b/packages/ui/src/__tests__/tool-activity-presentation.test.ts
@@ -22,17 +22,19 @@ import { describe, it } from 'node:test';
import { createElement, type ReactNode } from 'react';
import { renderToStaticMarkup as renderReactToStaticMarkup } from 'react-dom/server';
import { computerUseModelCallArgs } from '@maka/core/computer-use';
+import { UI_LOCALES, type UiCatalog, type UiLocale } from '@maka/core/ui-locale';
import { ToolCallDetail, ToolTrow } from '../tool-activity.js';
import type { ToolActivityItem } from '../materialize.js';
import { LocaleProvider } from '../locale-context.js';
import { ToolResultPreview } from '../tool-activity/tool-result-preview.js';
+import { getToolActivityCopy } from '../tool-activity/copy.js';
import {
computerActionLabel,
computerRunningLabel,
isComputerTool,
} from '../tool-activity/computer-action-label.js';
-function renderToStaticMarkup(node: ReactNode, locale: 'zh-CN' | 'en' = 'zh-CN'): string {
+function renderToStaticMarkup(node: ReactNode, locale: UiLocale = 'zh-CN'): string {
return renderReactToStaticMarkup(createElement(LocaleProvider, {
locale,
children: node,
@@ -73,6 +75,20 @@ describe('tool activity presentation', () => {
assert.doesNotMatch(zh, /Client Capability tools require/);
assert.match(en, /Bypass mode required/);
assert.match(en, /Switch and retry/);
+
+ const errorMessages = {
+ 'zh-CN': '需要“绕过”模式。此操作会直接控制本机应用,无法在沙箱模式下执行。',
+ 'zh-TW': '需要“繞過”模式。此操作會直接控制本機應用,無法在沙箱模式下執行。',
+ en: 'Bypass mode required. This action controls a local app directly and cannot run inside the sandbox.',
+ } satisfies UiCatalog<string>;
+ for (const locale of UI_LOCALES) {
+ const row = renderToStaticMarkup(createElement(ToolTrow, { items: [item] }), locale);
+ assert.ok(row.includes(`title="${errorMessages[locale]}"`), `${locale}: full bypass error`);
+ assert.doesNotMatch(row, /Client Capability tools require/);
+ const copy = getToolActivityCopy(locale).requiresBypass;
+ assert.ok(copy.errorMessage.startsWith(copy.title), `${locale}: tooltip opens with the banner title`);
+ assert.ok(copy.errorMessage.endsWith(copy.description), `${locale}: tooltip ends with the banner description`);
+ }
});
it('keeps generic requires-bypass failures verbatim', () => {
diff --git a/packages/ui/src/astryx-copy.ts b/packages/ui/src/astryx-copy.ts
index 30521baffb..9a72ad5a77 100644
--- a/packages/ui/src/astryx-copy.ts
+++ b/packages/ui/src/astryx-copy.ts
@@ -30,9 +30,9 @@
* in `shared-ui-copy.ts` instead and are referenced from the override map, so
* shared wording keeps one home.
*
- * `zh` only: `astryxMessageOverrides` returns `undefined` for `en`, which
- * resolves Astryx's shipped defaults — an `en` mirror here would be dead
- * config drifting against upstream.
+ * `zh` only: for `en`, `astryxMessageOverrides` overrides two drawer tooltips
+ * and otherwise resolves Astryx's shipped defaults — an `en` mirror here would
+ * be dead config drifting against upstream.
*/
export interface AstryxCopy {
appShell: { mobileNavigation: string; skipToContent: string };
diff --git a/packages/ui/src/astryx-i18n.tsx b/packages/ui/src/astryx-i18n.tsx
index 8230e358c6..08f0e06fa2 100644
--- a/packages/ui/src/astryx-i18n.tsx
+++ b/packages/ui/src/astryx-i18n.tsx
@@ -26,7 +26,7 @@ import type { Overrides } from '@astryxdesign/core/i18n';
import { getSharedUiCopy } from './shared-ui-copy.js';
import { ASTRYX_COPY_ZH, ASTRYX_COPY_ZH_TW } from './astryx-copy.js';
import { useUiLocale } from './locale-context.js';
-import type { UiLocale } from './locale-helpers.js';
+import type { UiCatalog, UiLocale } from '@maka/core/ui-locale';
/**
* Astryx ships no `zh` message catalog: its built-in strings ("Copy code",
@@ -65,7 +65,7 @@ export function AstryxLocaleProvider({
// on the overrides object, so a fresh map every render would re-render
// every Astryx i18n consumer on every AppShell render.
const overrides = useMemo(() => {
- const base = astryxMessageOverrides(locale)?.[locale];
+ const base = astryxMessageOverrides(locale)[locale];
const inherited = ambient.locale === locale
? ambient.overrides?.[locale]
: undefined;
@@ -89,25 +89,27 @@ export function AstryxLocaleProvider({
);
}
-export function astryxMessageOverrides(locale: UiLocale): Overrides | undefined {
- if (locale === 'en') {
- // en otherwise resolves to Astryx's shipped defaults. These two are the
- // sole exceptions: the drawer toggle's aria-label doubles as its visible
- // hover tooltip (composer.css renders attr(aria-label)), and the shipped
- // "Collapse {label}" / "Expand {label}" read as state names there — the
- // click affordance is the tooltip's whole point.
- return {
- en: {
- '@astryx.chatComposerDrawer.collapse': 'Click to collapse {label}',
- '@astryx.chatComposerDrawer.expand': 'Click to expand {label}',
- },
- };
- }
+const OVERRIDES_BY_LOCALE = {
+ // The drawer aria-label also serves as a tooltip; all other English copy uses Astryx defaults.
+ en: {
+ en: {
+ '@astryx.chatComposerDrawer.collapse': 'Click to collapse {label}',
+ '@astryx.chatComposerDrawer.expand': 'Click to expand {label}',
+ },
+ },
+ 'zh-CN': chineseOverrides('zh-CN', ASTRYX_COPY_ZH),
+ 'zh-TW': chineseOverrides('zh-TW', ASTRYX_COPY_ZH_TW),
+} satisfies UiCatalog<Overrides>;
+
+export function astryxMessageOverrides(locale: UiLocale): Overrides {
+ return OVERRIDES_BY_LOCALE[locale];
+}
+
+// The catalogues live off-barrel in astryx-copy.ts because nothing outside
+// this map consumes them.
+function chineseOverrides(locale: 'zh-CN' | 'zh-TW', astryx: typeof ASTRYX_COPY_ZH): Overrides {
const shared = getSharedUiCopy(locale);
const form = shared.formControls;
- // `locale` is narrowed to a Chinese locale past the early return; the catalogues live
- // off-barrel in astryx-copy.ts because nothing outside this map consumes it.
- const astryx = locale === 'zh-TW' ? ASTRYX_COPY_ZH_TW : ASTRYX_COPY_ZH;
return {
[locale]: {
'@astryx.codeBlock.copyCode': shared.markdown.copyCode,
diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts
index 12ed2c76d6..eef3fea626 100644
--- a/packages/ui/src/conversation-copy.ts
+++ b/packages/ui/src/conversation-copy.ts
@@ -44,39 +44,31 @@ function formatCompactTokenCount(count: number): string {
return `${thousands >= 100 ? Math.round(thousands) : Math.round(thousands * 10) / 10}k`;
}
-/** Wall-clock units for the goal chip's elapsed label, per locale (zh uses spaced words, en letters). */
-interface GoalElapsedUnits {
+/** Wall-clock units per locale (zh uses words, en letters); each copy entry supplies its own. */
+interface DurationUnits {
second: string;
minute: string;
hour: string;
day: string;
}
-/** Humanize a retry delay (seconds) — 1s granularity, locale-aware. zh: `4小时 28分 3秒`, en: `4h 28m 3s`. */
-export function formatRetryDelay(seconds: number, locale: UiLocale): string {
+/** Humanize a retry delay (seconds) — 1s granularity. zh: `4小时 28分 3秒`, en: `4h 28m 3s`. */
+export function formatRetryDelay(seconds: number, units: DurationUnits): string {
const s = Math.max(1, Math.ceil(seconds));
const d = Math.floor(s / 86_400);
const h = Math.floor((s % 86_400) / 3_600);
const m = Math.floor((s % 3_600) / 60);
const sec = s % 60;
- if (locale !== 'en') {
- const parts: string[] = [];
- if (d > 0) parts.push(`${d}天`);
- if (h > 0) parts.push(`${h}${locale === 'zh-TW' ? '小時' : '小时'}`);
- if (m > 0) parts.push(`${m}分`);
- if (sec > 0 || parts.length === 0) parts.push(`${sec}秒`);
- return parts.join(' ');
- }
const parts: string[] = [];
- if (d > 0) parts.push(`${d}d`);
- if (h > 0) parts.push(`${h}h`);
- if (m > 0) parts.push(`${m}m`);
- if (sec > 0 || parts.length === 0) parts.push(`${sec}s`);
+ if (d > 0) parts.push(`${d}${units.day}`);
+ if (h > 0) parts.push(`${h}${units.hour}`);
+ if (m > 0) parts.push(`${m}${units.minute}`);
+ if (sec > 0 || parts.length === 0) parts.push(`${sec}${units.second}`);
return parts.join(' ');
}
/** One shared elapsed ladder so the zh/en goalElapsed entries cannot drift. */
-function formatGoalElapsedUnits(elapsedMs: number, units: GoalElapsedUnits): string {
+function formatGoalElapsedUnits(elapsedMs: number, units: DurationUnits): string {
const seconds = Math.max(0, Math.floor(elapsedMs / 1000));
if (seconds < 60) return `${seconds}${units.second}`;
const minutes = Math.floor(seconds / 60);
@@ -548,7 +540,7 @@ const CONVERSATION_COPY = {
chooseAriaLabel: (label, branch) => branch ? `选择项目:${label},当前分支 ${branch}` : `选择项目:${label}`,
},
messages: {
- you: '你', assistant: 'Maka', processing: '正在处理…', continuing: '继续中…', awaitingModelOutput: '等待模型输出…', providerRetryScheduled: (seconds, attempt, maxAttempts) => `${formatRetryDelay(seconds, 'zh-CN')}后重试(${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `正在重试(${attempt}/${maxAttempts})`, providerRetryWaiting: (attempt, maxAttempts) => `等待重试(${attempt}/${maxAttempts})`, providerRetryReason: { network: '网络中断', provider_capacity: '模型服务暂时满载', provider_unavaila [...]
+ you: '你', assistant: 'Maka', processing: '正在处理…', continuing: '继续中…', awaitingModelOutput: '等待模型输出…', providerRetryScheduled: (seconds, attempt, maxAttempts) => `${formatRetryDelay(seconds, { day: '天', hour: '小时', minute: '分', second: '秒' })}后重试(${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `正在重试(${attempt}/${maxAttempts})`, providerRetryWaiting: (attempt, maxAttempts) => `等待重试(${attempt}/${maxAttempts})`, providerRetryReason: { network: '网络中断', provi [...]
editMessageDisabledDirectoryReferences: '包含文件夹引用的历史消息暂不支持编辑并重发',
userAriaLabel: '你发送的消息', systemAriaLabel: '系统消息', assistantAriaLabel: 'Maka 的回答', answerActionsAriaLabel: (context) => `回答操作${context ? `:${context}` : ''}`, answerActionAriaLabel: (action, context) => `${action}回答${context ? `:${context}` : ''}`, messageActionAriaLabel: (action, context) => `${action}消息${context ? `:${context}` : ''}`, sourceAriaLabel: '本轮回答的来源', derivativesAriaLabel: '本轮回答的衍生', scheduledTaskTriggered: '定时任务触发', scheduledTaskTitle: (id) => `由定时任务触发 · ${id}`, legac [...]
thinkingTruncatedTitle: '部分 reasoning 已截断;显示的是最近的内容', outputTruncatedTitle: '助手输出已超过单次回合上限,超出部分未渲染。如需完整内容请重新生成或查看持久化的任务日志。', removeAttachmentAriaLabel: (name) => `移除 ${name}`, quoteLabel: '引用', quoteExpandAriaLabel: '展开引用全文', quoteCollapseAriaLabel: '收起引用', removeQuoteAriaLabel: '移除引用', aborted: '已中断', abortedByStop: '已中断 · 由停止按钮触发',
@@ -700,7 +692,7 @@ const CONVERSATION_COPY = {
chooseAriaLabel: (label, branch) => branch ? `選擇專案:${label},目前分支 ${branch}` : `選擇專案:${label}`,
},
messages: {
- you: '你', assistant: 'Maka', processing: '正在處理…', continuing: '繼續中…', awaitingModelOutput: '等待模型輸出…', providerRetryScheduled: (seconds, attempt, maxAttempts) => `${formatRetryDelay(seconds, 'zh-TW')}後重試(${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `正在重試(${attempt}/${maxAttempts})`, providerRetryWaiting: (attempt, maxAttempts) => `等待重試(${attempt}/${maxAttempts})`, providerRetryReason: { network: '網路中斷', provider_capacity: '模型服務暫時滿載', provider_unavaila [...]
+ you: '你', assistant: 'Maka', processing: '正在處理…', continuing: '繼續中…', awaitingModelOutput: '等待模型輸出…', providerRetryScheduled: (seconds, attempt, maxAttempts) => `${formatRetryDelay(seconds, { day: '天', hour: '小時', minute: '分', second: '秒' })}後重試(${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `正在重試(${attempt}/${maxAttempts})`, providerRetryWaiting: (attempt, maxAttempts) => `等待重試(${attempt}/${maxAttempts})`, providerRetryReason: { network: '網路中斷', provi [...]
editMessageDisabledDirectoryReferences: '包含資料夾引用的歷史訊息暫不支援編輯並重發',
userAriaLabel: '你傳送的訊息', systemAriaLabel: '系統訊息', assistantAriaLabel: 'Maka 的回答', answerActionsAriaLabel: (context) => `回答操作${context ? `:${context}` : ''}`, answerActionAriaLabel: (action, context) => `${action}回答${context ? `:${context}` : ''}`, messageActionAriaLabel: (action, context) => `${action}訊息${context ? `:${context}` : ''}`, sourceAriaLabel: '本輪迴答的來源', derivativesAriaLabel: '本輪迴答的衍生', scheduledTaskTriggered: '定時任務觸發', scheduledTaskTitle: (id) => `由定時任務觸發 · ${id}`, legac [...]
thinkingTruncatedTitle: '部分 reasoning 已截斷;顯示的是最近的內容', outputTruncatedTitle: '助手輸出已超過單次回合上限,超出部分未渲染。如需完整內容請重新生成或檢視持久化的任務記錄。', removeAttachmentAriaLabel: (name) => `移除 ${name}`, quoteLabel: '引用', quoteExpandAriaLabel: '展開引用全文', quoteCollapseAriaLabel: '收起引用', removeQuoteAriaLabel: '移除引用', aborted: '(已中斷)', abortedByStop: '(已中斷 · 由停止按鈕觸發)',
@@ -878,7 +870,7 @@ const CONVERSATION_COPY = {
chooseAriaLabel: (label, branch) => branch ? `Choose project: ${label}, current branch ${branch}` : `Choose project: ${label}`,
},
messages: {
- you: 'You', assistant: 'Maka', processing: 'Working…', continuing: 'Continuing…', awaitingModelOutput: 'Waiting for model output…', providerRetryScheduled: (seconds, attempt, maxAttempts) => `Retrying in ${formatRetryDelay(seconds, 'en')} (${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `Retrying (${attempt}/${maxAttempts})`, providerRetryWaiting: (attempt, maxAttempts) => `Waiting to retry (${attempt}/${maxAttempts})`, providerRetryReason: { network: ' [...]
+ you: 'You', assistant: 'Maka', processing: 'Working…', continuing: 'Continuing…', awaitingModelOutput: 'Waiting for model output…', providerRetryScheduled: (seconds, attempt, maxAttempts) => `Retrying in ${formatRetryDelay(seconds, { day: 'd', hour: 'h', minute: 'm', second: 's' })} (${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `Retrying (${attempt}/${maxAttempts})`, providerRetryWaiting: (attempt, maxAttempts) => `Waiting to retry (${attempt}/${maxA [...]
editMessageDisabledDirectoryReferences: 'Edit & resend does not yet support messages with folder references',
userAriaLabel: 'Your message', systemAriaLabel: 'System message', assistantAriaLabel: "Maka's response", answerActionsAriaLabel: (context) => `Response actions${context ? `: ${context}` : ''}`, answerActionAriaLabel: (action, context) => `${action} response${context ? `: ${context}` : ''}`, messageActionAriaLabel: (action, context) => `${action} message${context ? `: ${context}` : ''}`, sourceAriaLabel: 'Source of this response', derivativesAriaLabel: 'Responses derived from this o [...]
thinkingTruncatedTitle: 'Some reasoning was truncated; showing the most recent content', outputTruncatedTitle: 'The assistant output exceeded the per-turn limit. Regenerate it or inspect the persisted task log for the complete content.', removeAttachmentAriaLabel: (name) => `Remove ${name}`, quoteLabel: 'Quote', quoteExpandAriaLabel: 'Show the full quoted excerpt', quoteCollapseAriaLabel: 'Collapse the quoted excerpt', removeQuoteAriaLabel: 'Remove quote', aborted: 'Interrupted', a [...]
diff --git a/packages/ui/src/skill-status.ts b/packages/ui/src/skill-status.ts
index 95611e8990..f628e197ad 100644
--- a/packages/ui/src/skill-status.ts
+++ b/packages/ui/src/skill-status.ts
@@ -117,26 +117,6 @@ export function formatSkillRuntimeLabel(skill: SkillEntry, copy: SkillsCopy): st
export function formatSkillLibraryDescription(skill: SkillEntry, copy: SkillsCopy): string | undefined {
const raw = skill.description?.trim();
if (!raw) return undefined;
- if (/[\u3400-\u9fff]/.test(raw)) return raw;
-
- const source = `${skill.id} ${skill.name} ${raw}`.toLowerCase();
- if (source.includes('docx') || source.includes('word') || source.includes('google docs')) {
- return copy.description.document;
- }
- if (source.includes('ppt') || source.includes('powerpoint') || source.includes('slide') || source.includes('presentation')) {
- return copy.description.presentation;
- }
- if (source.includes('spreadsheet') || source.includes('excel') || source.includes('csv') || source.includes('xlsx')) {
- return copy.description.spreadsheet;
- }
- if (source.includes('image') || source.includes('photo') || source.includes('bitmap')) {
- return copy.description.image;
- }
- if (source.includes('browser') || source.includes('chrome') || source.includes('web target')) {
- return copy.description.browser;
- }
- if (source.includes('macos') || source.includes('swiftui') || source.includes('appkit')) {
- return copy.description.macos;
- }
- return copy.description.fallback;
+ if (skill.sourceType !== 'bundled' || skill.userModified) return raw;
+ return copy.bundledDescription[skill.id] ?? raw;
}
diff --git a/packages/ui/src/skills-copy.ts b/packages/ui/src/skills-copy.ts
index 34fbf0e820..8385e95e73 100644
--- a/packages/ui/src/skills-copy.ts
+++ b/packages/ui/src/skills-copy.ts
@@ -110,15 +110,8 @@ export interface SkillsCopy {
overwrite: string;
update: string;
};
- description: {
- document: string;
- presentation: string;
- spreadsheet: string;
- image: string;
- browser: string;
- macos: string;
- fallback: string;
- };
+ /** Product copy for bundled skills, keyed by BUNDLED_SKILL_CATALOG id. */
+ bundledDescription: Partial<Record<string, string>>;
status: {
metadataError: string;
managed: Record<ManagedUpdateStatus, string>;
@@ -169,7 +162,7 @@ const SKILLS_COPY = {
context: { scope: { project: '项目', workspace: '工作区', user: '用户', custom: '自定义' }, decision: { advertised: '已进入上下文', disabled: '已停用', invalid: '元数据无效', host_incompatible: '主机不兼容', shadowed: '被高优先级覆盖', budget: '因预算省略' }, needsReview: '待确认', discoverySource: (scope, source) => `${scope}/${source} 发现源`, discoveryDiagnostic: { blocked_path: '路径被安全策略阻止', read_failed: '来源不可读取' } },
row: { opening: '打开中…', reviewing: '审查中…', use: '使用', openTitle: '打开 SKILL.md', pinTitle: '固定到技能上下文', unpinTitle: '取消固定', viewDiff: '查看差异', viewUpdate: '查看更新', confirmDeleteAriaLabel: (name) => `确认删除 ${name}`, deleteDescription: '此操作会删除这个 Skill 的文件,且无法撤销。', cancel: '取消', delete: '删除' },
review: { ariaLabel: 'Skill 更新审查', title: '更新审查', source: (id) => `来源 ${id}`, managedSource: '受管理来源', hasBaseline: '已有基线', missingBaseline: '缺少基线', lineTransition: (current, source) => `${current} → ${source} 行`, changedLines: (count) => `${count} 行不同`, warning: '工作区副本已有本地修改。继续更新会用来源库版本覆盖当前 SKILL.md。', workspace: '当前工作区', sourceVersion: '来源库版本', cancel: '取消', overwrite: '覆盖本地修改', update: '更新到来源版本' },
- description: { document: '创建、编辑、检查文档内容。', presentation: '创建、编辑、检查演示文稿。', spreadsheet: '创建、编辑、分析表格数据。', image: '生成或编辑图片素材。', browser: '打开、检查、操作网页界面。', macos: '辅助构建和调试 macOS 应用。', fallback: '打开技能文件查看适用场景。' },
+ bundledDescription: { 'computer-use': '查看并操作本机桌面应用的界面。' },
status: { metadataError: '元数据异常', managed: { source_missing: '来源缺失', update_available: '可更新', local_modified: '本地已修改', metadata_error: '元数据异常', up_to_date: '受管理', not_managed: '受管理' }, modified: '已修改', bundled: '内置', local: '本地', stateError: '状态异常', enabled: '已启用', disabled: '已停用' },
page: { title: '技能', toolbarAria: '技能筛选与视图', metaInstalled: (count) => `${count} 个已安装`, metaUpdates: (count) => `${count} 个可更新`, metaAvailable: (count) => `${count} 个可安装`, searchMatches: (count) => `${count} 个匹配`, search: '搜索技能', openFolder: '打开目录', moreActions: '更多技能操作', refreshing: '刷新中…', refresh: '刷新' },
detail: { label: '技能详情', enabled: '启用', pinned: '已固定', inspectorOpened: (name) => `已打开 ${name} 的详情`, idLabel: '标识', scopeLabel: '范围', sourceLabel: '来源', contextLabel: '上下文', runtimeLabel: '运行状态', toolsLabel: '声明工具', pathLabel: '路径' },
@@ -184,7 +177,7 @@ const SKILLS_COPY = {
context: { scope: { project: '專案', workspace: '工作區', user: '使用者', custom: '自訂' }, decision: { advertised: '已進入上下文', disabled: '已停用', invalid: '後設資料無效', host_incompatible: '主機不相容', shadowed: '被高優先順序覆蓋', budget: '因預算省略' }, needsReview: '待確認', discoverySource: (scope, source) => `${scope}/${source} 發現源`, discoveryDiagnostic: { blocked_path: '路徑被安全策略阻止', read_failed: '來源不可讀取' } },
row: { opening: '開啟中…', reviewing: '審查中…', use: '使用', openTitle: '開啟 SKILL.md', pinTitle: '固定到技能上下文', unpinTitle: '取消固定', viewDiff: '檢視差異', viewUpdate: '檢視更新', confirmDeleteAriaLabel: (name) => `確認刪除 ${name}`, deleteDescription: '此操作會刪除這個 Skill 的檔案,且無法撤銷。', cancel: '取消', delete: '刪除' },
review: { ariaLabel: 'Skill 更新審查', title: '更新審查', source: (id) => `來源 ${id}`, managedSource: '受管理來源', hasBaseline: '已有基線', missingBaseline: '缺少基線', lineTransition: (current, source) => `${current} → ${source} 行`, changedLines: (count) => `${count} 行不同`, warning: '工作區副本已有本地修改。繼續更新會用來源庫版本覆蓋目前 SKILL.md。', workspace: '目前工作區', sourceVersion: '來源庫版本', cancel: '取消', overwrite: '覆蓋本地修改', update: '更新到來源版本' },
- description: { document: '建立、編輯、檢查文件內容。', presentation: '建立、編輯、檢查簡報。', spreadsheet: '建立、編輯、分析表格資料。', image: '生成或編輯圖片素材。', browser: '開啟、檢查、操作網頁介面。', macos: '輔助構建和除錯 macOS 應用。', fallback: '開啟技能檔案檢視適用場景。' },
+ bundledDescription: { 'computer-use': '檢視並操作本機桌面應用的介面。' },
status: { metadataError: '後設資料異常', managed: { source_missing: '來源缺失', update_available: '可更新', local_modified: '本地已修改', metadata_error: '後設資料異常', up_to_date: '受管理', not_managed: '受管理' }, modified: '已修改', bundled: '內建', local: '本地', stateError: '狀態異常', enabled: '已啟用', disabled: '已停用' },
page: { title: '技能', toolbarAria: '技能篩選與檢視', metaInstalled: (count) => `${count} 個已安裝`, metaUpdates: (count) => `${count} 個可更新`, metaAvailable: (count) => `${count} 個可安裝`, searchMatches: (count) => `${count} 個符合`, search: '搜尋技能', openFolder: '開啟目錄', moreActions: '更多技能操作', refreshing: '重新整理中…', refresh: '重新整理' },
detail: { label: '技能詳情', enabled: '啟用', pinned: '已固定', inspectorOpened: (name) => `已開啟 ${name} 的詳情`, idLabel: '標識', scopeLabel: '範圍', sourceLabel: '來源', contextLabel: '上下文', runtimeLabel: '執行狀態', toolsLabel: '宣告工具', pathLabel: '路徑' },
@@ -199,7 +192,7 @@ const SKILLS_COPY = {
context: { scope: { project: 'Project', workspace: 'Workspace', user: 'User', custom: 'Custom' }, decision: { advertised: 'In context', disabled: 'Disabled', invalid: 'Invalid metadata', host_incompatible: 'Host incompatible', shadowed: 'Shadowed', budget: 'Budget omitted' }, needsReview: 'Needs review', discoverySource: (scope, source) => `${scope}/${source} discovery source`, discoveryDiagnostic: { blocked_path: 'Path blocked by the safety policy', read_failed: 'Source could not be [...]
row: { opening: 'Opening…', reviewing: 'Reviewing…', use: 'Use', openTitle: 'Open SKILL.md', pinTitle: 'Pin to the skill context', unpinTitle: 'Unpin', viewDiff: 'View diff', viewUpdate: 'View update', confirmDeleteAriaLabel: (name) => `Delete ${name}?`, deleteDescription: 'This removes the Skill files and cannot be undone.', cancel: 'Cancel', delete: 'Delete' },
review: { ariaLabel: 'Skill update review', title: 'Update review', source: (id) => `Source ${id}`, managedSource: 'Managed source', hasBaseline: 'Baseline available', missingBaseline: 'No baseline', lineTransition: (current, source) => `${current} → ${source} lines`, changedLines: (count) => `${count} ${count === 1 ? 'line differs' : 'lines differ'}`, warning: 'The workspace copy has local changes. Continuing will replace the current SKILL.md with the source version.', workspace: 'C [...]
- description: { document: 'Create, edit, and inspect documents.', presentation: 'Create, edit, and inspect presentations.', spreadsheet: 'Create, edit, and analyze spreadsheet data.', image: 'Generate or edit images.', browser: 'Open, inspect, and operate web interfaces.', macos: 'Build and debug macOS apps.', fallback: 'Open the skill file to see when to use it.' },
+ bundledDescription: { 'computer-use': 'Inspect and operate local desktop app interfaces.' },
status: { metadataError: 'Metadata error', managed: { source_missing: 'Source missing', update_available: 'Update available', local_modified: 'Locally modified', metadata_error: 'Metadata error', up_to_date: 'Managed', not_managed: 'Managed' }, modified: 'Modified', bundled: 'Built in', local: 'Local', stateError: 'State error', enabled: 'Enabled', disabled: 'Disabled' },
page: { title: 'Skills', toolbarAria: 'Skill filters and views', metaInstalled: (count) => `${count} installed`, metaUpdates: (count) => count === 1 ? '1 update available' : `${count} updates available`, metaAvailable: (count) => count === 1 ? '1 available to install' : `${count} available to install`, searchMatches: (count) => `${count} ${count === 1 ? 'match' : 'matches'}`, search: 'Search skills', openFolder: 'Open folder', moreActions: 'More Skill actions', refreshing: 'Refreshin [...]
detail: { label: 'Skill details', enabled: 'Enabled', pinned: 'Pinned', inspectorOpened: (name) => `${name} details opened`, idLabel: 'ID', scopeLabel: 'Scope', sourceLabel: 'Source', contextLabel: 'Context', runtimeLabel: 'Runtime', toolsLabel: 'Declared tools', pathLabel: 'Path' },
diff --git a/packages/ui/src/tool-activity.tsx b/packages/ui/src/tool-activity.tsx
index dd084ae7da..6b4be6bc49 100644
--- a/packages/ui/src/tool-activity.tsx
+++ b/packages/ui/src/tool-activity.tsx
@@ -649,10 +649,7 @@ function astryxToolStatus(item: ToolActivityItem): ChatToolCallItem['status'] {
function toolCallErrorMessage(item: ToolActivityItem, locale: UiLocale): string | undefined {
if (item.status !== 'errored') return undefined;
if (isRequiresBypassToolResult(item.result)) {
- const copy = getToolActivityCopy(locale).requiresBypass;
- return locale !== 'en'
- ? `${copy.title}。${copy.description}`
- : `${copy.title}. ${copy.description}`;
+ return getToolActivityCopy(locale).requiresBypass.errorMessage;
}
return summarizeErrorText(formatUserVisibleToolText(
redactSecrets(extractErrorText(item.result, locale)),
diff --git a/packages/ui/src/tool-activity/copy.ts b/packages/ui/src/tool-activity/copy.ts
index b3f3929fe4..8b042f8a71 100644
--- a/packages/ui/src/tool-activity/copy.ts
+++ b/packages/ui/src/tool-activity/copy.ts
@@ -49,6 +49,7 @@ export interface ToolActivityCopy {
requiresBypass: {
title: string;
description: string;
+ errorMessage: string;
action: string;
pending: string;
};
@@ -196,6 +197,7 @@ const TOOL_ACTIVITY_COPY = {
requiresBypass: {
title: '需要“绕过”模式',
description: '此操作会直接控制本机应用,无法在沙箱模式下执行。',
+ errorMessage: '需要“绕过”模式。此操作会直接控制本机应用,无法在沙箱模式下执行。',
action: '切换并重试',
pending: '正在切换…',
},
@@ -303,6 +305,7 @@ const TOOL_ACTIVITY_COPY = {
requiresBypass: {
title: '需要“繞過”模式',
description: '此操作會直接控制本機應用,無法在沙箱模式下執行。',
+ errorMessage: '需要“繞過”模式。此操作會直接控制本機應用,無法在沙箱模式下執行。',
action: '切換並重試',
pending: '正在切換…',
},
@@ -410,6 +413,7 @@ const TOOL_ACTIVITY_COPY = {
requiresBypass: {
title: 'Bypass mode required',
description: 'This action controls a local app directly and cannot run inside the sandbox.',
+ errorMessage: 'Bypass mode required. This action controls a local app directly and cannot run inside the sandbox.',
action: 'Switch and retry',
pending: 'Switching…',
},
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org