fix(web): localize Chinese model copy
This commit is contained in:
1 parent
8fda2f620a
commit
555c04bdfc
16 files changed
+92
-35
No files matched your search
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md
|
||||
2026-08-23-locale-owned-client-ui-copy.md: 623189b8faa45f0ef2a1b724d3e050d33f7d59bd
|
||||
2026-08-23-locale-owned-client-ui-copy.zh.md: e2a6f49de89e6e6ba2734dd69cf536a3e13d2aea
|
||||
2026-08-23-locale-owned-client-ui-copy.md: bc4d8efbce1f246f712e3afe6af40d1518e8cb17
|
||||
2026-08-23-locale-owned-client-ui-copy.zh.md: ce996a4aafc392e739bec2eccd8714cfeff7d8e9
|
||||
@@ -12,6 +12,8 @@ Typed locale namespaces and bilingual dictionary parity proved that registered d
|
||||
|
||||
**Locale dictionaries own all product-authored client UI wording.** Visible text, accessibility names, tooltips, placeholders, empty states, status labels, units, and formatting templates reach presentation through a typed `t` seat or an already-localized prop. A value authored by a user, model, provider, plugin, wire peer, or operating system remains data and renders verbatim; protocol tags, tool names, paths, URLs, JSON/JavaScript literals, and stable internal ids are not translated.
|
||||
|
||||
Product-owned catalog descriptions follow the same rule. The client maps an exact built-in provider, model, and description to a locale key; a changed description or an external provider description remains provider data and renders verbatim.
|
||||
|
||||
**Cordis-free primitives require complete localized copy props and own no language fallback.** `MarkdownText`, `JsonTree`, `TerminalBlock`, `DiffBlock`, `ReadBlock`, `SearchBlock`, `WebBlock`, `CodeBlock`, `JsonBlock`, `HoverCard`, and `ConnectionIndicator` receive their chrome from the feature render site. This preserves the primitive package's runtime independence while making omission a type error instead of silently selecting Chinese or English. Shared words live in the `common` namespace; feature-specific phrases stay with the feature that decides their meaning.
|
||||
|
||||
**Localized display text is never an identity.** Models and stores retain discriminants, stable ids, and non-display markers. Renderers translate after matching, and request maps carry stable group membership into the trajectory ledger. A client-synthesized error that must survive in a view model uses a stable marker and is translated only when displayed. Language switching therefore changes wording without changing selection, grouping, search identity, or lifecycle state.
|
||||
@@ -22,7 +24,7 @@ The product-authored error and design-literal exclusions, primitive defaults, an
|
||||
|
||||
## Verification
|
||||
|
||||
The AST check's own Vitest spec pins direct JSX, template branches, semantic copy props, label data, formatter returns, locale-key calls, structural attributes, and dictionary owners. Locale dictionary parity pins identical `zh`/`en` keys. Client component suites exercise both direct translated seats and locale-prop adapters, and the assembled web replay plus the required real-server GIF demonstrate the shipped locale switch on the actual trajectory surface.
|
||||
The AST check's own Vitest spec pins direct JSX, template branches, semantic copy props, label data, formatter returns, locale-key calls, structural attributes, and dictionary owners. Locale dictionary parity pins identical `zh`/`en` keys. Client suites exercise direct translated seats, locale-prop adapters, and built-in catalog-description localization without altering external descriptions. The assembled web replay plus the required real-server GIF demonstrate the shipped locale switch on the actual trajectory surface.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ typed locale namespace 与双语字典对等性可以证明已注册字典完整
|
||||
|
||||
**所有产品编写的 client UI 措辞都由 locale 字典持有。** 可见文本、无障碍名称、tooltip、placeholder、空状态、状态标签、单位和格式模板必须经 typed `t` 席位或已本地化 prop 到达展示层。由用户、模型、提供方、插件、wire 对端或操作系统编写的值仍是数据并原样渲染;协议 tag、工具名称、路径、URL、JSON/JavaScript 字面量和稳定内部 id 不翻译。
|
||||
|
||||
产品持有的目录说明遵循同一规则。client 将完全匹配的内置提供方、模型与说明映射到 locale key;说明发生变化或来自外部提供方时,它仍是提供方数据并原样渲染。
|
||||
|
||||
**Cordis-free 原子组件要求完整的本地化文案 prop,且自身不持有语言回落值。** `MarkdownText`、`JsonTree`、`TerminalBlock`、`DiffBlock`、`ReadBlock`、`SearchBlock`、`WebBlock`、`CodeBlock`、`JsonBlock`、`HoverCard` 与 `ConnectionIndicator` 的 chrome 均由功能渲染点传入。这样既保留原子组件包的运行时独立性,也让遗漏成为类型错误,而不是静默选择中文或英文。共享用词进入 `common` namespace;功能专属短语留在决定其语义的功能侧。
|
||||
|
||||
**本地化展示文本绝不承担身份。** 模型与存储保留判别字段、稳定 id 和非展示 marker。渲染器先匹配再翻译,请求映射通过稳定的组成员关系进入 trajectory ledger。必须保存在视图模型中的 client 合成错误使用稳定 marker,只在展示时翻译。因此语言切换只改变措辞,不改变选择、分组、搜索身份或生命周期状态。
|
||||
@@ -22,7 +24,7 @@ typed locale namespace 与双语字典对等性可以证明已注册字典完整
|
||||
|
||||
## Verification
|
||||
|
||||
AST 检查自身的 Vitest spec 固定直接 JSX、模板分支、语义文案 prop、label 数据、格式化函数返回值、locale key 调用、结构属性和字典 owner。locale 字典对等性固定 `zh`/`en` key 一致。client 组件测试同时覆盖直接翻译席位与 locale prop 适配器;组装 web 回放和规定的真实服务器 GIF 在实际 trajectory 界面上展示发布的语言切换。
|
||||
AST 检查自身的 Vitest spec 固定直接 JSX、模板分支、语义文案 prop、label 数据、格式化函数返回值、locale key 调用、结构属性和字典 owner。locale 字典对等性固定 `zh`/`en` key 一致。client 测试覆盖直接翻译席位、locale prop 适配器,以及不改变外部说明的内置目录说明本地化。组装 web 回放和规定的真实服务器 GIF 在实际 trajectory 界面上展示发布的语言切换。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
- button "减小字号":
|
||||
- img
|
||||
- text: px 对话显示 控制已完成轮次的过程内容
|
||||
- button "Compact":
|
||||
- text: Compact
|
||||
- button "紧凑":
|
||||
- text: 紧凑
|
||||
- img
|
||||
- text: 繁忙时的发送行为 智能体运行时 Enter 键和发送按钮的行为;Cmd/Ctrl+Enter 使用另一行为
|
||||
- button "排队发送":
|
||||
|
||||
@@ -424,9 +424,9 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByText('对话显示', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: 'Compact', exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: 'Normal', exact: true }).click()
|
||||
await dialog.getByRole('button', { name: 'Normal', exact: true }).waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: '紧凑', exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: '标准', exact: true }).click()
|
||||
await dialog.getByRole('button', { name: '标准', exact: true }).waitFor({ timeout: 10_000 })
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/ui-chat:\n\s+transcriptView: normal/)
|
||||
await page.keyboard.press('Escape')
|
||||
@@ -437,11 +437,11 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const reloaded = page.getByRole('dialog', { name: '设置' })
|
||||
await reloaded.getByRole('button', { name: 'Normal', exact: true }).waitFor({ timeout: 10_000 })
|
||||
await reloaded.getByRole('button', { name: '标准', exact: true }).waitFor({ timeout: 10_000 })
|
||||
|
||||
await reloaded.getByRole('button', { name: 'Normal', exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: 'Compact', exact: true }).click()
|
||||
await reloaded.getByRole('button', { name: 'Compact', exact: true }).waitFor({ timeout: 10_000 })
|
||||
await reloaded.getByRole('button', { name: '标准', exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: '紧凑', exact: true }).click()
|
||||
await reloaded.getByRole('button', { name: '紧凑', exact: true }).waitFor({ timeout: 10_000 })
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/ui-chat:\n\s+transcriptView: compact/)
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-chat/README.md
|
||||
README.md: 91618e226dd25c8629ff103ac5a958e688aeed26
|
||||
README.zh.md: 4f9629c8974fbee9e56ed003ebd57bbc3b029577
|
||||
README.md: 62b6a8c8644953010ba61dc19780c5e8a5a01991
|
||||
README.zh.md: 9aa8cec85bde83130ecf06806f9831d491f09ea5
|
||||
@@ -39,7 +39,7 @@ A completed Turn shows an expandable usage row only when the loaded window inclu
|
||||
<a id="turn-process-folding"></a>
|
||||
## Turn Process Folding
|
||||
|
||||
Settings → General exposes a persisted `Normal` / `Compact` conversation-display preference in the `ui-chat` namespace; `Compact` is the default. Normal leaves process rows visible and renders no Turn-process control. In Compact mode, the System prompt remains independently visible before the opening User throughout the Turn. Context injection, reasoning, Assistant material, Tool rows, and Retry rows remain expanded while a Turn is open. At `turn/end`, its latest Step becomes the final-answer boundary only when it contains non-blank text, an image, or an unknown visible block—and no Tool-call block; preceding Context injection, reasoning, earlier Assistant material, Tool rows, and Retry rows then collapse by default. The control reports Turn-wide durable counts for non-subagent Tool calls, reply-bearing Assistant messages before the final answer, and subagent delegation calls; zero-valued segments are omitted, the Tool and subagent figures are mutually exclusive, and neither System prompt nor Context injection contributes a count. When all three counts are zero, the process still folds and the control reads `Thought for a while`. A full-width divider below the summary separates it from the answer or expanded process rows. User and steering messages, System prompt, error, max-token, and turn-tail rows stay outside, and a closed Turn with no final answer keeps all process evidence visible. A newly available process control is inserted without changing the relative order of existing rows: opening human input precedes the control and process rows from their first projection, while System prompt remains above that input. While older history remains available through Load earlier, process controls stay absent and no members are hidden; once history is complete, every eligible closed Turn uses the collapsed default immediately. Stable Chat Node Seats keep every renderer mounted, hidden members add no flow spacing, and a closed control sits 8px above its answer only when no independent input intervenes. Completion collapse does not depend on tail-follow position, so a reader above the tail may see the transcript reflow. An automatic collapse that would hide keyboard focus keeps the group open and leaves focus in place; a manual close focuses the process control before hiding its members. The session-scoped store records only manually expanded Turn-and-answer-Step generations; a different answer generation starts collapsed.
|
||||
Settings → General exposes a persisted, localized `Normal` / `Compact` conversation-display preference in the `ui-chat` namespace; `Compact` is the default. Normal leaves process rows visible and renders no Turn-process control. In Compact mode, the System prompt remains independently visible before the opening User throughout the Turn. Context injection, reasoning, Assistant material, Tool rows, and Retry rows remain expanded while a Turn is open. At `turn/end`, its latest Step becomes the final-answer boundary only when it contains non-blank text, an image, or an unknown visible block—and no Tool-call block; preceding Context injection, reasoning, earlier Assistant material, Tool rows, and Retry rows then collapse by default. The control reports Turn-wide durable counts for non-subagent Tool calls, reply-bearing Assistant messages before the final answer, and subagent delegation calls; zero-valued segments are omitted, the Tool and subagent figures are mutually exclusive, and neither System prompt nor Context injection contributes a count. When all three counts are zero, the process still folds and the control reads `Thought for a while`. A full-width divider below the summary separates it from the answer or expanded process rows. User and steering messages, System prompt, error, max-token, and turn-tail rows stay outside, and a closed Turn with no final answer keeps all process evidence visible. A newly available process control is inserted without changing the relative order of existing rows: opening human input precedes the control and process rows from their first projection, while System prompt remains above that input. While older history remains available through Load earlier, process controls stay absent and no members are hidden; once history is complete, every eligible closed Turn uses the collapsed default immediately. Stable Chat Node Seats keep every renderer mounted, hidden members add no flow spacing, and a closed control sits 8px above its answer only when no independent input intervenes. Completion collapse does not depend on tail-follow position, so a reader above the tail may see the transcript reflow. An automatic collapse that would hide keyboard focus keeps the group open and leaves focus in place; a manual close focuses the process control before hiding its members. The session-scoped store records only manually expanded Turn-and-answer-Step generations; a different answer generation starts collapsed.
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ Chat 会为非空的初始请求、显式消息序列起点、真实 system 字
|
||||
<a id="turn-process-folding"></a>
|
||||
## 轮次过程折叠
|
||||
|
||||
「设置 → 通用设置」提供持久化到 `ui-chat` 命名空间的 `Normal` / `Compact` 对话显示偏好,默认使用 `Compact`。Normal 保持所有过程行可见且不渲染轮次过程控件。Compact 模式下,系统提示词在整个轮次中始终独立显示于开场 User 上方。轮次打开期间,上下文注入、推理、Assistant 内容、工具行与重试行始终展开。到 `turn/end` 时,最后一个步骤只有在包含非空文本、图片或未知可见块且不含工具调用块时才成为最终正文边界;边界之前的上下文注入、推理、较早 Assistant 内容、工具行与重试行随后默认收起。控件展示覆盖整个轮次的非 subagent 工具调用数、最终正文之前带回复内容的 Assistant 消息数和 subagent 委派数;值为 0 的分段省略,工具调用与 subagent 两项互斥,系统提示词与上下文注入都不增加计数。三项全为 0 时过程仍会收起,控件标题显示「已思考」(英文为 `Thought for a while`)。摘要下方的通栏分隔线将其与正文或展开后的过程行隔开。用户与 steering 消息、系统提示词、错误、最大 token 与 turn-tail 行留在过程组外;关闭时没有最终正文的轮次保留全部过程证据。新的过程控件插入时不会改变既有行的相对顺序:开场人工输入从首次投影起便位于控件和过程行之前,系统提示词则始终位于该输入上方。只要仍可通过「加载更早」获取历史,过程控件就不出现,也不会隐藏任何成员;历史加载完整后,每个合格的已关闭轮次立即使用默认收起状态。稳定 Chat Node Seat 会让每个 renderer 保持挂载,隐藏成员不产生消息流间距;只有中间没有独立输入时,收起控件才与正文相隔 8px。完成后的收起不依赖是否跟随尾部,因此正在上方阅读的用户可能看到 transcript 高度变化。若自动收起会隐藏当前键盘焦点,则过程组保持展开且焦点留在原处;手动收起会先把焦点移到过程控件,再隐藏成员。会话作用域 store 只记录用户手动展开的「轮次 + 正文步骤」generation;不同正文 generation 默认收起。
|
||||
「设置 → 通用设置」提供持久化到 `ui-chat` 命名空间的「标准」/「紧凑」对话显示偏好,默认使用「紧凑」。标准模式保持所有过程行可见且不渲染轮次过程控件。紧凑模式下,系统提示词在整个轮次中始终独立显示于开场 User 上方。轮次打开期间,上下文注入、推理、Assistant 内容、工具行与重试行始终展开。到 `turn/end` 时,最后一个步骤只有在包含非空文本、图片或未知可见块且不含工具调用块时才成为最终正文边界;边界之前的上下文注入、推理、较早 Assistant 内容、工具行与重试行随后默认收起。控件展示覆盖整个轮次的非 subagent 工具调用数、最终正文之前带回复内容的 Assistant 消息数和 subagent 委派数;值为 0 的分段省略,工具调用与 subagent 两项互斥,系统提示词与上下文注入都不增加计数。三项全为 0 时过程仍会收起,控件标题显示「已思考」(英文为 `Thought for a while`)。摘要下方的通栏分隔线将其与正文或展开后的过程行隔开。用户与 steering 消息、系统提示词、错误、最大 token 与 turn-tail 行留在过程组外;关闭时没有最终正文的轮次保留全部过程证据。新的过程控件插入时不会改变既有行的相对顺序:开场人工输入从首次投影起便位于控件和过程行之前,系统提示词则始终位于该输入上方。只要仍可通过「加载更早」获取历史,过程控件就不出现,也不会隐藏任何成员;历史加载完整后,每个合格的已关闭轮次立即使用默认收起状态。稳定 Chat Node Seat 会让每个 renderer 保持挂载,隐藏成员不产生消息流间距;只有中间没有独立输入时,收起控件才与正文相隔 8px。完成后的收起不依赖是否跟随尾部,因此正在上方阅读的用户可能看到 transcript 高度变化。若自动收起会隐藏当前键盘焦点,则过程组保持展开且焦点留在原处;手动收起会先把焦点移到过程控件,再隐藏成员。会话作用域 store 只记录用户手动展开的「轮次 + 正文步骤」generation;不同正文 generation 默认收起。
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ export const zh = {
|
||||
'chat.turnNavigation.turn': '第 {turn} 轮',
|
||||
'settings.transcript.title': '对话显示',
|
||||
'settings.transcript.description': '控制已完成轮次的过程内容',
|
||||
'settings.transcript.normal': 'Normal',
|
||||
'settings.transcript.compact': 'Compact',
|
||||
'settings.transcript.normal': '标准',
|
||||
'settings.transcript.compact': '紧凑',
|
||||
'fileOpen.title': '无法打开文件',
|
||||
'fileOpen.unknown': '无法打开此文件',
|
||||
'message.extraBlock': '附加内容块',
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { GlobalStandardProps } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import { bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { TranscriptViewRow, type TranscriptViewRowProps } from '../src/client/settings/TranscriptViewRow.tsx'
|
||||
import { en } from '../src/client/locale.ts'
|
||||
import { en, zh } from '../src/client/locale.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -31,7 +31,7 @@ function noPendingInteraction() {
|
||||
// The resource hook the resources plugin merges into GlobalStandardProps; this row reads no address.
|
||||
const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource']
|
||||
|
||||
function mount(mode: 'normal' | 'compact' = 'compact') {
|
||||
function mount(mode: 'normal' | 'compact' = 'compact', dictionary: typeof en | typeof zh = en) {
|
||||
const source = createSnapshotStore(mode)
|
||||
const setTranscriptView = vi.fn((next: 'normal' | 'compact') => { source.set(next) })
|
||||
const props: TranscriptViewRowProps = {
|
||||
@@ -41,7 +41,7 @@ function mount(mode: 'normal' | 'compact' = 'compact') {
|
||||
useResource,
|
||||
useTranscriptView: bindSnapshotSelector(source),
|
||||
setTranscriptView,
|
||||
t: makeTranslate(en),
|
||||
t: makeTranslate(dictionary),
|
||||
}
|
||||
render(<TranscriptViewRow {...props} />)
|
||||
return { setTranscriptView }
|
||||
@@ -66,4 +66,11 @@ describe('TranscriptViewRow', () => {
|
||||
fireEvent.pointerDown(document.body)
|
||||
expect(screen.queryByRole('menuitem', { name: 'Compact' })).toBeNull()
|
||||
})
|
||||
|
||||
it('shows the conversation-display values in Chinese', () => {
|
||||
mount('compact', zh)
|
||||
fireEvent.click(screen.getByRole('button', { name: '紧凑' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '标准' }))
|
||||
expect(screen.getByRole('button', { name: '标准' })).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-model-selection/README.md
|
||||
README.md: b050f1fa7e680b7669e208d88851754aa72f09e2
|
||||
README.zh.md: 85b4d44a1beb45a5a11fa946d2f36d83f9201e29
|
||||
README.md: ae939ce678bcc50aa69c6c260f1782830eea103f
|
||||
README.zh.md: d9826f55a48f141f8baa65f230e24ea8d0254e30
|
||||
@@ -29,7 +29,7 @@ Mount this plugin alongside `ui-conversation` and the commands package; the comp
|
||||
|
||||
### Model and effort
|
||||
|
||||
Models stay grouped by provider. The menu shows model and effort names only; catalog descriptions remain available to other consumers. The `/model` popup applies the selected model's default effort; the composer can then choose any advertised effort. An adapter without reasoning metadata leaves the Effort row absent; there is no arbitrary effort input.
|
||||
Models stay grouped by provider. The composer menu shows model and effort names only. The `/model` popup shows provider names and catalog descriptions; it localizes the two built-in DeepSeek descriptions and leaves external provider descriptions verbatim. The popup applies the selected model's default effort; the composer can then choose any advertised effort. An adapter without reasoning metadata leaves the Effort row absent; there is no arbitrary effort input.
|
||||
|
||||
### Unroutable sessions
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
|
||||
|
||||
### 模型与推理强度
|
||||
|
||||
模型按提供方分组。菜单只显示模型与推理强度名称;目录中的说明仍可供其他消费方使用。`/model` 弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。
|
||||
模型按提供方分组。composer 菜单只显示模型与推理强度名称。`/model` 弹窗显示提供方名称与目录说明;其中两个内置 DeepSeek 模型的说明使用当前语言,外部提供方说明保持原文。弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。
|
||||
|
||||
### 不可路由的会话
|
||||
|
||||
|
||||
@@ -46,15 +46,30 @@ function rowId(providerId: string, modelId: string): string {
|
||||
return `${providerId}/${modelId}`
|
||||
}
|
||||
|
||||
const BUILTIN_DESCRIPTION_KEYS: Readonly<Record<string, ModelKey>> = {
|
||||
'deepseek-official/deepseek-v4-flash': 'option.deepseekV4Flash.description',
|
||||
'deepseek-official/deepseek-v4-pro': 'option.deepseekV4Pro.description',
|
||||
}
|
||||
|
||||
function descriptionOf(
|
||||
providerId: string,
|
||||
model: ModelDirectoryState['groups'][number]['models'][number],
|
||||
t: TranslateNS<'model'>,
|
||||
): string | undefined {
|
||||
const key = BUILTIN_DESCRIPTION_KEYS[rowId(providerId, model.id)]
|
||||
return key !== undefined && model.description === en[key] ? t(key) : model.description
|
||||
}
|
||||
|
||||
/** Flatten the directory into popup rows; failure rows are listed for visibility but never selectable. */
|
||||
function optionsOf(directory: ModelDirectoryState, t: TranslateNS<'model'>): SelectOption[] {
|
||||
const rows: SelectOption[] = []
|
||||
for (const group of directory.groups) {
|
||||
for (const model of group.models) {
|
||||
const description = descriptionOf(group.id, model, t)
|
||||
rows.push({
|
||||
id: rowId(group.id, model.id),
|
||||
label: model.name,
|
||||
detail: model.description !== undefined ? `${group.name} · ${model.description}` : group.name,
|
||||
detail: description !== undefined ? `${group.name} · ${description}` : group.name,
|
||||
...(directory.current !== null
|
||||
&& directory.current.provider === group.id
|
||||
&& directory.current.model === model.id
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
export const zh = {
|
||||
'command.description': '选择本会话使用的模型',
|
||||
'option.loadError': '目录加载失败:{message}',
|
||||
'option.deepseekV4Flash.description': '快速、高效且经济;适合目标明确、常规或并行任务。',
|
||||
'option.deepseekV4Pro.description': '更强的自主编码、知识与复杂推理能力;适合复杂或质量优先的任务,但成本更高。',
|
||||
'trigger.fallback': '选择模型',
|
||||
'trigger.loading': '正在加载模型…',
|
||||
'trigger.selectAria': '选择模型',
|
||||
@@ -37,6 +39,8 @@ export type ModelKey = keyof typeof zh
|
||||
export const en = {
|
||||
'command.description': 'Select the model for this conversation',
|
||||
'option.loadError': 'Catalog failed to load: {message}',
|
||||
'option.deepseekV4Flash.description': 'Fast, efficient, and economical; suited to focused, routine, or parallel tasks.',
|
||||
'option.deepseekV4Pro.description': 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.',
|
||||
'trigger.fallback': 'Select model',
|
||||
'trigger.loading': 'Loading models…',
|
||||
'trigger.selectAria': 'Select model',
|
||||
|
||||
@@ -30,6 +30,7 @@ const GROUPS = [{
|
||||
{
|
||||
id: 'deepseek-v4-flash',
|
||||
name: 'DeepSeek-V4-Flash',
|
||||
description: 'Fast, efficient, and economical; suited to focused, routine, or parallel tasks.',
|
||||
reasoning: {
|
||||
efforts: [
|
||||
{ id: 'off', name: 'Off' },
|
||||
@@ -42,6 +43,7 @@ const GROUPS = [{
|
||||
{
|
||||
id: 'deepseek-v4-pro',
|
||||
name: 'DeepSeek-V4-Pro',
|
||||
description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.',
|
||||
reasoning: {
|
||||
efforts: [
|
||||
{ id: 'off', name: 'Off' },
|
||||
@@ -52,10 +54,18 @@ const GROUPS = [{
|
||||
},
|
||||
},
|
||||
],
|
||||
}, {
|
||||
id: 'external',
|
||||
name: 'External Provider',
|
||||
models: [{
|
||||
id: 'deepseek-v4-flash',
|
||||
name: 'External Flash',
|
||||
description: 'Provider-authored description.',
|
||||
}],
|
||||
}]
|
||||
|
||||
/** Boot the plugin over fake faces + a stateful fake host (current moves on selectModel). */
|
||||
async function bench() {
|
||||
async function bench(locale: 'zh' | 'en' = 'zh') {
|
||||
const ctx = new Context()
|
||||
let defaultSelection: ModelSelection = { provider: 'deepseek-official', model: 'deepseek-v4-flash' }
|
||||
let selected = defaultSelection
|
||||
@@ -117,10 +127,9 @@ async function bench() {
|
||||
},
|
||||
})
|
||||
const localeRuntime = new LocaleRuntime(ctx)
|
||||
// This spec asserts the shipped Chinese copy. There is no jsdom `window` in
|
||||
// this lane, so browser-language detection never runs and the locale comes
|
||||
// from FALLBACK_LOCALE (en): state the asserted locale explicitly.
|
||||
localeRuntime.setLocale('zh')
|
||||
// There is no jsdom `window` in this lane, so browser-language detection
|
||||
// never runs. Each bench states the locale its assertions require.
|
||||
localeRuntime.setLocale(locale)
|
||||
ctx.provide('locale', localeRuntime)
|
||||
const scopes = new Map<SessionId, Context>()
|
||||
const addressed = new Set<SessionId>()
|
||||
@@ -179,15 +188,33 @@ describe('ui-model-selection dual entry', () => {
|
||||
expect(b.seat().locale).toBe('model')
|
||||
})
|
||||
|
||||
it('popup options mark the host current active with the provider group in the detail', async () => {
|
||||
it('localizes built-in descriptions and preserves external provider descriptions', async () => {
|
||||
const b = await bench()
|
||||
b.mint('s1')
|
||||
const options = await b.contribution().ui.options(projection('s1'), new AbortController().signal)
|
||||
expect(options.map((o: SelectOption) => o.label)).toEqual(['DeepSeek-V4-Flash', 'DeepSeek-V4-Pro'])
|
||||
expect(options[0]).toMatchObject({ active: true, detail: 'DeepSeek' })
|
||||
expect(options.map((o: SelectOption) => o.label)).toEqual([
|
||||
'DeepSeek-V4-Flash', 'DeepSeek-V4-Pro', 'External Flash',
|
||||
])
|
||||
expect(options[0]).toMatchObject({
|
||||
active: true,
|
||||
detail: 'DeepSeek · 快速、高效且经济;适合目标明确、常规或并行任务。',
|
||||
})
|
||||
expect(options[1]?.detail)
|
||||
.toBe('DeepSeek · 更强的自主编码、知识与复杂推理能力;适合复杂或质量优先的任务,但成本更高。')
|
||||
expect(options[2]?.detail).toBe('External Provider · Provider-authored description.')
|
||||
expect(options[1]?.active).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps built-in descriptions unchanged in English', async () => {
|
||||
const b = await bench('en')
|
||||
b.mint('s1')
|
||||
const options = await b.contribution().ui.options(projection('s1'), new AbortController().signal)
|
||||
expect(options[0]?.detail)
|
||||
.toBe('DeepSeek · Fast, efficient, and economical; suited to focused, routine, or parallel tasks.')
|
||||
expect(options[1]?.detail)
|
||||
.toBe('DeepSeek · Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.')
|
||||
})
|
||||
|
||||
it('a seat selection is the current the popup marks active next — one shared state', async () => {
|
||||
const b = await bench()
|
||||
b.mint('s1')
|
||||
|
||||
Reference in new issue
Block a user