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