Browse Source

Merge pull request #4321 from deepseek-harness/turtle/builtin-plugin-i18n

fix(web): localize built-in plugin names and descriptions
Turtle 4 ngày trước cách đây
mục cha
commit
e879c7456a

+ 9 - 9
apps/web/tests/expected/plugin-manager/live-enabled.expected.md

@@ -8,17 +8,17 @@
 - text: "3"
 - text: "3"
 - list:
 - list:
   - listitem:
   - listitem:
-    - button "查看 experimental-agent-team-profile": experimental-agent-team-profile
-    - text: 官方 Experimental profile bundle enabling Agent Teams over dsh-base
-    - switch "启用 experimental-agent-team-profile"
+    - button "查看 智能体团队(实验性)": 智能体团队(实验性)
+    - text: 官方 启用智能体团队协作与团队工具。
+    - switch "启用 智能体团队(实验性)"
   - listitem:
   - listitem:
-    - button "查看 experimental-agent-team-web-profile": experimental-agent-team-web-profile
-    - text: 官方 Experimental Web profile layer for Agent Teams Remote and UI plugins
-    - switch "启用 experimental-agent-team-web-profile"
+    - button "查看 智能体团队 Web 界面(实验性)": 智能体团队 Web 界面(实验性)
+    - text: 官方 在浏览器中查看团队成员、任务看板和成员会话。
+    - switch "启用 智能体团队 Web 界面(实验性)"
   - listitem:
   - listitem:
-    - button "查看 experimental-auto-review": experimental-auto-review
-    - text: 官方 Per-tool LLM authorization review for the DeepSeek Harness Auto permission preset
-    - switch "启用 experimental-auto-review"
+    - button "查看 自动授权审查(实验性)": 自动授权审查(实验性)
+    - text: 官方 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。
+    - switch "启用 自动授权审查(实验性)"
 - heading "已安装" [level=3]
 - heading "已安装" [level=3]
 - text: "1"
 - text: "1"
 - list:
 - list:

+ 9 - 9
apps/web/tests/expected/plugin-manager/manager.expected.md

@@ -8,17 +8,17 @@
 - text: "3"
 - text: "3"
 - list:
 - list:
   - listitem:
   - listitem:
-    - button "查看 experimental-agent-team-profile": experimental-agent-team-profile
-    - text: 官方 Experimental profile bundle enabling Agent Teams over dsh-base
-    - switch "启用 experimental-agent-team-profile"
+    - button "查看 智能体团队(实验性)": 智能体团队(实验性)
+    - text: 官方 启用智能体团队协作与团队工具。
+    - switch "启用 智能体团队(实验性)"
   - listitem:
   - listitem:
-    - button "查看 experimental-agent-team-web-profile": experimental-agent-team-web-profile
-    - text: 官方 Experimental Web profile layer for Agent Teams Remote and UI plugins
-    - switch "启用 experimental-agent-team-web-profile"
+    - button "查看 智能体团队 Web 界面(实验性)": 智能体团队 Web 界面(实验性)
+    - text: 官方 在浏览器中查看团队成员、任务看板和成员会话。
+    - switch "启用 智能体团队 Web 界面(实验性)"
   - listitem:
   - listitem:
-    - button "查看 experimental-auto-review": experimental-auto-review
-    - text: 官方 Per-tool LLM authorization review for the DeepSeek Harness Auto permission preset
-    - switch "启用 experimental-auto-review"
+    - button "查看 自动授权审查(实验性)": 自动授权审查(实验性)
+    - text: 官方 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。
+    - switch "启用 自动授权审查(实验性)"
 - heading "已安装" [level=3]
 - heading "已安装" [level=3]
 - text: "1"
 - text: "1"
 - list:
 - list:

+ 38 - 0
apps/web/tests/plugin-manager.e2e.ts

@@ -88,6 +88,44 @@ describe('web e2e: plugin manager', () => {
     expect(tripwire.pageErrors).toEqual([])
     expect(tripwire.pageErrors).toEqual([])
   }, 60_000)
   }, 60_000)
 
 
+  it('updates built-in names and descriptions when the UI language changes', async () => {
+    onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-locale'))
+    const panel = await openPluginsPanel()
+    await panel.getByRole('button', { name: '查看 智能体团队(实验性)' }).click()
+    const packageName = panel.locator('[data-plugin-name]')
+    expect(await packageName.textContent()).toBe('@deepseek-ai/dsh-experimental-agent-team-profile')
+    expect(await panel.getByText('启用智能体团队协作与团队工具。').count()).toBe(1)
+    try {
+      await page.getByRole('button', { name: '设置', exact: true }).click()
+      await page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '中文' }).click()
+      await page.getByRole('menuitem', { name: 'English' }).click()
+      await page.getByRole('dialog', { name: 'Settings' }).waitFor()
+      await page.keyboard.press('Escape')
+      await panel.getByRole('heading', { name: 'Experimental Agent Teams', exact: true }).waitFor()
+      expect(await packageName.textContent()).toBe('@deepseek-ai/dsh-experimental-agent-team-profile')
+      expect(await panel.getByText('Enable agent team collaboration and team tools.').count()).toBe(1)
+      await panel.getByRole('button', { name: 'Back to plugins' }).click()
+      for (const title of ['Experimental Agent Teams', 'Experimental Agent Teams Web UI', 'Experimental Auto Authorization Review']) {
+        await panel.getByRole('button', { name: `View ${title}`, exact: true }).waitFor()
+        expect(await panel.getByRole('switch', { name: `Enable ${title}`, exact: true }).count()).toBe(1)
+      }
+      expect(await panel.getByText('View team members, the task board, and teammate sessions in the browser.').count()).toBe(1)
+      expect(await panel.getByText('Add an Auto review permission mode that uses the model to assess authorization before each tool call.').count()).toBe(1)
+    } finally {
+      if (await page.locator('html').getAttribute('lang') === 'en') {
+        if (await page.getByRole('dialog', { name: 'Settings' }).count() === 0) {
+          await page.getByRole('button', { name: 'Settings', exact: true }).click()
+        }
+        await page.getByRole('dialog', { name: 'Settings' }).getByRole('button', { name: 'English' }).click()
+        await page.getByRole('menuitem', { name: '中文' }).click()
+        await page.getByRole('dialog', { name: '设置' }).waitFor()
+      }
+      await closeSettings()
+    }
+    await panel.getByRole('button', { name: '查看 智能体团队(实验性)', exact: true }).waitFor()
+    expect(tripwire.pageErrors).toEqual([])
+  }, 60_000)
+
   it('checks a spec before installing it and words what the check refused', async () => {
   it('checks a spec before installing it and words what the check refused', async () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-install'))
     onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-install'))
     const panel = await openPluginsPanel()
     const panel = await openPluginsPanel()

+ 2 - 2
packages/client/ui-plugin-manager/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-plugin-manager/README.md
 #   pnpm run verify-translation-pairing --write packages/client/ui-plugin-manager/README.md
-README.md: 313ebf7df21ea73c0c20b1c84cac6767c2079037
-README.zh.md: 81b15b459d8bea4721ed2f84607b1c737101b3b8
+README.md: 1a3ae0f0b5e6cfca4fdbb272f84ca4b641933ba6
+README.zh.md: 0d4d1aeb98fecb24a6bc3e9d0ac84389b0fe939e

+ 2 - 0
packages/client/ui-plugin-manager/README.md

@@ -27,6 +27,8 @@ Use the **Plugins** entry in the Web sidebar to manage the profile's installed b
 
 
 Select **Plugins** in the sidebar. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. **Built in** comes first and lists the bundles the installation ships for switching on, each tagged official, off until switched on and without an uninstall; **Installed** lists the bundles the profile holds. Cards are listed by name, so switching a bundle on or off does not move its card. A dependency without a bundle patch is not a plugin and is not listed unless the profile selects it, in which case it carries a problem tag. Global configuration remains in the Settings **Plugins** section.
 Select **Plugins** in the sidebar. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. **Built in** comes first and lists the bundles the installation ships for switching on, each tagged official, off until switched on and without an uninstall; **Installed** lists the bundles the profile holds. Cards are listed by name, so switching a bundle on or off does not move its card. A dependency without a bundle patch is not a plugin and is not listed unless the profile selects it, in which case it carries a problem tag. Global configuration remains in the Settings **Plugins** section.
 
 
+The built-in Agent Teams, Agent Teams Web UI, and Auto Authorization Review bundles have localized names and descriptions that follow the UI language. Their detail pages retain the full npm package name; other packages display their short package name and original description.
+
 ### Installing a bundle
 ### Installing a bundle
 
 
 **Add plugin** takes a package name with an optional version, a Git address, a tarball, or an absolute local path; the dialog says a package name is what follows `dsh plugin add` in a README. **Not sure what to enter?** under the field opens a guide that shows the three common forms with an example each; **Use example** drops one into the field. **Install** first asks the Host to read what the spec names (`pluginManager.inspect`): a name the list already shows, a name the registry does not have, a path without a package, a package without a bundle patch, or a spec pnpm would refuse comes back under the field as one sentence, with the spec kept for editing. An accepted spec opens the installing screen, which shows the package's name, one-liner, and version as the Host read them and folds pnpm's command and output behind **Show install details**. A finished install offers **Enable now**, which switches the new bundle on, closes the dialog, and scrolls the list to it; closing instead leaves it installed and off. A failed install says what went wrong in one line — the registry or network could not be reached, the package was not found, the disk is full, the profile is not writable, pnpm blocked a build script — with pnpm's output behind the details and **Retry** at hand; the Host has already put the profile files back. When pnpm blocked a dependency's install scripts, the failed screen lists the packages whose scripts wait for permission and offers **Allow these scripts and retry** in place of **Retry**; the Host saves the permission in the profile's `pnpm-workspace.yaml`, which a failed run leaves as pnpm wrote it, then runs pnpm again, and the installed screen names what was allowed. A successful installation does not certify that a module can activate.
 **Add plugin** takes a package name with an optional version, a Git address, a tarball, or an absolute local path; the dialog says a package name is what follows `dsh plugin add` in a README. **Not sure what to enter?** under the field opens a guide that shows the three common forms with an example each; **Use example** drops one into the field. **Install** first asks the Host to read what the spec names (`pluginManager.inspect`): a name the list already shows, a name the registry does not have, a path without a package, a package without a bundle patch, or a spec pnpm would refuse comes back under the field as one sentence, with the spec kept for editing. An accepted spec opens the installing screen, which shows the package's name, one-liner, and version as the Host read them and folds pnpm's command and output behind **Show install details**. A finished install offers **Enable now**, which switches the new bundle on, closes the dialog, and scrolls the list to it; closing instead leaves it installed and off. A failed install says what went wrong in one line — the registry or network could not be reached, the package was not found, the disk is full, the profile is not writable, pnpm blocked a build script — with pnpm's output behind the details and **Retry** at hand; the Host has already put the profile files back. When pnpm blocked a dependency's install scripts, the failed screen lists the packages whose scripts wait for permission and offers **Allow these scripts and retry** in place of **Retry**; the Host saves the permission in the profile's `pnpm-workspace.yaml`, which a failed run leaves as pnpm wrote it, then runs pnpm again, and the installed screen names what was allowed. A successful installation does not certify that a module can activate.

+ 2 - 0
packages/client/ui-plugin-manager/README.zh.md

@@ -27,6 +27,8 @@ kind: "package-reference"
 
 
 在侧栏选择**插件**。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**内置**排在前面,列出安装随附、供开启的组合包,每个带官方标签,开启前保持关闭,且没有卸载;**已安装**列出 profile 持有的组合包。卡片按名称排序,启停组合包不会挪动它的卡片。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**插件**分区中编辑。
 在侧栏选择**插件**。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**内置**排在前面,列出安装随附、供开启的组合包,每个带官方标签,开启前保持关闭,且没有卸载;**已安装**列出 profile 持有的组合包。卡片按名称排序,启停组合包不会挪动它的卡片。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**插件**分区中编辑。
 
 
+内置的 Agent Teams、Agent Teams Web UI 和 Auto Authorization Review 组合包使用随界面语言切换的本地化名称和描述。详情页保留完整 npm 包名;其他包显示简写包名和原始描述。
+
 ### 安装一个组合包
 ### 安装一个组合包
 
 
 **添加插件**接受包名(可带版本)、Git 地址、压缩包或本地绝对路径;对话框说明包名就是 README 里 `dsh plugin add` 后面的那一段。输入框下方的**不知道该填什么?**展开一段引导,给出三种常见形式各一个示例;**填入示例**把示例填进输入框。**安装**先让 Host 读出 spec 指向什么(`pluginManager.inspect`):列表中已有的名字、注册表没有的名字、没有包的路径、没有组合包 patch 的包,或 pnpm 会拒绝的 spec,都以一句话回到输入框下方,spec 保留可继续编辑。通过检查的 spec 打开安装中界面,展示 Host 读到的包名、一句话简介和版本,pnpm 的命令与输出折叠在**查看安装详情**之后。安装完成后提供**立即启用**:启用新组合包、关闭对话框并把列表滚动到它;直接关闭则让它保持已安装但关闭。安装失败时用一行话说明原因——注册表或网络不可达、包不存在、磁盘已满、profile 不可写、pnpm 拦下了构建脚本——pnpm 输出在详情里,**重试**就在手边;Host 已经把 profile 文件放回原样。pnpm 拦下依赖的安装脚本时,失败界面列出等待允许的包,并以**允许这些脚本并重试**取代**重试**;Host 把授权写进 profile 的 `pnpm-workspace.yaml`(失败的运行保留 pnpm 写入的这个文件)再运行 pnpm,安装完成界面会说明允许了哪些脚本。安装成功不代表模块一定能够激活。
 **添加插件**接受包名(可带版本)、Git 地址、压缩包或本地绝对路径;对话框说明包名就是 README 里 `dsh plugin add` 后面的那一段。输入框下方的**不知道该填什么?**展开一段引导,给出三种常见形式各一个示例;**填入示例**把示例填进输入框。**安装**先让 Host 读出 spec 指向什么(`pluginManager.inspect`):列表中已有的名字、注册表没有的名字、没有包的路径、没有组合包 patch 的包,或 pnpm 会拒绝的 spec,都以一句话回到输入框下方,spec 保留可继续编辑。通过检查的 spec 打开安装中界面,展示 Host 读到的包名、一句话简介和版本,pnpm 的命令与输出折叠在**查看安装详情**之后。安装完成后提供**立即启用**:启用新组合包、关闭对话框并把列表滚动到它;直接关闭则让它保持已安装但关闭。安装失败时用一行话说明原因——注册表或网络不可达、包不存在、磁盘已满、profile 不可写、pnpm 拦下了构建脚本——pnpm 输出在详情里,**重试**就在手边;Host 已经把 profile 文件放回原样。pnpm 拦下依赖的安装脚本时,失败界面列出等待允许的包,并以**允许这些脚本并重试**取代**重试**;Host 把授权写进 profile 的 `pnpm-workspace.yaml`(失败的运行保留 pnpm 写入的这个文件)再运行 pnpm,安装完成界面会说明允许了哪些脚本。安装成功不代表模块一定能够激活。

+ 6 - 6
packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx

@@ -21,7 +21,7 @@ import {
   type ConfirmState, type InstallInputError, type InstallState, type InstallSubject, type PackageRow, type PackageView,
   type ConfirmState, type InstallInputError, type InstallState, type InstallSubject, type PackageRow, type PackageView,
   type PluginManagerFace,
   type PluginManagerFace,
 } from './manager-store.ts'
 } from './manager-store.ts'
-import { managementText, noticeText, shortName, type Translate } from './presentation.ts'
+import { managementText, noticeText, packageText, type Translate } from './presentation.ts'
 import css from './PluginManagerPage.module.css'
 import css from './PluginManagerPage.module.css'
 
 
 /** Full component props assembled by the main slot renderer. */
 /** Full component props assembled by the main slot renderer. */
@@ -213,7 +213,7 @@ function PackageCard({ pkg, t, busy, highlighted, onOpen, onSetEnabled }: {
   readonly onOpen: () => void
   readonly onOpen: () => void
   readonly onSetEnabled: (enabled: boolean) => void
   readonly onSetEnabled: (enabled: boolean) => void
 }): ReactNode {
 }): ReactNode {
-  const title = shortName(pkg.name)
+  const { title, description } = packageText(pkg, t)
   const status = packageStatus(pkg)
   const status = packageStatus(pkg)
   return (
   return (
     <li
     <li
@@ -230,7 +230,7 @@ function PackageCard({ pkg, t, busy, highlighted, onOpen, onSetEnabled }: {
             {pkg.optional ? <Tag className={css.statusTag} tone="info">{t('statusOfficial')}</Tag> : null}
             {pkg.optional ? <Tag className={css.statusTag} tone="info">{t('statusOfficial')}</Tag> : null}
             {status === 'problem' ? <Tag className={css.statusTag} tone="danger">{t('statusProblem')}</Tag> : null}
             {status === 'problem' ? <Tag className={css.statusTag} tone="danger">{t('statusProblem')}</Tag> : null}
           </div>
           </div>
-          {pkg.description === undefined ? null : <span className={css.cardDesc}>{pkg.description}</span>}
+          {description === undefined ? null : <span className={css.cardDesc}>{description}</span>}
         </div>
         </div>
         <div className={css.cardEnd}>
         <div className={css.cardEnd}>
           <EnableSwitch pkg={pkg} title={title} t={t} busy={busy} onSetEnabled={onSetEnabled} />
           <EnableSwitch pkg={pkg} title={title} t={t} busy={busy} onSetEnabled={onSetEnabled} />
@@ -262,7 +262,7 @@ function PackageDetail({
   readonly onUninstall: () => void
   readonly onUninstall: () => void
   readonly onSetRowEnabled: (row: PackageRow, enabled: boolean) => void
   readonly onSetRowEnabled: (row: PackageRow, enabled: boolean) => void
 }): ReactNode {
 }): ReactNode {
-  const title = shortName(pkg.name)
+  const { title, description } = packageText(pkg, t)
   const status = packageStatus(pkg)
   const status = packageStatus(pkg)
   return (
   return (
     <div className={css.detail} data-plugin-detail={pkg.name}>
     <div className={css.detail} data-plugin-detail={pkg.name}>
@@ -299,7 +299,7 @@ function PackageDetail({
           {status === 'problem' ? <Tag className={css.statusTag} tone="danger">{t('statusProblem')}</Tag> : null}
           {status === 'problem' ? <Tag className={css.statusTag} tone="danger">{t('statusProblem')}</Tag> : null}
         </div>
         </div>
         <p className={css.detailName}><code data-plugin-name>{pkg.name}</code></p>
         <p className={css.detailName}><code data-plugin-name>{pkg.name}</code></p>
-        <p className={css.detailDesc}>{pkg.description ?? t('noDescription')}</p>
+        <p className={css.detailDesc}>{description ?? t('noDescription')}</p>
       </div>
       </div>
       {pkg.error === undefined ? null : <p className={css.reason} role="status">{t('reasonLabel')}: {managementText(pkg.error, t)}</p>}
       {pkg.error === undefined ? null : <p className={css.reason} role="status">{t('reasonLabel')}: {managementText(pkg.error, t)}</p>}
       {pkg.readOnlyReason === undefined ? null : <p className={css.reason} role="status">{managementText({ code: pkg.readOnlyReason }, t)}</p>}
       {pkg.readOnlyReason === undefined ? null : <p className={css.reason} role="status">{managementText({ code: pkg.readOnlyReason }, t)}</p>}
@@ -635,7 +635,7 @@ function ConfirmDialog({ confirm, t, onConfirm, onCancel }: {
   readonly onConfirm: () => void
   readonly onConfirm: () => void
   readonly onCancel: () => void
   readonly onCancel: () => void
 }): ReactNode {
 }): ReactNode {
-  const name = shortName(confirm.packageName)
+  const { title: name } = packageText({ name: confirm.packageName }, t)
   return (
   return (
     <Modal
     <Modal
       open
       open

+ 12 - 0
packages/client/ui-plugin-manager/src/client/locales.ts

@@ -16,6 +16,12 @@ export const zh = {
   overriddenNotice: '{name} 已保存,但被更高优先级的配置覆盖,当前未生效',
   overriddenNotice: '{name} 已保存,但被更高优先级的配置覆盖,当前未生效',
   bundlesTitle: '已安装',
   bundlesTitle: '已安装',
   builtinTitle: '内置',
   builtinTitle: '内置',
+  builtinAgentTeamTitle: '智能体团队(实验性)',
+  builtinAgentTeamDescription: '启用智能体团队协作与团队工具。',
+  builtinAgentTeamWebTitle: '智能体团队 Web 界面(实验性)',
+  builtinAgentTeamWebDescription: '在浏览器中查看团队成员、任务看板和成员会话。',
+  builtinAutoReviewTitle: '自动授权审查(实验性)',
+  builtinAutoReviewDescription: '提供自动审查权限模式,由模型在每次工具调用前判断是否授权。',
   statusProblem: '异常',
   statusProblem: '异常',
   statusOfficial: '官方',
   statusOfficial: '官方',
   reasonLabel: '原因',
   reasonLabel: '原因',
@@ -168,6 +174,12 @@ export const en = {
   overriddenNotice: '{name} was saved, but a higher-priority configuration overrides it, so it is not in effect',
   overriddenNotice: '{name} was saved, but a higher-priority configuration overrides it, so it is not in effect',
   bundlesTitle: 'Installed',
   bundlesTitle: 'Installed',
   builtinTitle: 'Built in',
   builtinTitle: 'Built in',
+  builtinAgentTeamTitle: 'Experimental Agent Teams',
+  builtinAgentTeamDescription: 'Enable agent team collaboration and team tools.',
+  builtinAgentTeamWebTitle: 'Experimental Agent Teams Web UI',
+  builtinAgentTeamWebDescription: 'View team members, the task board, and teammate sessions in the browser.',
+  builtinAutoReviewTitle: 'Experimental Auto Authorization Review',
+  builtinAutoReviewDescription: 'Add an Auto review permission mode that uses the model to assess authorization before each tool call.',
   statusProblem: 'Problem',
   statusProblem: 'Problem',
   statusOfficial: 'Official',
   statusOfficial: 'Official',
   reasonLabel: 'Reason',
   reasonLabel: 'Reason',

+ 26 - 1
packages/client/ui-plugin-manager/src/client/presentation.ts

@@ -3,11 +3,23 @@
 import type { ManagementError } from '@deepseek-ai/dsh-api-remotes/client'
 import type { ManagementError } from '@deepseek-ai/dsh-api-remotes/client'
 import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
 import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
 import type { PluginManagerLocaleKey } from './locales.ts'
 import type { PluginManagerLocaleKey } from './locales.ts'
-import type { FailedAction, ManagerNotice } from './manager-store.ts'
+import type { FailedAction, ManagerNotice, PackageView } from './manager-store.ts'
 
 
 /** The translate seat of the manager's dictionary. */
 /** The translate seat of the manager's dictionary. */
 export type Translate = PropsLocale<'pluginManager'>['t']
 export type Translate = PropsLocale<'pluginManager'>['t']
 
 
+const BUILTIN_COPY = new Map<string, { title: PluginManagerLocaleKey; description: PluginManagerLocaleKey }>([
+  ['@deepseek-ai/dsh-experimental-agent-team-profile', {
+    title: 'builtinAgentTeamTitle', description: 'builtinAgentTeamDescription',
+  }],
+  ['@deepseek-ai/dsh-experimental-agent-team-web-profile', {
+    title: 'builtinAgentTeamWebTitle', description: 'builtinAgentTeamWebDescription',
+  }],
+  ['@deepseek-ai/dsh-experimental-auto-review', {
+    title: 'builtinAutoReviewTitle', description: 'builtinAutoReviewDescription',
+  }],
+])
+
 /** The sentence each of the Host's refusal codes reads as. */
 /** The sentence each of the Host's refusal codes reads as. */
 const CODE_KEYS = {
 const CODE_KEYS = {
   'management-required': 'reasonManagementRequired',
   'management-required': 'reasonManagementRequired',
@@ -54,6 +66,19 @@ export function shortName(name: string): string {
   return unscoped.replace(/^dsh-(?:host-|client-)?/, '')
   return unscoped.replace(/^dsh-(?:host-|client-)?/, '')
 }
 }
 
 
+/**
+ * Localize known built-in packages by exact npm name at render time.
+ * @param pkg - original package identity and optional metadata description.
+ * @param t - the manager's current translate function.
+ * @returns localized copy, or the package's short name and original description.
+ */
+export function packageText(pkg: Pick<PackageView, 'name' | 'description'>, t: Translate): { title: string; description: string | undefined } {
+  const keys = BUILTIN_COPY.get(pkg.name)
+  return keys === undefined
+    ? { title: shortName(pkg.name), description: pkg.description }
+    : { title: t(keys.title), description: t(keys.description) }
+}
+
 /**
 /**
  * The sentence one notice shows.
  * The sentence one notice shows.
  * @param notice - the last action's outcome.
  * @param notice - the last action's outcome.

+ 61 - 8
packages/client/ui-plugin-manager/tests/components.client.spec.tsx

@@ -7,16 +7,18 @@ import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
 import { PluginManagerPage } from '../src/client/PluginManagerPage.tsx'
 import { PluginManagerPage } from '../src/client/PluginManagerPage.tsx'
 import type { PluginManagerPageProps } from '../src/client/PluginManagerPage.tsx'
 import type { PluginManagerPageProps } from '../src/client/PluginManagerPage.tsx'
 import { rowKey, type InstallState, type PackageRow, type PackageView, type PluginManagerState } from '../src/client/manager-store.ts'
 import { rowKey, type InstallState, type PackageRow, type PackageView, type PluginManagerState } from '../src/client/manager-store.ts'
-import { en, type PluginManagerLocaleKey } from '../src/client/locales.ts'
+import { en, zh, type PluginManagerLocaleKey } from '../src/client/locales.ts'
 
 
 afterEach(cleanup)
 afterEach(cleanup)
 
 
-const t = ((key: PluginManagerLocaleKey, params?: Record<string, string>): string =>
+const translate = (dict: typeof en): PluginManagerPageProps['t'] => ((key: PluginManagerLocaleKey, params?: Record<string, string>): string =>
   Object.entries(params ?? {}).reduce(
   Object.entries(params ?? {}).reduce(
     (text, [name, value]) => text.replaceAll(`{${name}}`, value),
     (text, [name, value]) => text.replaceAll(`{${name}}`, value),
-    en[key],
+    dict[key],
   )) as PluginManagerPageProps['t']
   )) as PluginManagerPageProps['t']
 
 
+const t = translate(en)
+
 function pkg(overrides: Partial<PackageView> = {}): PackageView {
 function pkg(overrides: Partial<PackageView> = {}): PackageView {
   return {
   return {
     name: 'dsh-better-sidebar',
     name: 'dsh-better-sidebar',
@@ -74,8 +76,13 @@ function renderTab(state: Partial<PluginManagerState> = {}) {
     ...actions,
     ...actions,
     usePluginManager: bindSnapshotSelector(store),
     usePluginManager: bindSnapshotSelector(store),
   } as unknown as PluginManagerPageProps
   } as unknown as PluginManagerPageProps
-  render(<PluginManagerPage {...props} />)
-  return { store, actions, set: (next: Partial<PluginManagerState>) => { act(() => { store.set({ ...store.getSnapshot(), ...next }) }) } }
+  const { rerender } = render(<PluginManagerPage {...props} />)
+  return {
+    store,
+    actions,
+    set: (next: Partial<PluginManagerState>) => { act(() => { store.set({ ...store.getSnapshot(), ...next }) }) },
+    setLanguage: (dict: typeof en) => { rerender(<PluginManagerPage {...props} t={translate(dict)} />) },
+  }
 }
 }
 
 
 describe('PluginManagerPage', () => {
 describe('PluginManagerPage', () => {
@@ -140,14 +147,60 @@ describe('PluginManagerPage', () => {
     const { actions } = renderTab({
     const { actions } = renderTab({
       packages: [pkg({ name: '@deepseek-ai/dsh-experimental-agent-team-profile', installed: false, optional: true, enabled: false })],
       packages: [pkg({ name: '@deepseek-ai/dsh-experimental-agent-team-profile', installed: false, optional: true, enabled: false })],
     })
     })
-    fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'experimental-agent-team-profile') }))
+    fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', en.builtinAgentTeamTitle) }))
     const detail = document.querySelector('[data-plugin-detail]') as HTMLElement
     const detail = document.querySelector('[data-plugin-detail]') as HTMLElement
     expect(within(detail).getByText(en.statusOfficial)).toBeTruthy()
     expect(within(detail).getByText(en.statusOfficial)).toBeTruthy()
-    expect(within(detail).queryByRole('button', { name: en.uninstallLabel.replace('{name}', 'experimental-agent-team-profile') })).toBeNull()
-    fireEvent.click(within(detail).getByRole('switch', { name: en.enableToggle.replace('{name}', 'experimental-agent-team-profile') }))
+    expect(within(detail).queryByRole('button', { name: en.uninstallLabel.replace('{name}', en.builtinAgentTeamTitle) })).toBeNull()
+    fireEvent.click(within(detail).getByRole('switch', { name: en.enableToggle.replace('{name}', en.builtinAgentTeamTitle) }))
     expect(actions.setEnabled).toHaveBeenCalledExactlyOnceWith('@deepseek-ai/dsh-experimental-agent-team-profile', true)
     expect(actions.setEnabled).toHaveBeenCalledExactlyOnceWith('@deepseek-ai/dsh-experimental-agent-team-profile', true)
   })
   })
 
 
+  it.each([
+    ['agent-team-profile', 'builtinAgentTeamTitle', 'builtinAgentTeamDescription'],
+    ['agent-team-web-profile', 'builtinAgentTeamWebTitle', 'builtinAgentTeamWebDescription'],
+    ['auto-review', 'builtinAutoReviewTitle', 'builtinAutoReviewDescription'],
+  ] as const)('localizes %s across cards, details, switches, and uninstall confirmation', (suffix, titleKey, descriptionKey) => {
+    const name = `@deepseek-ai/dsh-experimental-${suffix}`
+    const { actions, set, setLanguage } = renderTab({ packages: [pkg({ name, description: 'Original metadata.' })] })
+    const assertCard = (dict: typeof en) => {
+      expect(screen.getByRole('button', { name: dict.openDetail.replace('{name}', dict[titleKey]) }).textContent).toBe(dict[titleKey])
+      expect(screen.getByText(dict[descriptionKey])).toBeTruthy()
+      expect(screen.getByRole('switch', { name: dict.enableToggle.replace('{name}', dict[titleKey]) })).toBeTruthy()
+      expect(screen.queryByText('Original metadata.')).toBeNull()
+    }
+    assertCard(en)
+    setLanguage(zh)
+    assertCard(zh)
+    fireEvent.click(screen.getByRole('switch', { name: zh.enableToggle.replace('{name}', zh[titleKey]) }))
+    expect(actions.setEnabled).toHaveBeenCalledExactlyOnceWith(name, false)
+    fireEvent.click(screen.getByRole('button', { name: zh.openDetail.replace('{name}', zh[titleKey]) }))
+    for (const dict of [zh, en]) {
+      setLanguage(dict)
+      expect(screen.getByRole('heading', { level: 3 }).textContent).toBe(dict[titleKey])
+      expect(screen.getByText(dict[descriptionKey])).toBeTruthy()
+      expect(document.querySelector('[data-plugin-name]')?.textContent).toBe(name)
+      expect(screen.getByRole('switch', { name: dict.enableToggle.replace('{name}', dict[titleKey]) })).toBeTruthy()
+      expect(screen.getByRole('button', { name: dict.uninstallLabel.replace('{name}', dict[titleKey]) })).toBeTruthy()
+    }
+    fireEvent.click(screen.getByRole('button', { name: en.uninstallLabel.replace('{name}', en[titleKey]) }))
+    expect(actions.uninstall).toHaveBeenCalledExactlyOnceWith(name)
+    set({ confirm: { action: 'uninstall', packageName: name } })
+    for (const dict of [en, zh]) {
+      setLanguage(dict)
+      expect(screen.getByRole('dialog', { name: dict.confirmUninstallTitle.replace('{name}', dict[titleKey]) })).toBeTruthy()
+    }
+  })
+
+  it('preserves metadata for another scope with the same short name', () => {
+    const name = '@acme/dsh-experimental-agent-team-profile'
+    const { setLanguage } = renderTab({ packages: [pkg({ name, description: 'Third-party description.' })] })
+    setLanguage(zh)
+    fireEvent.click(screen.getByRole('button', { name: zh.openDetail.replace('{name}', 'experimental-agent-team-profile') }))
+    expect(screen.getByRole('heading', { level: 3 }).textContent).toBe('experimental-agent-team-profile')
+    expect(screen.getByText('Third-party description.')).toBeTruthy()
+    expect(document.querySelector('[data-plugin-name]')?.textContent).toBe(name)
+  })
+
   it('opens a guide under the field and drops an example into it', () => {
   it('opens a guide under the field and drops an example into it', () => {
     const { actions } = renderTab({ install: { ...IDLE_INSTALL, open: true } })
     const { actions } = renderTab({ install: { ...IDLE_INSTALL, open: true } })
     expect(screen.queryByText(en.installGuideIntro)).toBeNull()
     expect(screen.queryByText(en.installGuideIntro)).toBeNull()