From 8769d57c984e9f2389f0fa7fa80d4ab1700b2259 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:16:34 +0800 Subject: [PATCH] fix(web): localize permission preset labels --- apps/web/tests/access-confirmation.e2e.ts | 10 ++-- .../access-confirmation/ui.expected.md | 8 +-- .../settings-chrome/dialog.expected.md | 4 +- apps/web/tests/settings-chrome.e2e.ts | 18 +++--- .../ui-conversation/src/client/locales.ts | 14 +++-- .../src/client/skeleton/PermissionSelect.tsx | 58 ++++++++++++------- .../tests/input-bar.client.spec.tsx | 53 +++++++++++------ .../src/client/PermissionRow.tsx | 9 +-- .../ui-permission-presets/src/client/index.ts | 8 ++- .../src/client/locales.ts | 24 ++++++-- .../src/client/presentation.ts | 35 ++++++++++- .../tests/browser-plugin.client.spec.ts | 28 +++++++-- .../permission-presets-row.client.spec.tsx | 38 ++++++------ 13 files changed, 207 insertions(+), 100 deletions(-) diff --git a/apps/web/tests/access-confirmation.e2e.ts b/apps/web/tests/access-confirmation.e2e.ts index 26aa2d3d3d..e5595c94a8 100644 --- a/apps/web/tests/access-confirmation.e2e.ts +++ b/apps/web/tests/access-confirmation.e2e.ts @@ -50,13 +50,13 @@ describe('web e2e: Full access confirmation', () => { const access = page.locator('button[aria-label^="访问模式"]').first() await access.waitFor({ timeout: 10_000 }) - expect(await access.getAttribute('aria-label')).toBe('访问模式,当前:Workspace Write') + expect(await access.getAttribute('aria-label')).toBe('访问模式,当前:可写入工作区') await access.click() - await page.getByRole('menuitem', { name: 'Full access' }).click() - const dialog = page.getByRole('dialog', { name: '确认启用 Full access?' }) + await page.getByRole('menuitem', { name: '完全权限' }).click() + const dialog = page.getByRole('dialog', { name: '确认启用完全权限?' }) await dialog.waitFor({ timeout: 10_000 }) - const enable = dialog.getByRole('button', { name: '启用 Full access' }) + const enable = dialog.getByRole('button', { name: '启用完全权限' }) expect(await enable.isDisabled()).toBe(true) // The modal is in this page's body (not a native/new window) and escapes @@ -69,7 +69,7 @@ describe('web e2e: Full access confirmation', () => { expect(await enable.isEnabled()).toBe(true) await enable.click() await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 }) - .toBe('访问模式,当前:Full access') + .toBe('访问模式,当前:完全权限') expect(await dialog.count()).toBe(0) expect(tripwire.pageErrors).toEqual([]) }, 60_000) diff --git a/apps/web/tests/expected/access-confirmation/ui.expected.md b/apps/web/tests/expected/access-confirmation/ui.expected.md index 7852dffc5a..d554a96a72 100644 --- a/apps/web/tests/expected/access-confirmation/ui.expected.md +++ b/apps/web/tests/expected/access-confirmation/ui.expected.md @@ -1,10 +1,10 @@ -- dialog "确认启用 Full access?": - - heading "确认启用 Full access?" [level=2] +- dialog "确认启用完全权限?": + - heading "确认启用完全权限?" [level=2] - button "关闭": - img - img - - paragraph: 启用 Full access 后,agent 将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。 + - paragraph: 启用完全权限后,智能体将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。 - checkbox "我已了解风险,并愿意继续" - text: 我已了解风险,并愿意继续 - button "取消" - - button "启用 Full access" [disabled] + - button "启用完全权限" [disabled] diff --git a/apps/web/tests/expected/settings-chrome/dialog.expected.md b/apps/web/tests/expected/settings-chrome/dialog.expected.md index 369dc19e71..9ff9301454 100644 --- a/apps/web/tests/expected/settings-chrome/dialog.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog.expected.md @@ -18,8 +18,8 @@ - img - text: 关闭 - text: 权限 选择新会话的默认权限模式 - - button "Workspace Write": - - text: Workspace Write + - button "可写入工作区": + - text: 可写入工作区 - img - text: 语言 - button "中文": diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts index bb13a0fa5e..4b8fd7495f 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/apps/web/tests/settings-chrome.e2e.ts @@ -62,7 +62,7 @@ describe('web e2e: settings modal and General preferences', () => { expect(await trigger.getAttribute('aria-expanded')).toBe('true') // General is active by default; Permission, Language and Appearance are functional. expect(await dialog.getByRole('button', { name: '通用设置' }).getAttribute('aria-current')).toBe('true') - await dialog.getByRole('button', { name: 'Workspace Write' }).waitFor({ timeout: 10_000 }) + await dialog.getByRole('button', { name: '可写入工作区' }).waitFor({ timeout: 10_000 }) await expect.poll(() => dialog.getByText('语言', { exact: true }).count(), { timeout: 5_000 }).toBe(1) await expect.poll(() => dialog.getByText('外观', { exact: true }).count(), { timeout: 5_000 }).toBe(1) const openDocument = dialog.getByRole('button', { name: '打开配置文件' }) @@ -150,12 +150,12 @@ describe('web e2e: settings modal and General preferences', () => { await page.getByRole('button', { name: '设置', exact: true }).click() const dialog = page.getByRole('dialog', { name: '设置' }) await dialog.waitFor({ timeout: 10_000 }) - const selector = dialog.getByRole('button', { name: 'Workspace Write' }) + const selector = dialog.getByRole('button', { name: '可写入工作区' }) await selector.waitFor({ timeout: 10_000 }) await expect.poll(() => selector.isEnabled(), { timeout: 5_000 }).toBe(true) await selector.click() - await page.getByRole('menuitem', { name: 'Read Only' }).click() - await dialog.getByRole('button', { name: 'Read Only' }).waitFor({ timeout: 10_000 }) + await page.getByRole('menuitem', { name: '仅可查看' }).click() + await dialog.getByRole('button', { name: '仅可查看' }).waitFor({ timeout: 10_000 }) const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8') expect(document).toContain('permission:') @@ -170,14 +170,14 @@ describe('web e2e: settings modal and General preferences', () => { ['approval/policy', { policy: 'ask' }], ]) - await dialog.getByRole('button', { name: 'Read Only' }).click() - await page.getByRole('menuitem', { name: 'Full access' }).click() - const confirmation = page.getByRole('dialog', { name: '确认启用 Full access?' }) - const enable = confirmation.getByRole('button', { name: '启用 Full access' }) + await dialog.getByRole('button', { name: '仅可查看' }).click() + await page.getByRole('menuitem', { name: '完全权限' }).click() + const confirmation = page.getByRole('dialog', { name: '确认启用完全权限?' }) + const enable = confirmation.getByRole('button', { name: '启用完全权限' }) expect(await enable.isDisabled()).toBe(true) await confirmation.getByRole('checkbox').click() await enable.click() - await dialog.getByRole('button', { name: 'Full access' }).waitFor({ timeout: 10_000 }) + await dialog.getByRole('button', { name: '完全权限' }).waitFor({ timeout: 10_000 }) const confirmedDocument = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8') expect(confirmedDocument).toContain('defaultPreset: danger-full-access') const confirmed = scaffold.ctx.sessions.create(SessionId('settings-permission-confirmed')) diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index 9eaf2a82a0..3b1f9c1fc8 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -56,12 +56,14 @@ export const zh = { 'settings.enter.description': '仅在智能体运行时生效;Cmd/Ctrl+Enter 使用另一行为', 'settings.enter.queue': '排队发送', 'settings.enter.steer': '插话发送', - 'access.confirm.title': '确认启用 Full access?', - 'access.confirm.description': '启用 Full access 后,agent 将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。', + 'access.preset.readOnly': '仅可查看', + 'access.preset.workspaceWrite': '可写入工作区', + 'access.preset.fullAccess': '完全权限', + 'access.confirm.title': '确认启用完全权限?', + 'access.confirm.description': '启用完全权限后,智能体将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。', 'access.confirm.acknowledge': '我已了解风险,并愿意继续', 'access.confirm.cancel': '取消', - 'access.confirm.enable': '启用 Full access', - 'access.fullLabel': 'Full access', + 'access.confirm.enable': '启用完全权限', 'hero.headline': '探索未至之境', 'hero.preview': '预览版', 'hero.chooseWorkspace': '选择工作区', @@ -204,12 +206,14 @@ export const en = { 'settings.enter.description': 'Busy only; Cmd/Ctrl+Enter uses the other behavior', 'settings.enter.queue': 'Queue', 'settings.enter.steer': 'Steer', + 'access.preset.readOnly': 'Read Only', + 'access.preset.workspaceWrite': 'Workspace Write', + 'access.preset.fullAccess': 'Full access', 'access.confirm.title': 'Enable Full access?', 'access.confirm.description': 'Full access reduces confirmation steps and lets the agent perform more actions directly, including sensitive operations, file changes, or external commands. Only use it when you trust the current task.', 'access.confirm.acknowledge': 'I understand the risks and want to continue', 'access.confirm.cancel': 'Cancel', 'access.confirm.enable': 'Enable Full access', - 'access.fullLabel': 'Full access', 'hero.headline': 'Into the Unknown', 'hero.preview': 'Preview', 'hero.chooseWorkspace': 'Choose workspace', diff --git a/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx b/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx index 249031f4c0..b54049efef 100644 --- a/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx @@ -5,6 +5,7 @@ import type { PermissionSelect as PermissionSelectValue } from '@deepseek-ai/dsh import { IconChevronDownOutline14, Menu, RiskConfirmation } from '@deepseek-ai/dsh-client-ui-primitives' import type { MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives' import type { ComposerBarProps } from '../contract/slots.ts' +import { en } from '../locales.ts' import css from './PermissionSelect.module.css' const FULL_ACCESS = 'danger-full-access' @@ -15,14 +16,14 @@ const FULL_ACCESS = 'danger-full-access' const shieldOutline = 'M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z' -const permissionGlyphs = { - 'read-only': ( +const permissionGlyphs = new Map([ + ['read-only', ( - ), - 'workspace-write': ( + )], + ['workspace-write', ( @@ -30,38 +31,48 @@ const permissionGlyphs = { - ), - [FULL_ACCESS]: ( + )], + [FULL_ACCESS, ( - ), -} as Record + )], +]) /** Glyph for a permission option value; host-configured names outside the design set get none. */ function permissionGlyph(value: string): ReactNode | undefined { - return permissionGlyphs[value] + return permissionGlyphs.get(value) } /** - * Display transform: kebab-case machine names render as title-case labels - * (`workspace-write` → `Workspace Write`); non-kebab host-configured names - * pass through. Full access intentionally overrides the machine-name - * transform so both permission surfaces use the product label `Full access`; - * the warning body remains locale-aware. + * Display transform: built-in machine names render as locale product labels; + * non-kebab host-configured names pass through. */ function displayName(name: string): string { if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) return name return name.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') } -function optionLabel( - option: PermissionSelectValue['options'][number], +const BUILT_IN_PERMISSION_NAMES = new Map([ + ['read-only', en['access.preset.readOnly']], + ['workspace-write', en['access.preset.workspaceWrite']], + [FULL_ACCESS, en['access.preset.fullAccess']], +]) + +function permissionLabel( + value: string, + name: string, t: ComposerBarProps['t'], ): string { - return option.value === FULL_ACCESS ? t('access.fullLabel') : displayName(option.name) + const builtInName = BUILT_IN_PERMISSION_NAMES.get(value) + if (builtInName !== undefined && (name === value || name === builtInName)) { + if (value === 'read-only') return t('access.preset.readOnly') + if (value === 'workspace-write') return t('access.preset.workspaceWrite') + if (value === FULL_ACCESS) return t('access.preset.fullAccess') + } + return displayName(name) } export interface PermissionSelectProps { @@ -89,13 +100,20 @@ export function PermissionSelect({ value, locked, command, t }: PermissionSelect const currentValue = pick ?? value.currentValue const current = value.options.find(option => option.value === currentValue) + const currentLabel = current === undefined + ? permissionLabel(currentValue, currentValue, t) + : permissionLabel(current.value, current.name, t) const busy = pick !== null || confirmation !== null const items: MenuEntry[] = value.options .filter(o => o.value !== 'custom') .map((option) => { const icon = permissionGlyph(option.value) - return { id: option.value, label: optionLabel(option, t), ...icon === undefined ? {} : { icon } } + return { + id: option.value, + label: permissionLabel(option.value, option.name, t), + ...icon === undefined ? {} : { icon }, + } }) const submit = (id: string): void => { @@ -141,7 +159,7 @@ export function PermissionSelect({ value, locked, command, t }: PermissionSelect