Merge origin/master into xtr/tool-executor-plugins

This commit is contained in:
_Kerman committed 2026-10-21 18:01:55 +08:00
commit d98025a089
517 files changed
+3881 -181

No files matched your search

@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-09-17-multiline-display-dollar-math.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.md
/agent-note-multi-line-display-dollar-math:
en: 8f57c77aa43fbd6c
zh: ef5192184e4923d5
/agent-note-multi-line-display-dollar-math/problem:
en: e4b73644a982844c
zh: 48f86358242d6ccc
/agent-note-multi-line-display-dollar-math/decision:
en: d6b136b1f62285b7
zh: aba323bfdb800162
/agent-note-multi-line-display-dollar-math/alternatives-considered:
en: e0833b5674e648e7
zh: b0472020b07099b9
/agent-note-multi-line-display-dollar-math/consequences:
en: 70f16597bd640e93
zh: 0e0c9005f331e514
/agent-note-multi-line-display-dollar-math/testing:
en: f17a1453df91ce88
zh: 4f15be0139e8b694
@@ -0,0 +1,66 @@
# Agent Note: Multi-line display-dollar math
Status: implemented
English | [中文](2026-09-17-multiline-display-dollar-math.zh.md)
## Problem
Authors and models write `$$` display math in three shapes. The settled grammar rendered two of them: `$$math$$` on one line, and an opening `$$` alone on its line closed by `$$` alone on its line. The third shape — math on the opening line, a closing `$$` at the end of the last math line — did not:
```markdown
$$f(x)=x
\text{continued}$$
```
`micromark-extension-math` treats text after an opening `$$` as fence meta and accepts a closing fence only on a line whose content is exactly `$$` (after optional whitespace). The [markdown compatibility layer](../../archived/feature/2026-07-23-web-assistant-markdown.md) claimed only the same-line shape, and the settled renderer passes `node.value` to KaTeX while ignoring `node.meta`. The block therefore never closed: it consumed the document text that followed, `value` retained the literal `$$`, KaTeX raised `Can't use function '$' in math mode`, and [katex.tsx](../../../../packages/client/ui-primitives/src/markdown/katex.tsx)'s error arm painted the TeX source plus the swallowed prose in red.
In the session that surfaced this, one chat reply lost lines 22–55 of 55 to a single error span, and six note files lost every line after such a block (up to 500 lines) to one. A second defect shared the mechanism: when a later lone `$$` did close the flow, the opening line had already been assigned to `meta`, so the renderer silently dropped the first line of the formula.
## Decision
[mathCompatibility.ts](../../../../packages/client/ui-primitives/src/markdown/mathCompatibility.ts) gains multi-line display-dollar blocks. Its dollar construct keeps the opening line's text in `mathFlowValue` instead of fence meta and closes at a trailing `$$` on any later content line, so `$$a+b` continued by `c+d$$` becomes one math node.
A `$$` sequence that fails to close ends the candidate: immediately on a later line, and at that line's ending on the opening line (`$$x$$ trailing` fails there). Within the opening line the scan continues, so a later `$$` there still closes (`$$\verb*a$$a$$`). Scanning past either point would consume a later block's closing fence. A solitary `$` is content rather than a failed fence, so a formula that uses one — `$$P = \begin{cases} 1 & \text{if $x>0$} \end{cases}$$` — keeps the block open and closes normally wherever this flow owns the block. A trailing backslash escapes the line ending, not the next line's first character, so the escape run ends at the line ending instead of reaching into the next candidate. An unescaped `%` starts a TeX comment that runs to the line ending, so a `$$` at the end of a later line closes nothing when the comment holds text before it. A `%` whose text before the fence is empty or whitespace (`95%$$`, `95 %$$`) holds no comment text, and a block that opens and closes on one line keeps closing at that line's `$$`, as it did before this flow spanned lines. That state and its gate on the closing check belong to the dollar flow. The characters right after `\verb` or `\verb*` are that command's delimiter rather than a fence, so the delimiter and, when the region is empty, the `$` closing it are content; that tail is cleared at a line ending and after an escaped backslash (`\\verb` is a line break followed by letters), and a comment holds no commands. Otherwise verbatim regions are not parsed, which keeps `\verb`-bearing formulas on the same value path as every other input.
Its paragraph-interruption check declines instead of consuming the line ending, for the dollar construct only. Consuming it there keeps the paragraph open across the line boundary, so the document layer keeps the container and counts the next line lazy for every construct tried afterwards; that made upstream's `mathFlow`, the construct tried after this one, end empty inside a list item. Declining also leaves the interruption decision to that upstream flow, whose own check short-circuits without consuming and claims only the fences it accepts, so a candidate neither construct accepts (`**before` followed by `$$a\$b`) leaves its paragraph intact. The backslash construct has no competing upstream construct and keeps consuming there.
[parse.ts](../../../../packages/client/ui-primitives/src/markdown/parse.ts) registers `mathCompatibility()` after `math()`: micromark dispatches the extension registered last first, so the dollar flow above takes precedence over upstream's `mathFlow` while `math()` still owns inline `$…$`.
## Alternatives considered
**Keep the compatibility layer at same-line `$$…$$`.** The multi-line shape is what GitHub, KaTeX's auto-render, VS Code, and Typora accept, and the form models emit for multi-case formulas; leaving it unparsed means a red error span in place of the formula plus the rest of the document.
**Repair the parsed tree afterwards.** The flow consumes the swallowed text into `mathFlowValue`, where it is indistinguishable from math source; `meta` and `value` cannot be reassembled into the intended block, and the following blocks were never parsed at all.
**Rewrite `$$` fences in the source text before parsing.** The compatibility layer is a grammar extension by design; a text pass would need to locate code spans, fences, and escapes itself, and would change author text rather than parse it.
**Continue the dollar flow after a failed close.** That is what a plain `multiline` flag produces: `$$x$$ trailing\n\n$$y$$` then scans to the second block's `$$` and renders one math node holding both lines' text, where upstream inline math renders `x`, the prose, and `y` separately. Existing behavior for `$$x$$ trailing` already pinned the fallback, so the flow declines the candidate instead. Declining on any `$` rather than a full `$$` sequence also broke formulas that use a solitary `$` inside `\text{}`.
**End the attempt as soon as one line resists parsing.** Declining every failed same-line close swallows nothing but turns `$$a$b` lines into a quadratic scan and drops the solitary-`$` formulas above. The condition is the full `$$` sequence, not the character.
**Claim the paragraph interruption in the check.** Returning success there, as upstream's flow does for the fences it accepts, prevents the lazy bookkeeping that broke the list case, but it also ends the paragraph for candidates that later fail (`**before\n$$a\$b\nafter**` split in two and exposed its `**`). Declining instead lets upstream's own check decide.
**Apply the interruption guard to the backslash construct too.** The guard exists to protect the construct tried after this one; `\[`/`\(` have no second construct, and an unconditional guard dropped their multi-line blocks (`Prose line` followed by `\[`).
## Consequences
The three shapes now produce the same math node, so formulas stop truncating at the opening line and stop swallowing the prose after them. `$$` fence meta is no longer honored: `$$asciimath$$` parses as a formula whose value is `asciimath`, which upstream's flow treats as fence meta and drops. That spelling has no user in this repository, and the renderer never read `meta` anyway.
Six inputs still reach upstream's flow or stay in the surrounding paragraph, because the compatibility flow declines them. Trailing text after a closing `$$` (`$$x$$ trailing`, or a CJK quote mark closing a line of prose) resolves through upstream inline math, as the existing `markdown.client.spec.tsx` case pins — including when a later `$$` block follows. A `$$` sequence the line does not end after declines the block: immediately on a later line, and at that line's ending on the opening line. A `$$` block left open runs to the next lone `$$` or to EOF exactly as it did before this change, because the upstream flow closes at EOF.
The fourth is a multi-line block whose opening line carries a solitary `$` and that must interrupt a paragraph, with no blank line before it. Both flows decline the interruption there — this one by design, upstream because its fence meta stops at the `$` — so the block stays inside the paragraph and renders as inline math or not at all. Separating the block from the paragraph with a blank line makes this flow own it and render the display block.
The fifth follows from the comment gate: a block whose only closing fence sits behind a comment that holds text keeps scanning instead of closing there, so when nothing else closes it the block reaches the upstream flow as the shapes above do. A fence behind a whitespace-only comment closes the block there, which leaves a following lone `$$` as its own empty block where the flow it replaced kept one node.
The sixth is a multi-line block inside a container whose continuation line falls back to the container's indentation. `- Define:` followed by `$$f(x)=x` at the item's indentation and then a de-indented continuation line gives upstream's flow a fence meta and no value, so the reader sees an empty display block and the continuation as prose — the opening line is gone. Keeping the continuation at the same indentation, or moving the block out of the list, renders it normally. Both flow behaviors match the grammar this replaced; only the multi-line blocks this flow owns are new.
One further limitation is not a fallback: the flow reads the six characters before a fence and does not parse verbatim regions, so a fence inside an unterminated region (`\verb|$$`) closes the block there, and a region whose text ends in `\verb` (`\verb**\verb*$$`) has its fence read as that text's delimiter. The six characters before either fence are locally identical, so no rule that reads only that tail separates them.
Repeated unclosed candidates keep linear parser work: an open candidate declines at the next line that starts with a full `$$` it cannot close, rather than scanning each candidate to EOF, which would make 9.6 KB of `$$a$b` lines take ~4 s instead of ~16 ms. KaTeX-level failures in a formula's own source, such as `\arccot`, remain errors by design.
## Testing
`packages/client/ui-primitives/tests/markdown.client.spec.tsx` adds the multi-line case — opening-line math closed on a content line, closed by a lone fence, after a paragraph, inside a list item, and at column zero after a list item — and that case fails without the change. The same file also pins a multi-line formula that uses a solitary `$`, which like the case above renders only with this change. Further cases pin one condition each: a trailing-text candidate ahead of a later block (the flow must decline a full `$$` the line does not end after), a solitary `$` inside a single-line display formula (the flow must not decline on a single `$`), an escaped-dollar candidate that leaves its paragraph intact (the interruption check must not claim the paragraph), an unclosed `\[` candidate that leaves its paragraph intact (the guard must not apply to the backslash construct), a candidate ending in an odd backslash (the escape run must end at the line ending), a closing fence inside a TeX comment, a `%` the fence follows immediately or after whitespace, a second `%` inside an open comment, a `%` inside `\verb|…|`, the delimiter right after `\verb` and `\verb*` together with its empty region, an escaped backslash before `verb`, a verb tail that crosses no line ending, a comment ending in `verb`, a later-line trailing fence whose following prose upstream takes, a failed fence inside a comment, and bounded-work cases for repeated candidates with that backslash, without it, and ending in a verb tail.
The DOM compatibility corpus adds `math-multiline-display` with its `.settled.txt` and `.streaming.txt` fixtures, which also pin that a column-zero `$$` after a list item stays outside the list item. `apps/web/tests/math-rendering.e2e.ts` renders the multi-line shape in the browser lane.
@@ -0,0 +1,66 @@
# Agent Note: Multi-line display-dollar math
Status: implemented
[English](2026-09-17-multiline-display-dollar-math.md) | 中文
## Problem
作者和模型写 `$$` 行间公式时有三种写法。定稿语法只渲染其中两种:一行内开闭的 `$$math$$`,以及 `$$` 独占一行、由独占一行的 `$$` 闭合。第三种——开围栏同一行就带公式内容,闭合 `$$` 落在最后一行公式行行尾——不渲染:
```markdown
$$f(x)=x
\text{continued}$$
```
`micromark-extension-math` 把开围栏 `$$` 之后的同一行文本当作围栏 meta,并且只接受内容恰为 `$$`(后接可选空白)的行作为闭合围栏。[markdown 兼容层](../../archived/feature/2026-07-23-web-assistant-markdown.md)只认同一行开闭这一种形状,而定稿渲染器把 `node.value` 交给 KaTeX,不读 `node.meta`。于是该块始终不闭合:它吞掉后续正文,`value` 里残留字面 `$$`,KaTeX 抛出 `Can't use function '$' in math mode`,[katex.tsx](../../../../packages/client/ui-primitives/src/markdown/katex.tsx) 的错误分支把 TeX 原文连同被吞掉的正文一起显示为红色。
在暴露该问题的会话中,一条聊天回复 55 行里有 34 行(第 22–55 行)变成同一个错误块,六个笔记文件从出错点起的所有行(最多约 500 行)同样变成一个错误块。同一机制还带出第二个缺陷:当更靠后的独占行 `$$` 恰好闭合了这个流时,开围栏那一行已经归给 `meta`,渲染器于是静默丢掉公式的第一行。
## Decision
[mathCompatibility.ts](../../../../packages/client/ui-primitives/src/markdown/mathCompatibility.ts) 现在支持跨行的显示美元块。dollar 构造把开围栏同一行的文本留在 `mathFlowValue` 里而不是当作围栏 meta,并在后续任意内容行行尾的 `$$` 处闭合,于是 `$$a+b` 接 `c+d$$` 成为一个 math 节点。
闭合失败的 `$$` 序列会结束该候选:出现在后续行时立即结束;出现在开围栏行时在该行行尾结束(`$$x$$ trailing` 即在此放弃)。在开围栏行内扫描继续,因此同一行更靠后的 `$$` 仍能闭合(`$$\verb*a$$a$$`)。越过这两点继续扫描都会吞掉更靠后一个块的闭合围栏。孤立的一个 `$` 属于公式内容而不是失败的围栏,所以凡是本 flow 拥有该块的场合,用到它的公式——`$$P = \begin{cases} 1 & \text{if $x>0$} \end{cases}$$`——保持块打开并正常闭合。行尾反斜杠转义的是换行本身,不是下一行的第一个字符,因此转义状态在换行处结束,不会延续到下一个候选。未转义的 `%` 会开启延续到行尾的 TeX 注释:当注释在围栏之前还有文本时,块后续行行尾的 `$$` 不闭合任何东西;而围栏之前只有空白或什么都没有的 `%`(`95%$$`、`95 %$$`)不含注释文本,且在一行内开闭的块仍在该行的 `$$` 处闭合,与它跨行之前的行为一致。该状态及其对闭合判定的门控属于 dollar flow。`\verb` 或 `\verb*` 之后的那个字符是该命令的分隔符而不是围栏,因此该分隔符、以及区域为空时紧随其后的那个 `$` 都属于内容;这段尾部状态在换行处、以及在被转义的反斜杠之后清空(`\\verb` 是换行加字母,不是该命令),注释里也不识别命令。除此之外不解析原样文本区域,使含 `\verb` 的公式与其它输入走同一条 value 路径。
它的段落打断检查改为「不认领」,且只作用于 dollar 构造。在那里消耗换行会让段落跨行边界保持打开,document 层因此保留容器并把下一行对随后尝试的所有构造计为 lazy;这正是上游 `mathFlow`(本构造之后被尝试的构造)在列表项内以空块结束的原因。不认领同时把打断决定交给上游 flow:它自己的检查不消耗换行就短路返回,并且只认领它接受的围栏,因此两个构造都不接受的候选(`**before` 后接 `$$a\$b`)保持原段落。反斜杠构造没有竞争的上游构造,仍然照旧消耗换行。
[parse.ts](../../../../packages/client/ui-primitives/src/markdown/parse.ts) 把 `mathCompatibility()` 注册在 `math()` 之后:micromark 先派发最后注册的扩展,上面的 dollar flow 因此优先于上游 `mathFlow`,而行内 `$…$` 仍由 `math()` 负责。
## Alternatives considered
**兼容层维持只处理同一行 `$$…$$`。** 多行写法是 GitHub、KaTeX auto-render、VS Code、Typora 都接受的形状,也是模型写多分支公式时实际采用的形状;不解析它意味着公式位置显示为红色错误块,并连带吞掉后文。
**解析后修补语法树。** 被吞掉的正文已经进入 `mathFlowValue`,与数学源码无法区分;`meta` 和 `value` 无法还原成原意的块,而且后面的块根本没有被解析。
**解析前用文本替换改写 `$$` 围栏。** 兼容层的设计就是语法扩展;文本替换需要自己定位代码 span、围栏和转义,而且改的是作者原文而不是解析它。
**闭合失败后继续扫描。** 这正是单纯打开 `multiline` 会得到的行为:`$$x$$ trailing\n\n$$y$$` 会一路扫到第二个块的 `$$`,渲染成一个同时含着两行文本的 math 节点;而上游行内数学会把 `x`、正文和 `y` 分开渲染。`$$x$$ trailing` 的既有行为已经固定了这条回退,因此该构造放弃这个候选。对任意一个 `$`(而不是完整的 `$$` 序列)就放弃,还会破坏在 `\text{}` 内使用单个 `$` 的公式。
**只要有一行解析不下去就结束尝试。** 对每次同一行闭合失败都放弃虽然不再吞文,却会让 `$$a$b` 这类行变成平方级扫描,并丢掉上面那些含单个 `$` 的公式。判据是完整的 `$$` 序列,不是那个字符。
**在检查阶段认领段落打断。** 像上游 flow 那样返回成功可以避免破坏列表用例的 lazy 记账,但也会让最终失败的候选提前结束段落(`**before\n$$a\$b\nafter**` 被拆成两段并暴露 `**`)。改为不认领,把决定交给上游自己的检查。
**把打断守卫也用在反斜杠构造上。** 这个守卫是为了保护随后被尝试的构造;`\[`/`\(` 没有第二个构造,无条件套用会丢掉它们的跨行块(`Prose line` 后接 `\[`)。
## Consequences
三种写法现在产生同一个 math 节点,公式不再在开围栏那一行被截断,也不再吞掉其后的正文。`$$` 围栏 meta 不再生效:`$$asciimath$$` 会解析成 value 为 `asciimath` 的公式,而上游 flow 会把该段当作 fence meta 丢弃。这种写法在本仓库没有使用者,且渲染器本来就从不读 `meta`。
有六种输入仍会走到上游 flow,或留在所属段落里,因为兼容 flow 主动放弃它们。闭合 `$$` 之后还有文本时(`$$x$$ trailing`,或用中文引号收尾的散文行)走上游行内数学,这一点由 `markdown.client.spec.tsx` 中已有用例固定,后面还有别的 `$$` 块时同样如此。「后面不是行尾」的 `$$` 序列会让块放弃:出现在后续行时立即放弃,出现在开围栏行时在该行行尾放弃。未闭合的 `$$` 块仍像本次改动之前那样延伸到下一个独占行 `$$` 或文件结尾,因为上游 flow 在 EOF 处闭合。
第四种是「开围栏行含孤立 `$`、且需要打断段落、前面又没有空行」的多行块。两个 flow 都不认领这次打断——本 flow 出于设计,上游则因为它的围栏 meta 在 `$` 处即停止——该块因此留在段落里,渲染成行内数学或根本不渲染。用空行把块与段落分开,本 flow 就会拥有它并渲染 display。
第五种来自注释门控:当块唯一的闭合围栏位于「带文本的注释」之后时,块继续扫描而不是在那里闭合,若再没有别的围栏闭合它,就会像上面几种一样走到上游 flow。若围栏之前只有空白,则块在该处闭合,此后单独一行的 `$$` 会成为它自己的空块——而被替换掉的语法那时只给出一个节点。
第六种是容器内续行退回容器缩进的多行块。`- Define:` 之后以列表项缩进写 `$$f(x)=x`,再接一行不缩进的续行,上游 flow 会得到有 meta、无 value 的结果:读者看到一个空的 display 块加一行散文,公式首行无声消失。续行保持同样缩进,或把整块移到列表之外,即可正常渲染。这两种 flow 行为都与本次替换掉的语法一致;真正新增的只是「本 flow 拥有的多行块」。
另有一项不属于回退的限制:flow 只读取围栏之前的六个字符、不解析原样文本区域,因此未闭合区域内部的围栏(`\verb|$$`)会在那里闭合块,而内容以文本 `\verb` 结尾的区域(`\verb**\verb*$$`)其围栏会被读成该文本的分隔符。两种情况下围栏前的六个字符在局部完全相同,仅凭这段尾部无法区分。
重复的未闭合候选保持线性解析开销:未闭合的候选会在下一行遇到无法闭合的完整 `$$` 时放弃,而不是把每个候选都扫到 EOF——否则 9.6 KB 的 `$$a$b` 行会花约 4 秒而不是约 16 毫秒。公式自身源码层面的 KaTeX 失败,例如 `\arccot`,仍按设计报错。
## Testing
`packages/client/ui-primitives/tests/markdown.client.spec.tsx` 新增多行用例——开围栏行内容由内容行行尾 `$$` 闭合、由独占行 `$$` 闭合、出现在段落之后、出现在列表项内,以及列表项之后的零缩进围栏——该用例去掉改动即失败。同一文件还固定了「使用单个 `$` 的多行公式」,它与上面的多行用例一样只有本次改动才会渲染成 display。另有一组用例各自对应一个条件:后面还有块时的尾随文本候选(flow 必须放弃「后面不是行尾」的完整 `$$`)、单行显示公式内的单个 `$`(不得因为一个 `$` 就放弃)、保持段落完整性的转义美元候选(打断检查不得认领段落)、保持段落完整性的未闭合 `\[` 候选(守卫不得作用于反斜杠构造)、以奇数个反斜杠结尾的候选(转义状态必须在换行处结束)、TeX 注释内的闭合围栏、紧邻围栏或只隔空白的 `%`、已开启注释内的第二个 `%`、`\verb|…|` 内的 `%`、`\verb` 与 `\verb*` 之后的分隔符及其空区域、`verb` 之前的转义反斜杠、不跨行的 verb 尾部、以 `verb` 结尾的注释、后续行尾随文本围栏(其后正文由上游接管)、注释内失败的围栏,以及带该反斜杠、不带该反斜杠、以 verb 尾部结尾三种重复候选的有界解析开销。
DOM 兼容语料新增 `math-multiline-display` 及其 `.settled.txt`、`.streaming.txt`,同时固定了「列表项之后的零缩进 `$$` 不进入列表项内」。`apps/web/tests/math-rendering.e2e.ts` 在浏览器 lane 渲染多行写法。
+2 -2
View File
@@ -33,8 +33,8 @@
en: 372297005b57a08b
zh: 074a635ac17e4362
/deepseek-harness-desktop/installation-ownership:
en: b3cf09d7c901d727
zh: 2ec34045a201d8e9
en: ae0384d699f55cea
zh: ffb879b1d4b08bae
/deepseek-harness-desktop/installation-ownership/runtime-and-plugin-activation:
en: a1a47b09f4a72ba4
zh: 7b1cf27f0b72cf31
+1 -1
View File
@@ -150,7 +150,7 @@ The product UI retains Web actions, including "Open In..." through the shared au
Update copy preserves the complete version, including prerelease suffixes, without adding v or V. When no update is available, the dialog title reports the result and the body shows the current version.
Electron chooses typed English, Chinese, or Japanese shell copy from its application locale and falls back to English. The macOS application bundle declares English, Simplified Chinese, and Japanese in `CFBundleLocalizations`, allowing macOS to match the initial application language to the user’s preferred languages. Saved Client UI language preferences still take precedence for the main interface. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources.
Electron chooses typed English, Chinese, or Japanese shell copy from its application locale and falls back to English. The macOS application bundle declares English, Simplified Chinese, and Japanese in `CFBundleLocalizations`, allowing macOS to match the initial application language to the user’s preferred languages. Saved Client UI language preferences still take precedence for the main interface. Windows NSIS installer resources live in one JSON file per language under `installer/locales/`; packaging generates NSIS strings and discovers the installer language list from these files. English, Simplified Chinese, and Japanese resources must have matching keys and format placeholders; `pnpm run check:i18n` validates them. The native installer test builds and exercises each configured language. Platform account pages and onboarding artwork use English for languages other than Chinese. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources.
Windows and macOS relay native fullscreen transitions and the state after each page load to the renderer, so overlays can adapt their caption clearance.
+1 -1
View File
@@ -150,7 +150,7 @@ Electron 拥有 `$DSH_HOME/profiles/desktop`。其 `dependencies` 包含 pnpm
升级文案完整展示版本号(包括预发布后缀),不额外添加 v 或 V。无可用更新时,弹窗标题显示检查结果,正文显示当前版本。
Electron 根据应用语言选择类型化的英文、中文或日文 shell 文案,并回退到英文。macOS 应用包通过 `CFBundleLocalizations` 声明支持英语、简体中文和日语,让 macOS 根据用户的首选语言匹配初始应用语言。主界面仍优先使用已保存的 Client UI 语言偏好。在 Windows 上,主文档的语言会更新桌面菜单、恢复与更新提示。仓库 Client UI i18n 检查覆盖桌面端源码。
Electron 根据应用语言选择类型化的英文、中文或日文 shell 文案,并回退到英文。macOS 应用包通过 `CFBundleLocalizations` 声明支持英语、简体中文和日语,让 macOS 根据用户的首选语言匹配初始应用语言。主界面仍优先使用已保存的 Client UI 语言偏好。 Windows NSIS 安装器在 `installer/locales/` 下按语言保存独立 JSON 资源;打包从这些文件生成 NSIS 文案和安装器语言清单。英文、简体中文和日语资源的键与格式占位符必须一致,`pnpm run check:i18n` 负责检查。原生安装器测试逐个构建并验证配置中的语言。账户平台页面和引导插图对中文以外的语言使用英文。在 Windows 上,主文档的语言会更新桌面菜单、恢复与更新提示。仓库 Client UI i18n 检查覆盖桌面端源码。
Windows 和 macOS 会在原生全屏切换及每次页面加载后向渲染器同步全屏状态,让浮层调整顶栏避让距离。
+1 -1
View File
@@ -53,7 +53,7 @@ export interface DesktopElectronBuilderConfig {
readonly perMachine: false
readonly allowElevation: false
readonly allowToChangeInstallationDirectory: false
readonly installerLanguages: readonly ['en_US', 'zh_CN']
readonly installerLanguages: readonly string[]
}
readonly beforeBuild: () => Promise<boolean>
readonly beforePack: (context: BeforePackContext) => Promise<void>
+40
View File
@@ -0,0 +1,40 @@
{
"nsisLanguage": "ENGLISH",
"strings": {
"INSTALLER_INSTALL": "Install now",
"INSTALLER_FINISH": "Finish",
"INSTALLER_NATIVE_PROGRESS": "Preparing installation… %d%%",
"INSTALLER_PROGRESS_EXTRACT": "Extracting files… %d%%",
"INSTALLER_PROGRESS_COPY": "Installing files… %d%%",
"INSTALLER_PROGRESS_REGISTER": "Finishing installation… %d%%",
"INSTALLER_PROGRESS_CLEAN": "Cleaning temporary files… %d%%",
"INSTALLER_CHOOSE_PATH": "Choose location",
"INSTALLER_BROWSE": "Browse",
"INSTALLER_LAUNCH": "Launch now",
"INSTALLER_LAUNCH_FAILED": "DeepSeek Harness could not be launched. Retry, or clear Launch now and close the installer.",
"INSTALLER_UI_ERROR": "The installer could not initialize its window. Close it and try again.",
"INSTALLER_THEME_ERROR": "Use /THEME=auto, /THEME=light or /THEME=dark.",
"INSTALLER_FAILED": "Installation did not finish. Close this installer and retry. If the problem persists, download the installer again.",
"INSTALLER_RUNNING": "DeepSeek Harness is running. Quit it from the system tray and try again.",
"INSTALLER_DIRECTORY_FAILED": "The installation directory could not be replaced.",
"INSTALLER_DIRECTORY_HINT": "Check access to the installation directory and close programs using its files, then run the installer again. Copy the error details if the problem persists.",
"INSTALLER_DIRECTORY_RESTORE_HINT": "The previous installation could not be restored. Keep the backup directory shown in the error details and contact support before installing again.",
"INSTALLER_PATH_INVALID": "Choose a full local folder path. Drive roots, system folders, links and special characters are not supported.",
"INSTALLER_PATH_OWNERSHIP": "Choose an empty folder or the registered DeepSeek Harness installation folder.",
"INSTALLER_PATH_WRITABLE": "This location is not writable. Choose a folder available to the current user.",
"INSTALLER_DISK_SPACE": "There is not enough free disk space. Choose another location.",
"INSTALLER_PER_USER": "This installer supports the current user only. Uninstall the existing all-users installation first.",
"INSTALLER_EXTRACT_FAILED": "The application files could not be extracted.",
"INSTALLER_EXTRACT_HINT": "Security software may have blocked a file, or the disk may be full. Close this installer and try again. If it keeps failing, send us the error details below.",
"INSTALLER_EXTRACT_COPY": "Copy error details",
"INSTALLER_EXTRACT_EXPAND": "Show details",
"INSTALLER_EXTRACT_COLLAPSE": "Hide details",
"INSTALLER_EXTRACT_SAVED": "The full report was saved to %s",
"INSTALLER_EXTRACT_UNSAVED": "The full report could not be saved. Use Copy error details to keep it.",
"INSTALLER_EXTRACT_COPIED": "Error details were copied to the clipboard."
},
"uninstaller": {
"welcome": "Welcome",
"finished": "has been uninstalled"
}
}
+40
View File
@@ -0,0 +1,40 @@
{
"nsisLanguage": "JAPANESE",
"strings": {
"INSTALLER_INSTALL": "今すぐインストール",
"INSTALLER_FINISH": "完了",
"INSTALLER_NATIVE_PROGRESS": "インストールを準備しています… %d%%",
"INSTALLER_PROGRESS_EXTRACT": "ファイルを展開しています… %d%%",
"INSTALLER_PROGRESS_COPY": "ファイルをインストールしています… %d%%",
"INSTALLER_PROGRESS_REGISTER": "インストールを完了しています… %d%%",
"INSTALLER_PROGRESS_CLEAN": "一時ファイルを削除しています… %d%%",
"INSTALLER_CHOOSE_PATH": "インストール先を選択",
"INSTALLER_BROWSE": "参照",
"INSTALLER_LAUNCH": "今すぐ起動",
"INSTALLER_LAUNCH_FAILED": "DeepSeek Harness を起動できませんでした。再試行するか、「今すぐ起動」のチェックを外してインストーラーを閉じてください。",
"INSTALLER_UI_ERROR": "インストーラーのウィンドウを初期化できませんでした。閉じてから再試行してください。",
"INSTALLER_THEME_ERROR": "/THEME=auto、/THEME=light、/THEME=dark のいずれかを指定してください。",
"INSTALLER_FAILED": "インストールが完了しませんでした。インストーラーを閉じて再試行してください。問題が解決しない場合は、インストーラーを再度ダウンロードしてください。",
"INSTALLER_RUNNING": "DeepSeek Harness が実行中です。システムトレイから終了して再試行してください。",
"INSTALLER_DIRECTORY_FAILED": "インストール先のディレクトリを置き換えられませんでした。",
"INSTALLER_DIRECTORY_HINT": "インストール先へのアクセス権限を確認し、そのファイルを使用しているプログラムを閉じてから、インストーラーを再実行してください。問題が続く場合はエラーの詳細をコピーしてください。",
"INSTALLER_DIRECTORY_RESTORE_HINT": "以前のインストールを復元できませんでした。エラーの詳細に表示されたバックアップディレクトリを保持し、再インストール前にサポートへお問い合わせください。",
"INSTALLER_PATH_INVALID": "ローカルフォルダーの完全なパスを選択してください。ドライブのルート、システムフォルダー、リンク、特殊文字を含むパスは使用できません。",
"INSTALLER_PATH_OWNERSHIP": "空のフォルダー、または登録済みの DeepSeek Harness インストール先を選択してください。",
"INSTALLER_PATH_WRITABLE": "この場所には書き込めません。現在のユーザーが書き込めるフォルダーを選択してください。インストーラーは管理者権限を要求しません。",
"INSTALLER_DISK_SPACE": "ディスクの空き容量が不足しています。別の場所を選択してください。",
"INSTALLER_PER_USER": "このインストーラーは現在のユーザーのみを対象としています。先にすべてのユーザー向けの既存のインストールを削除してください。",
"INSTALLER_EXTRACT_FAILED": "アプリケーションのファイルを展開できませんでした。",
"INSTALLER_EXTRACT_HINT": "セキュリティソフトがファイルをブロックしたか、ディスクの空き容量が不足している可能性があります。インストーラーを閉じて再試行してください。問題が続く場合は、以下のエラーの詳細をお送りください。",
"INSTALLER_EXTRACT_COPY": "エラーの詳細をコピー",
"INSTALLER_EXTRACT_EXPAND": "詳細を表示",
"INSTALLER_EXTRACT_COLLAPSE": "詳細を非表示",
"INSTALLER_EXTRACT_SAVED": "完全なレポートを %s に保存しました",
"INSTALLER_EXTRACT_UNSAVED": "完全なレポートを保存できませんでした。「エラーの詳細をコピー」で保存してください。",
"INSTALLER_EXTRACT_COPIED": "エラーの詳細をクリップボードにコピーしました。"
},
"uninstaller": {
"welcome": "アンインストール ウィザードへようこそ",
"finished": "アンインストールされました"
}
}
+40
View File
@@ -0,0 +1,40 @@
{
"nsisLanguage": "SIMPCHINESE",
"strings": {
"INSTALLER_INSTALL": "立即安装",
"INSTALLER_FINISH": "完成",
"INSTALLER_NATIVE_PROGRESS": "正在准备安装… %d%%",
"INSTALLER_PROGRESS_EXTRACT": "正在解压文件… %d%%",
"INSTALLER_PROGRESS_COPY": "正在安装文件… %d%%",
"INSTALLER_PROGRESS_REGISTER": "正在完成安装… %d%%",
"INSTALLER_PROGRESS_CLEAN": "正在清理临时文件… %d%%",
"INSTALLER_CHOOSE_PATH": "选择安装位置",
"INSTALLER_BROWSE": "选择",
"INSTALLER_LAUNCH": "立即启动",
"INSTALLER_LAUNCH_FAILED": "无法启动 DeepSeek Harness。请重试,或取消勾选立即启动后关闭安装程序。",
"INSTALLER_UI_ERROR": "无法初始化安装窗口,请关闭后重试。",
"INSTALLER_THEME_ERROR": "主题参数须为 /THEME=auto、/THEME=light 或 /THEME=dark。",
"INSTALLER_FAILED": "安装未完成,请关闭安装程序后重试。如仍然失败,请重新下载安装包。",
"INSTALLER_RUNNING": "DeepSeek Harness 正在运行,请先在系统托盘中退出应用后重试。",
"INSTALLER_DIRECTORY_FAILED": "无法替换安装目录。",
"INSTALLER_DIRECTORY_HINT": "请检查安装目录的访问权限,关闭占用其中内容的程序后重新运行安装器。如仍失败,请复制错误详情。",
"INSTALLER_DIRECTORY_RESTORE_HINT": "未能恢复原安装目录。请保留错误详情中列出的备份目录,联系支持人员后再安装。",
"INSTALLER_PATH_INVALID": "请选择本地磁盘上的完整文件夹路径,不能使用磁盘根目录、系统目录、链接目录或包含特殊字符的路径。",
"INSTALLER_PATH_OWNERSHIP": "请选择空文件夹,或 DeepSeek Harness 原来的安装目录。",
"INSTALLER_PATH_WRITABLE": "无法写入此位置。请选择当前用户可写入的文件夹;安装程序不会申请管理员权限。",
"INSTALLER_DISK_SPACE": "此磁盘的可用空间不足,请选择其他位置。",
"INSTALLER_PER_USER": "此安装程序仅支持当前用户。请先卸载已有的所有用户安装版本。",
"INSTALLER_EXTRACT_FAILED": "无法解压应用程序文件。",
"INSTALLER_EXTRACT_HINT": "可能是安全软件拦截了文件,或磁盘空间不足。请关闭安装程序后重试;如仍然失败,请将下方错误信息发送给我们。",
"INSTALLER_EXTRACT_COPY": "复制错误信息",
"INSTALLER_EXTRACT_EXPAND": "显示详情",
"INSTALLER_EXTRACT_COLLAPSE": "隐藏详情",
"INSTALLER_EXTRACT_SAVED": "完整错误报告已保存到 %s",
"INSTALLER_EXTRACT_UNSAVED": "无法保存完整错误报告,请使用“复制错误信息”保留。",
"INSTALLER_EXTRACT_COPIED": "错误信息已复制到剪贴板。"
},
"uninstaller": {
"welcome": "欢迎",
"finished": "已从你"
}
}
-63
View File
@@ -1,63 +0,0 @@
; NSIS owns installer localization independently of the Electron application locale.
LangString INSTALLER_INSTALL ${LANG_ENGLISH} "Install now"
LangString INSTALLER_INSTALL ${LANG_SIMPCHINESE} "立即安装"
LangString INSTALLER_FINISH ${LANG_ENGLISH} "Finish"
LangString INSTALLER_FINISH ${LANG_SIMPCHINESE} "完成"
LangString INSTALLER_NATIVE_PROGRESS ${LANG_ENGLISH} "Preparing installation… %d%%"
LangString INSTALLER_NATIVE_PROGRESS ${LANG_SIMPCHINESE} "正在准备安装… %d%%"
LangString INSTALLER_PROGRESS_EXTRACT ${LANG_ENGLISH} "Extracting files… %d%%"
LangString INSTALLER_PROGRESS_EXTRACT ${LANG_SIMPCHINESE} "正在解压文件… %d%%"
LangString INSTALLER_PROGRESS_COPY ${LANG_ENGLISH} "Installing files… %d%%"
LangString INSTALLER_PROGRESS_COPY ${LANG_SIMPCHINESE} "正在安装文件… %d%%"
LangString INSTALLER_PROGRESS_REGISTER ${LANG_ENGLISH} "Finishing installation… %d%%"
LangString INSTALLER_PROGRESS_REGISTER ${LANG_SIMPCHINESE} "正在完成安装… %d%%"
LangString INSTALLER_PROGRESS_CLEAN ${LANG_ENGLISH} "Cleaning temporary files… %d%%"
LangString INSTALLER_PROGRESS_CLEAN ${LANG_SIMPCHINESE} "正在清理临时文件… %d%%"
LangString INSTALLER_CHOOSE_PATH ${LANG_ENGLISH} "Choose location"
LangString INSTALLER_CHOOSE_PATH ${LANG_SIMPCHINESE} "选择安装位置"
LangString INSTALLER_BROWSE ${LANG_ENGLISH} "Browse"
LangString INSTALLER_BROWSE ${LANG_SIMPCHINESE} "选择"
LangString INSTALLER_LAUNCH ${LANG_ENGLISH} "Launch now"
LangString INSTALLER_LAUNCH ${LANG_SIMPCHINESE} "立即启动"
LangString INSTALLER_LAUNCH_FAILED ${LANG_ENGLISH} "DeepSeek Harness could not be launched. Retry, or clear Launch now and close the installer."
LangString INSTALLER_LAUNCH_FAILED ${LANG_SIMPCHINESE} "无法启动 DeepSeek Harness。请重试,或取消勾选立即启动后关闭安装程序。"
LangString INSTALLER_UI_ERROR ${LANG_ENGLISH} "The installer could not initialize its window. Close it and try again."
LangString INSTALLER_UI_ERROR ${LANG_SIMPCHINESE} "无法初始化安装窗口,请关闭后重试。"
LangString INSTALLER_THEME_ERROR ${LANG_ENGLISH} "Use /THEME=auto, /THEME=light or /THEME=dark."
LangString INSTALLER_THEME_ERROR ${LANG_SIMPCHINESE} "主题参数须为 /THEME=auto、/THEME=light 或 /THEME=dark。"
LangString INSTALLER_FAILED ${LANG_ENGLISH} "Installation did not finish. Close this installer and retry. If the problem persists, download the installer again."
LangString INSTALLER_FAILED ${LANG_SIMPCHINESE} "安装未完成,请关闭安装程序后重试。如仍然失败,请重新下载安装包。"
LangString INSTALLER_RUNNING ${LANG_ENGLISH} "DeepSeek Harness is running. Quit it from the system tray and try again."
LangString INSTALLER_RUNNING ${LANG_SIMPCHINESE} "DeepSeek Harness 正在运行,请先在系统托盘中退出应用后重试。"
LangString INSTALLER_DIRECTORY_FAILED ${LANG_ENGLISH} "The installation directory could not be replaced."
LangString INSTALLER_DIRECTORY_FAILED ${LANG_SIMPCHINESE} "无法替换安装目录。"
LangString INSTALLER_DIRECTORY_HINT ${LANG_ENGLISH} "Check access to the installation directory and close programs using its files, then run the installer again. Copy the error details if the problem persists."
LangString INSTALLER_DIRECTORY_HINT ${LANG_SIMPCHINESE} "请检查安装目录的访问权限,关闭占用其中内容的程序后重新运行安装器。如仍失败,请复制错误详情。"
LangString INSTALLER_DIRECTORY_RESTORE_HINT ${LANG_ENGLISH} "The previous installation could not be restored. Keep the backup directory shown in the error details and contact support before installing again."
LangString INSTALLER_DIRECTORY_RESTORE_HINT ${LANG_SIMPCHINESE} "未能恢复原安装目录。请保留错误详情中列出的备份目录,联系支持人员后再安装。"
LangString INSTALLER_PATH_INVALID ${LANG_ENGLISH} "Choose a full local folder path. Drive roots, system folders, links and special characters are not supported."
LangString INSTALLER_PATH_INVALID ${LANG_SIMPCHINESE} "请选择本地磁盘上的完整文件夹路径,不能使用磁盘根目录、系统目录、链接目录或包含特殊字符的路径。"
LangString INSTALLER_PATH_OWNERSHIP ${LANG_ENGLISH} "Choose an empty folder or the registered DeepSeek Harness installation folder."
LangString INSTALLER_PATH_OWNERSHIP ${LANG_SIMPCHINESE} "请选择空文件夹,或 DeepSeek Harness 原来的安装目录。"
LangString INSTALLER_PATH_WRITABLE ${LANG_ENGLISH} "This location is not writable. Choose a folder available to the current user."
LangString INSTALLER_PATH_WRITABLE ${LANG_SIMPCHINESE} "无法写入此位置。请选择当前用户可写入的文件夹;安装程序不会申请管理员权限。"
LangString INSTALLER_DISK_SPACE ${LANG_ENGLISH} "There is not enough free disk space. Choose another location."
LangString INSTALLER_DISK_SPACE ${LANG_SIMPCHINESE} "此磁盘的可用空间不足,请选择其他位置。"
LangString INSTALLER_PER_USER ${LANG_ENGLISH} "This installer supports the current user only. Uninstall the existing all-users installation first."
LangString INSTALLER_PER_USER ${LANG_SIMPCHINESE} "此安装程序仅支持当前用户。请先卸载已有的所有用户安装版本。"
LangString INSTALLER_EXTRACT_FAILED ${LANG_ENGLISH} "The application files could not be extracted."
LangString INSTALLER_EXTRACT_FAILED ${LANG_SIMPCHINESE} "无法解压应用程序文件。"
LangString INSTALLER_EXTRACT_HINT ${LANG_ENGLISH} "Security software may have blocked a file, or the disk may be full. Close this installer and try again. If it keeps failing, send us the error details below."
LangString INSTALLER_EXTRACT_HINT ${LANG_SIMPCHINESE} "可能是安全软件拦截了文件,或磁盘空间不足。请关闭安装程序后重试;如仍然失败,请将下方错误信息发送给我们。"
LangString INSTALLER_EXTRACT_COPY ${LANG_ENGLISH} "Copy error details"
LangString INSTALLER_EXTRACT_COPY ${LANG_SIMPCHINESE} "复制错误信息"
LangString INSTALLER_EXTRACT_EXPAND ${LANG_ENGLISH} "Show details"
LangString INSTALLER_EXTRACT_EXPAND ${LANG_SIMPCHINESE} "显示详情"
LangString INSTALLER_EXTRACT_COLLAPSE ${LANG_ENGLISH} "Hide details"
LangString INSTALLER_EXTRACT_COLLAPSE ${LANG_SIMPCHINESE} "隐藏详情"
LangString INSTALLER_EXTRACT_SAVED ${LANG_ENGLISH} "The full report was saved to %s"
LangString INSTALLER_EXTRACT_SAVED ${LANG_SIMPCHINESE} "完整错误报告已保存到 %s"
LangString INSTALLER_EXTRACT_UNSAVED ${LANG_ENGLISH} "The full report could not be saved. Use Copy error details to keep it."
LangString INSTALLER_EXTRACT_UNSAVED ${LANG_SIMPCHINESE} "无法保存完整错误报告,请使用“复制错误信息”保留。"
LangString INSTALLER_EXTRACT_COPIED ${LANG_ENGLISH} "Error details were copied to the clipboard."
LangString INSTALLER_EXTRACT_COPIED ${LANG_SIMPCHINESE} "错误信息已复制到剪贴板。"
@@ -1,7 +1,8 @@
import { readInstallerLocales, renderInstallerStrings } from './installer-locales.mjs'
import { officePackageDirectories } from '../../../scripts/libreoffice-packages.mjs'
import { X509Certificate } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
import { readFile, writeFile } from 'node:fs/promises'
import { join, relative, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
import { execFile } from 'node:child_process'
@@ -129,6 +130,7 @@ export function createElectronBuilderConfig(
'-OutputDirectory', join(buildPaths.root, 'installer-ui')], {
env: scrubWindowsSigningEnvironment(env), windowsHide: true,
})
await writeFile(join(buildPaths.root, 'installer-ui', 'strings.nsh'), renderInstallerStrings(readInstallerLocales()), 'utf8')
if (windowsSigner !== undefined) {
await windowsSigner({ path: join(buildPaths.root, 'installer-ui', 'window-frame.dll'), hash: 'sha256', isNest: false })
}
@@ -262,7 +264,7 @@ export function createElectronBuilderConfig(
perMachine: false,
allowElevation: false,
allowToChangeInstallationDirectory: false,
installerLanguages: ['en_US', 'zh_CN'],
installerLanguages: readInstallerLocales().map(locale => locale.id),
differentialPackage: true,
},
detectUpdateChannel: false,
@@ -0,0 +1,25 @@
/** One installer language's compiler identifier, UI strings and NSIS Modern UI test expectations. */
export interface InstallerLocale {
/** electron-builder language and resource filename without its suffix. */
id: string
/** NSIS LANG_ constant suffix. */
nsisLanguage: string
/** Custom installer messages, with Windows printf placeholders preserved. */
strings: Record<string, string>
/** Substrings of the upstream Modern UI uninstaller's welcome and finished pages. */
uninstaller: Record<string, string>
}
/**
* Read complete per-language JSON resources and reject inconsistent keys or placeholders.
* @param directory - Resource directory, defaulting to Desktop installer/locales.
* @returns Validated resources, ordered by filename.
*/
export function readInstallerLocales(directory?: string): InstallerLocale[]
/**
* Generate NSIS LangString declarations from validated resources.
* @param locales - All configured languages or one isolated native-test language.
* @returns UTF-8 NSIS source with literal text escaped.
*/
export function renderInstallerStrings(locales: InstallerLocale[]): string
@@ -0,0 +1,75 @@
/** Per-language JSON resources shared by NSIS packaging, native UI tests and the i18n check. */
import { readFileSync, readdirSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
const defaultDirectory = fileURLToPath(new URL('../installer/locales/', import.meta.url))
/**
* Read and validate installer translations before generating compiler input.
* @param {string} directory - Directory containing one JSON file per installer language.
* @returns {{id: string, nsisLanguage: string, strings: Record<string, string>, uninstaller: Record<string, string>}[]} Complete language resources.
*/
export function readInstallerLocales(directory = defaultDirectory) {
const locales = readdirSync(directory).filter(name => name.endsWith('.json')).sort().map(name => {
if (!/^[a-z]{2}_[A-Z]{2}\.json$/u.test(name)) throw new Error(`${name}: invalid installer language filename`)
/** @type {unknown} */
const resource = JSON.parse(readFileSync(`${directory}/${name}`, 'utf8'))
if (typeof resource !== 'object' || resource === null || !('nsisLanguage' in resource)
|| typeof resource.nsisLanguage !== 'string' || !/^[A-Z]+$/u.test(resource.nsisLanguage)) {
throw new Error(`${name}: invalid NSIS language identifier`)
}
/** @param {unknown} value - Parsed JSON dictionary. @returns {Record<string, string>} Nonempty text values. */
const dictionary = (value) => {
if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error(`${name}: expected text dictionary`)
/** @type {Record<string, string>} */
const result = {}
for (const [key, text] of Object.entries(value)) {
if (typeof text !== 'string' || text.trim() === '') throw new Error(`${name}: ${key} must be nonempty text`)
result[key] = text
}
return result
}
const strings = dictionary('strings' in resource ? resource.strings : undefined)
if (Object.keys(strings).some(key => !/^INSTALLER_[A-Z_]+$/u.test(key))) throw new Error(`${name}: invalid installer string key`)
return { id: name.slice(0, -5), nsisLanguage: resource.nsisLanguage, strings,
uninstaller: dictionary('uninstaller' in resource ? resource.uninstaller : undefined) }
})
for (const id of ['en_US', 'zh_CN', 'ja_JP']) {
if (!locales.some(locale => locale.id === id)) throw new Error(`installer locales: missing ${id}.json`)
}
const english = locales.find(locale => locale.id === 'en_US')
if (!english || Object.keys(english.strings).length === 0) throw new Error('installer locales: missing English strings')
if (Object.keys(english.uninstaller).sort().join(',') !== 'finished,welcome') throw new Error('installer locales: expected uninstaller welcome and finished text')
const languages = new Set()
for (const locale of locales) {
if (languages.has(locale.nsisLanguage)) throw new Error(`${locale.id}: duplicate NSIS language ${locale.nsisLanguage}`)
languages.add(locale.nsisLanguage)
for (const field of ['strings', 'uninstaller']) {
const expected = english[field]
const actual = locale[field]
if (Object.keys(expected).sort().join(',') !== Object.keys(actual).sort().join(',')) throw new Error(`${locale.id}: ${field} keys differ from English`)
for (const [key, text] of Object.entries(expected)) {
const placeholders = (/** @type {string} */ value) => (value.match(/%.?/gu) ?? []).join(',')
if (placeholders(text) !== placeholders(actual[key])) throw new Error(`${locale.id}: ${key} changes format placeholders`)
}
}
}
return locales
}
/**
* Generate guarded NSIS LangString declarations from validated JSON resources.
* @param {ReturnType<typeof readInstallerLocales>} locales - Resources for all languages or one isolated native-test build.
* @returns {string} UTF-8 NSIS source, with literal dollar signs, quotes and line breaks escaped.
*/
export function renderInstallerStrings(locales) {
const escape = (/** @type {string} */ text) => text.replace(/\$|"|\r|\n/gu, character => ({ '$': '$$', '"': '$\\"', '\r': '$\\r', '\n': '$\\n' })[character])
return '; Generated from installer/locales/*.json.\n' + locales.map(locale =>
`!ifdef LANG_${locale.nsisLanguage}\n` + Object.entries(locale.strings).map(([key, text]) =>
`LangString ${key} \${LANG_${locale.nsisLanguage}} "${escape(text)}"\n`).join('') + '!endif\n').join('')
}
if (import.meta.main) {
const locales = readInstallerLocales()
console.log(`verify-installer-locales: ${locales.length} languages have matching keys and format placeholders.`)
}
+1 -1
View File
@@ -9,7 +9,7 @@ ManifestDPIAware true
!endif
!macro customHeader
!define /ifndef INSTALLER_STRINGS_FILE "${INSTALLER_SOURCE_DIR}\strings.nsh"
!define /ifndef INSTALLER_STRINGS_FILE "${INSTALLER_BUILD_DIR}\strings.nsh"
!include "${INSTALLER_STRINGS_FILE}"
!ifdef BUILD_UNINSTALLER
BrandingText " "
@@ -1,4 +1,5 @@
/** Builds an isolated native payload through the production NSIS configuration and exercises its UI. */
import { readInstallerLocales, renderInstallerStrings } from './installer-locales.mjs'
import { execFile } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
@@ -107,8 +108,8 @@ SectionEnd
const compiler = await getMakeNsisPath()
await execute(compiler.path, ['/V2', payloadSource], { ...childOptions, env: { ...childOptions.env, ...compiler.env } })
if (sign) await sign({ path: join(payload, `${productName}.exe`), hash: 'sha256', isNest: false })
const sourceStrings = await readFile(join(appRoot, 'installer', 'strings.nsh'), 'utf8')
for (const language of ['en_US', 'zh_CN']) {
for (const locale of readInstallerLocales()) {
const language = locale.id
const config = createElectronBuilderConfig()
if (sign) {
config.win.forceCodeSigning = true
@@ -117,9 +118,7 @@ SectionEnd
const languageOutput = join(output, language)
await mkdir(languageOutput)
const strings = join(languageOutput, 'strings.nsh')
const languageId = language === 'en_US' ? 'ENGLISH' : 'SIMPCHINESE'
await writeFile(strings, sourceStrings.split('\n').filter((line) =>
!line.startsWith('LangString ') || line.includes(`\${LANG_${languageId}}`)).join('\n'))
await writeFile(strings, renderInstallerStrings([locale]), 'utf8')
const include = join(languageOutput, 'include.nsh')
await writeFile(include, `!define INSTALLER_BUILD_DIR "${join(output, 'ui')}"\n!define INSTALLER_STRINGS_FILE "${strings}"\n!include "${join(appRoot, 'scripts', 'installer.nsh')}"\n`)
await build({ projectDir: appRoot, prepackaged: payload, targets: Platform.WINDOWS.createTarget(['nsis'], Arch.x64), publish: 'never',
@@ -132,7 +131,7 @@ SectionEnd
join(appRoot, 'tests', uninstallOnly ? 'windows-uninstall-smoke.ps1' : 'windows-installer-smoke.ps1'),
'-Installer', join(languageOutput, 'installer-test.exe'),
'-ProductName', productName, '-RegistryKey', guid, '-OutputDirectory', languageOutput,
...uninstallOnly ? ['-Language', languageId, '-PackageName', packageName] : []], childOptions)
'-Language', language, ...uninstallOnly ? ['-PackageName', packageName] : []], childOptions)
process.stdout.write(`${language}\n${result.stdout}`)
}
succeeded = true
@@ -0,0 +1,56 @@
/** Per-language installer resources feed both the packaging language list and the NSIS compiler. */
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect, it, onTestFinished } from 'vitest'
import { readInstallerLocales, renderInstallerStrings } from '../scripts/installer-locales.mjs'
function fixture(): string {
const directory = mkdtempSync(join(tmpdir(), 'dsh-installer-locales-'))
onTestFinished(() => { rmSync(directory, { recursive: true, force: true }) })
for (const id of ['en_US', 'zh_CN', 'ja_JP']) {
writeFileSync(join(directory, `${id}.json`), readFileSync(new URL(`../installer/locales/${id}.json`, import.meta.url)))
}
return directory
}
it('generates Japanese NSIS strings from the same files that select packaged languages', () => {
const locales = readInstallerLocales()
expect(locales.map(locale => locale.id)).toEqual(['en_US', 'ja_JP', 'zh_CN'])
const source = renderInstallerStrings(locales)
expect(source).toContain('LangString INSTALLER_INSTALL ${LANG_JAPANESE} "今すぐインストール"')
expect(source).toContain('LangString INSTALLER_NATIVE_PROGRESS ${LANG_JAPANESE} "インストールを準備しています… %d%%"')
expect(source).toContain('!ifdef LANG_JAPANESE\n')
expect(source.match(/^LangString /gmu)).toHaveLength(93)
const japanese = locales.filter(locale => locale.id === 'ja_JP')
expect(renderInstallerStrings(japanese)).not.toContain('${LANG_ENGLISH}')
})
it.each(['en_US', 'zh_CN', 'ja_JP'])('rejects a missing %s configuration', (id) => {
const directory = fixture()
rmSync(join(directory, `${id}.json`))
expect(() => readInstallerLocales(directory)).toThrow(`missing ${id}.json`)
})
it.each([
['missing key', (resource: ReturnType<typeof readInstallerLocales>[number]) => { delete resource.strings.INSTALLER_INSTALL }, 'keys differ'],
['extra key', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.strings.INSTALLER_EXTRA = 'extra' }, 'keys differ'],
['empty text', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.strings.INSTALLER_INSTALL = '' }, 'nonempty text'],
['changed placeholder', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.strings.INSTALLER_NATIVE_PROGRESS = '%s%%' }, 'format placeholders'],
['missing percent escape', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.strings.INSTALLER_NATIVE_PROGRESS = '%d%' }, 'format placeholders'],
['extra format specifier', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.strings.INSTALLER_NATIVE_PROGRESS = '%d%% %q' }, 'format placeholders'],
['duplicate language', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.nsisLanguage = 'ENGLISH' }, 'duplicate NSIS language'],
['invalid compiler token', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.nsisLanguage = 'JA\n!error' }, 'invalid NSIS language'],
])('rejects %s', (_name, mutate, diagnostic) => {
const directory = fixture()
const resource = readInstallerLocales(directory).find(locale => locale.id === 'ja_JP')!
mutate(resource)
writeFileSync(join(directory, 'ja_JP.json'), JSON.stringify(resource))
expect(() => readInstallerLocales(directory)).toThrow(diagnostic)
})
it('escapes literal NSIS syntax without changing translated text', () => {
const locale = readInstallerLocales()[0]!
expect(renderInstallerStrings([{ ...locale, strings: { INSTALLER_INSTALL: '"$name"\nnext\rline' } }]))
.toContain('"$\\"$$name$\\"$\\nnext$\\rline"')
})
+12 -1
View File
@@ -5,13 +5,17 @@ import { AppInfo, Packager } from 'app-builder-lib'
import { expandMacro } from 'app-builder-lib/out/util/macroExpander.js'
import { describe, expect, it, vi } from 'vitest'
const { execute } = vi.hoisted(() => ({ execute: vi.fn(async () => undefined) }))
const { execute, write } = vi.hoisted(() => ({ execute: vi.fn(async () => undefined), write: vi.fn(async () => undefined) }))
vi.mock('node:child_process', async (importOriginal) => {
const original = await importOriginal<typeof import('node:child_process')>()
const { promisify } = await import('node:util')
return { ...original, execFile: Object.assign(vi.fn(), { [promisify.custom]: execute }) }
})
vi.mock('node:fs/promises', async importOriginal => ({
...await importOriginal<typeof import('node:fs/promises')>(), writeFile: write,
}))
describe('installer preparation preserves application dependencies', () => {
it.each(['win32', 'darwin'] as const)('rejects a missing production policy before signing on %s', async (platform) => {
const { createElectronBuilderConfig } = await import('../scripts/electron-builder-config.mjs')
@@ -22,6 +26,7 @@ describe('installer preparation preserves application dependencies', () => {
})
it.each(['win32', 'darwin'] as const)('preserves installer metadata and dependency ownership on %s', async (platform) => {
execute.mockClear()
write.mockClear()
const env = {
DSH_DESKTOP_APP_ID: 'com.example.installer',
DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN: 'https://policy.example.com',
@@ -49,6 +54,7 @@ describe('installer preparation preserves application dependencies', () => {
expect(appInfo.copyright).toBe(`Copyright © ${new Date().getFullYear()} DeepSeek Harness`)
if (platform === 'win32') {
expect(expandMacro(config.nsis.uninstallDisplayName, null, appInfo, {}, false)).toBe('DeepSeek Harness')
expect(config.nsis.installerLanguages).toEqual(['en_US', 'ja_JP', 'zh_CN'])
}
const aboutIcon = config.extraResources.find(resource => resource.to === 'icon.png')
expect(aboutIcon).toBeDefined()
@@ -71,6 +77,11 @@ describe('installer preparation preserves application dependencies', () => {
await packager.installAppDependencies(process.platform === 'win32' ? Platform.LINUX : Platform.WINDOWS, Arch.x64)
expect(packager.areNodeModulesHandledExternally).toBe(false)
expect(execute).toHaveBeenCalledTimes(platform === 'win32' ? 1 : 0)
expect(write).toHaveBeenCalledTimes(platform === 'win32' ? 1 : 0)
if (platform === 'win32') {
expect(write).toHaveBeenCalledWith(expect.stringMatching(/installer-ui[\\/]strings\.nsh$/u),
expect.stringContaining('LangString INSTALLER_INSTALL ${LANG_JAPANESE}'), 'utf8')
}
} finally {
vi.unstubAllEnvs()
vi.restoreAllMocks()
@@ -2,7 +2,7 @@
[CmdletBinding()]
param([Parameter(Mandatory)][string]$Installer, [Parameter(Mandatory)][string]$ProductName,
[Parameter(Mandatory)][string]$RegistryKey,
[Parameter(Mandatory)][string]$OutputDirectory)
[Parameter(Mandatory)][string]$OutputDirectory, [Parameter(Mandatory)][string]$Language)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'windows-installer-ui.ps1')
Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes
@@ -14,12 +14,8 @@ $uninstaller = Join-Path $installPath ('Uninstall ' + $ProductName + '.exe')
$processes = [Collections.Generic.List[Diagnostics.Process]]::new()
$results = [Collections.Generic.List[string]]::new()
$expected = Get-Content (Join-Path $PSScriptRoot 'expected/windows-installer.json') -Raw | ConvertFrom-Json
$localizedCopy = @{ ENGLISH = @{}; SIMPCHINESE = @{} }
Get-Content (Join-Path $PSScriptRoot '../installer/strings.nsh') -Encoding UTF8 | ForEach-Object {
if ($_ -match '^LangString (INSTALLER_\w+) \$\{LANG_(ENGLISH|SIMPCHINESE)\} "(.*)"$') {
$localizedCopy[$Matches[2]][$Matches[1]] = $Matches[3]
}
}
$resource = Get-Content (Join-Path $PSScriptRoot "../installer/locales/$Language.json") -Encoding UTF8 -Raw | ConvertFrom-Json
$localizedCopy = @{ $Language = $resource.strings }
function Wait-Control([Diagnostics.Process]$Process, [string]$Text, [switch]$Dialog) {
$timer = [Diagnostics.Stopwatch]::StartNew()
do {
@@ -4,6 +4,7 @@ param([Parameter(Mandatory)][string]$Installer, [Parameter(Mandatory)][string]$P
[Parameter(Mandatory)][string]$RegistryKey, [Parameter(Mandatory)][string]$OutputDirectory,
[Parameter(Mandatory)][string]$Language, [Parameter(Mandatory)][string]$PackageName)
$ErrorActionPreference = 'Stop'
$copy = (Get-Content (Join-Path $PSScriptRoot "../installer/locales/$Language.json") -Encoding UTF8 -Raw | ConvertFrom-Json).uninstaller
$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)
. (Join-Path $PSScriptRoot 'windows-installer-ui.ps1')
[InstallerCapture]::Initialize()
@@ -69,12 +70,12 @@ try {
if ($mode -eq 'home-inside') { $env:DSH_HOME = Join-Path $userDataPath 'home' } else { Remove-Item Env:DSH_HOME -ErrorAction SilentlyContinue }
$process = Start-Process -FilePath $uninstaller -ArgumentList $arguments -PassThru -WindowStyle ($(if ($mode -eq 'interactive') { 'Normal' } else { 'Hidden' }))
if ($mode -eq 'interactive') {
$welcomeText = if ($Language -eq 'ENGLISH') { 'Welcome' } else { -join [char[]]@(0x6B22, 0x8FCE) }
$welcomeText = $copy.welcome
$welcome = Wait-Text $welcomeText
$window = [InstallerCapture]::TopLevel($welcome)
[void][InstallerCapture]::SaveNative($window, (Join-Path $OutputDirectory 'uninstall-welcome.png'), $false)
[InstallerCapture]::Click([InstallerCapture]::GetDlgItem($window, 1))
$finishedText = if ($Language -eq 'ENGLISH') { 'has been uninstalled' } else { -join [char[]]@(0x5DF2, 0x4ECE, 0x4F60) }
$finishedText = $copy.finished
[void](Wait-Text $finishedText)
[InstallerCapture]::Click([InstallerCapture]::GetDlgItem($window, 1))
}
+5 -2
View File
@@ -103,14 +103,17 @@ describe.skipIf(MODE === 'record')('web e2e: native and Client locale preference
await jaDialog.getByRole('button', { name: '日本語', exact: true }).waitFor()
await expect.poll(() => reported).toEqual(['ja'])
expect(await page.evaluate(() => document.documentElement.lang)).toBe('ja')
await page.setViewportSize({ width: 390, height: 844 })
const overflows = (): Promise<number> => jaDialog.evaluate((dialog) => {
const containers = [dialog, ...dialog.querySelectorAll<HTMLElement>('*')]
// The narrow navigation strip scrolls horizontally by design; inspect the panel and section content.
const options = dialog.lastElementChild!
const containers = [dialog, options, ...options.querySelectorAll<HTMLElement>('*')]
.filter(element => element === dialog || ['auto', 'scroll'].includes(getComputedStyle(element).overflowX))
return containers.filter(element => element.clientWidth > 0 && element.scrollWidth > element.clientWidth + 1).length
})
await expect.poll(overflows).toBe(0)
await jaDialog.evaluate((dialog) => {
const options = dialog.querySelector('nav')!.nextElementSibling!.lastElementChild!
const options = dialog.lastElementChild!
const probe = document.createElement('div')
probe.dataset.localeOverflowProbe = ''
probe.style.width = '2000px'
@@ -76,10 +76,11 @@
- text: 共 2 个 · 2 运行中
- list:
- listitem:
- text: "@deepseek-ai/dsh-experimental-translator Machine translation with reusable Session results"
- text: 翻译服务 提供机器翻译,并复用会话中已保存的翻译结果。
- code: translator
- code: "@deepseek-ai/dsh-experimental-translator"
- text: 运行中
- switch "启用组件 @deepseek-ai/dsh-experimental-translator" [checked]
- switch "启用组件 翻译服务" [checked]
- listitem:
- text: 思考过程机器翻译 将展开的思考内容机器翻译为其他语言。
- code: cot-translation
@@ -73,10 +73,11 @@
- text: 共 2 个 · 2 运行中
- list:
- listitem:
- text: "@deepseek-ai/dsh-experimental-translator Machine translation with reusable Session results"
- text: 翻译服务 提供机器翻译,并复用会话中已保存的翻译结果。
- code: translator
- code: "@deepseek-ai/dsh-experimental-translator"
- text: 运行中
- switch "启用组件 @deepseek-ai/dsh-experimental-translator" [checked]
- switch "启用组件 翻译服务" [checked]
- listitem:
- text: 思考过程机器翻译 将展开的思考内容机器翻译为其他语言。
- code: cot-translation
@@ -0,0 +1,5 @@
- listitem:
- button "自動認可レビューを表示": 自動認可レビュー
- text: 実験的 各ツール呼び出しの前にモデルが認可を判断する、自動認可レビュー権限モードを提供します。
- status
- switch "自動認可レビューを有効化"
@@ -19,6 +19,7 @@
- text: .
- math: π 4 < θ < π 2
- math: θ ∈ ( π 4 , π 2 ) . (1)
- math: α + β γ
- table:
- rowgroup:
- row "Symbol Value":
@@ -0,0 +1,32 @@
/** Japanese package resources reach the real plugin manager through Host metadata and Client locale resolution. */
import { fileURLToPath } from 'node:url'
import { chromium, type Browser } from 'playwright'
import { expect, it, onTestFailed, onTestFinished } from 'vitest'
import { captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
import { saveFailureShot } from './support.ts'
it('displays Japanese plugin titles and descriptions from package metadata', async () => {
const resources: { scaffold?: WebScaffold; browser?: Browser } = {}
onTestFinished(async () => {
try { await resources.browser?.close() } finally { await resources.scaffold?.close() }
})
const scaffold = await launchWebScaffold()
resources.scaffold = scaffold
const browser = await chromium.launch()
resources.browser = browser
const page = await browser.newPage({ locale: 'ja-JP', viewport: { width: 1440, height: 960 } })
const tripwire = watchConsole(page)
onTestFailed(() => saveFailureShot(page, 'japanese-plugin-metadata'))
await page.goto(scaffold.authenticatedUrl)
await page.getByRole('button', { name: 'プラグイン', exact: true }).click()
const panel = page.locator('[data-plugin-panel]')
const title = panel.getByRole('button', { name: '自動認可レビューを表示', exact: true })
await title.waitFor()
const card = title.locator('xpath=ancestor::li')
await card.getByText('各ツール呼び出しの前にモデルが認可を判断する、自動認可レビュー権限モードを提供します。', { exact: true }).waitFor()
const expected = fileURLToPath(new URL('./expected/japanese-plugin-metadata/card.expected.md', import.meta.url))
await compareOrRefreshGolden(expected,
await captureStableAria(page, '[data-plugin-panel] li:has(button[aria-label="自動認可レビューを表示"])', scaffold.workspaceCwd), webSnapshotMode())
expect(tripwire.warnings).toEqual([])
expect(tripwire.pageErrors).toEqual([])
})
+5 -2
View File
@@ -56,6 +56,9 @@ function mathFixture(): string {
'',
'$$\\theta \\in \\left(\\frac{\\pi}{4}, \\frac{\\pi}{2}\\right). \\tag{1}$$',
'',
'$$\\alpha+\\beta',
'\\gamma$$',
'',
'| Symbol | Value |',
'| --- | --- |',
'| $\\theta$ | \\(\\frac{1}{5}\\) |',
@@ -119,8 +122,8 @@ describe('web e2e: settled Markdown math rendering', () => {
await sessionRow.click()
await expect.poll(() => page.getByText(DONE, { exact: true }).count(), { timeout: 15_000 }).toBe(1)
await expect.poll(() => page.locator('.katex').count(), { timeout: 10_000 }).toBe(6)
await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(2)
await expect.poll(() => page.locator('.katex').count(), { timeout: 10_000 }).toBe(7)
await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(3)
expect(await page.locator('.katex-error').count()).toBe(0)
const fonts = await page.evaluate(async () => {
const faces = [...document.fonts].filter(face => face.family.includes('KaTeX_'))
+1
View File
@@ -23,6 +23,7 @@
// this client-registered project must not also hold them (one program
// cannot see both sides of the cordis Context merges).
"exclude": [
"tests/japanese-plugin-metadata.e2e.ts",
"tests/rich-text-quotes.e2e.ts",
"tests/hover-input.e2e.ts",
"tests/collapse-timing.e2e.ts",
+2 -2
View File
@@ -21,8 +21,8 @@
en: 0894bd9f35430753
zh: 2d28090e71134edd
/cookbook-adding-a-workspace-package/5-add-optional-plugin-display-metadata:
en: cb799f5d561dd917
zh: 7739af36ef8ffb3f
en: 1238eaea376391d1
zh: c7e606f95d8980bf
/cookbook-adding-a-workspace-package/6-verify:
en: 80c7fb90b1175aae
zh: 2ba37962dbc57299
+1 -1
View File
@@ -136,7 +136,7 @@ Merge these entries into `package.json`, retaining existing runtime exports and
}
```
Keep language files together in `locale/`, with `en.json` as the discovery entry. Fields are optional; present values must be non-empty strings. English `meta.title` and `meta.description` use sentence case: capitalize only the first word, proper nouns, and product names, as in `Git worktrees` ([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location)). Missing files or fields permit fallback, while malformed JSON or invalid fields produce a per-plugin diagnostic.
Keep language files together in `locale/`, with `en.json` as the discovery entry. Fields are optional; present values must be non-empty strings. English `meta.title` and `meta.description` use sentence case: capitalize only the first word, proper nouns, and product names, as in `Git worktrees` ([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location)). At runtime, missing files or fields permit fallback, while malformed JSON or invalid fields produce a per-plugin diagnostic. First-party packages and plugin templates must provide `ja.json` covering every field present in `en.json` or optional `zh.json`, preserving placeholders; detail paths may point to the existing English document. Subpath resources under `locale/<subpath>/` follow the same checks. English descriptions explain user-visible behavior and end with sentence punctuation. Run `pnpm run check:i18n` to verify JSON metadata and UI dictionary completeness; the JSON checks also run in `check:all`, CI static checks, and hygiene.
Fields fall back independently before view-specific name formatting, using the existing locale language chain first:
+1 -1
View File
@@ -136,7 +136,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex
}
```
语言文件统一放在 `locale/`,并保留 `en.json` 作为发现入口。字段可选,填写时必须是非空字符串。英文 `meta.title` 和 `meta.description` 使用 sentence case:只大写首个单词、专有名词和产品名,例如 `Git worktrees`([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location))。文件或字段缺失时允许回退,JSON 损坏或字段无效时显示单插件诊断。
语言文件统一放在 `locale/`,并保留 `en.json` 作为发现入口。字段可选,填写时必须是非空字符串。英文 `meta.title` 和 `meta.description` 使用 sentence case:只大写首个单词、专有名词和产品名,例如 `Git worktrees`([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location))。运行时文件或字段缺失时允许回退,JSON 损坏或字段无效时显示单插件诊断。仓库内的包和插件模板必须提供 `ja.json`,覆盖 `en.json` 或可选 `zh.json` 中的全部字段并保留占位符;详情路径可指向现有英文文档。`locale/<subpath>/` 下的子路径资源使用相同检查。英文描述面向产品用户说明可见行为,并以句末标点结束。运行 `pnpm run check:i18n` 检查 JSON 元数据和 UI 字典完整性;JSON 检查也接入 `check:all`、CI 静态检查和 hygiene。
各字段先独立回退,再按页面规则格式化技术名称;回退优先经过现有 locale 语言链:
+3
View File
@@ -146,6 +146,9 @@
"verify-package-dependencies": "tsx scripts/verify-package-dependencies.ts",
"verify-npm-install-layout": "tsx scripts/verify-npm-install-layout.ts",
"verify-client-packages": "tsx scripts/verify-client-packages.ts",
"check:i18n": "tsx scripts/run-gates.ts i18n",
"verify-installer-locales": "node apps/desktop/scripts/installer-locales.mjs",
"verify-locale-metadata": "tsx scripts/verify-locale-metadata.ts",
"verify-client-ui-i18n": "tsx scripts/verify-client-ui-i18n.ts",
"verify-client-route-resolution": "tsx scripts/verify-client-route-resolution.ts",
"verify-no-bare-dispatcher": "tsx scripts/verify-no-bare-dispatcher.ts",
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ACP server",
"description": "Run agents through JSON-RPC over standard input and output for automation."
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ACP サーバー",
"description": "JSON-RPC の標準入出力でエージェントを操作する、自動化専用の ACP サーバーです。"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ACP 服务",
"description": "通过 JSON-RPC 标准输入输出驱动智能体,仅供自动化使用。"
}
}
+2
View File
@@ -19,9 +19,11 @@
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./locale/*.json": "./locale/*.json",
"./package.json": "./package.json"
},
"files": [
"locale/*.json",
"lib/index.js",
"lib/types/**/*.d.ts"
],
@@ -0,0 +1,6 @@
{
"meta": {
"title": "アカウント操作 API",
"description": "認証済みクライアントにアカウント操作機能を提供します。"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "リモート呼び出しゲートウェイ",
"description": "クライアントからの API 呼び出しを受け取り、対応するサービスに転送します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "バックグラウンドタスク監視 API",
"description": "バックグラウンドタスクの状態と出力をクライアントに継続して提供します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "Remote access control",
"description": "Control which methods non-owner connections can access and retain permission snapshots for each connection."
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "リモートアクセス制御",
"description": "所有者以外の接続にメソッド単位のリモートアクセス制御と接続ごとの権限スナップショットを提供します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "远程访问控制",
"description": "为非所有者连接提供方法级远程访问控制和各连接的权限快照。"
}
}
+2
View File
@@ -35,6 +35,7 @@
"default": "./lib/typert.remote-client.js"
},
"./src/*": "./src/*",
"./locale/*.json": "./locale/*.json",
"./package.json": "./package.json"
},
"dsh": {
@@ -54,6 +55,7 @@
"watch": "tsdown --watch"
},
"files": [
"locale/*.json",
"lib/index.js",
"lib/client.js",
"lib/types/**/*.js",
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "クライアント API 一覧",
"description": "アプリがクライアントに公開するサーバー機能をまとめます。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "セッション制御 API",
"description": "セッションの読み取り、実行制御、状態更新をクライアントに提供します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "設定読み書き API",
"description": "各種設定の読み取りと変更をクライアントに提供します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ターミナル制御 API",
"description": "セッションのターミナルの作成、入力、読み取り、復元をクライアントに提供します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ワークスペース制御 API",
"description": "ワークスペース操作と、再接続後に復元できる状態をクライアントに提供します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ワークスペースファイル API",
"description": "ワークスペース内のファイルの読み取り、ディレクトリ一覧、ファイル情報の更新を提供します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ローカル添付ファイル保存",
"description": "セッション内の画像などの添付ファイルを保存し、コンテンツ ID で読み取ります。"
}
}
@@ -7,16 +7,16 @@ export const OFFICIAL_ON_DEMAND_CATALOG: readonly OfficialBundleCatalogEntry[] =
{
packageName: '@deepseek-ai/dsh-subagent-claude-code',
meta: {
title: { 'en': 'Claude Code subagent', 'zh': 'Claude Code \u5B50\u667A\u80FD\u4F53' },
description: { 'en': 'Delegate one task to Claude Code using its native authentication and settings.', 'zh': '\u4F7F\u7528 Claude Code \u539F\u751F\u8BA4\u8BC1\u548C\u8BBE\u7F6E\u59D4\u6D3E\u5355\u4E2A\u4EFB\u52A1\u3002' },
title: { 'en': 'Claude Code subagent', 'ja': 'Claude Code \u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8', 'zh': 'Claude Code \u5B50\u667A\u80FD\u4F53' },
description: { 'en': 'Delegate one task to Claude Code using its native authentication and settings.', 'ja': 'Claude Code \u306E\u30CD\u30A4\u30C6\u30A3\u30D6\u306E\u8A8D\u8A3C\u3068\u8A2D\u5B9A\u3092\u4F7F\u7528\u3057\u3066\u3001\u5358\u72EC\u306E\u30BF\u30B9\u30AF\u3092\u59D4\u6D3E\u3057\u307E\u3059\u3002', 'zh': '\u4F7F\u7528 Claude Code \u539F\u751F\u8BA4\u8BC1\u548C\u8BBE\u7F6E\u59D4\u6D3E\u5355\u4E2A\u4EFB\u52A1\u3002' },
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNC45ODkzIiB5PSIxNS4xODc3IiB3aWR0aD0iMTIuMjM2NSIgaGVpZ2h0PSIxMi4yMzY1IiByeD0iMiIgZmlsbD0idXJsKCNkc2hfcGx1Z2luX2FydF9SMGEpIiBmaWxsLW9wYWNpdHk9IjAuOCI+PC9yZWN0PjxyZWN0IHg9IjguODcxMDkiIHk9IjkuMDY5NyIgd2lkdGg9IjEyLjIzNjUiIGhlaWdodD0iMTIuMjM2NSIgcng9IjIiIGZpbGw9InVybCgjZHNoX3BsdWdpbl9hcnRfUjBiKSIgZmlsbC1vcGFjaXR5PSIwLjgiPjwvcmVjdD48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYSIgeDE9IjIxLjEwNzUiIHkxPSIxNS4xODc3IiB4Mj0iMjEuMTA3NSIgeTI9IjI3LjQyNDMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjNDVFN0E0Ij48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDU5MDlEIj48L3N0b3A+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYiIgeDE9IjE0Ljk4OTQiIHkxPSI5LjA2OTciIHgyPSIxNC45ODk0IiB5Mj0iMjEuMzA2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiM2OUI5RkYiPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMjRERTIiPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4K',
},
},
{
packageName: '@deepseek-ai/dsh-subagent-codex',
meta: {
title: { 'en': 'Codex subagent', 'zh': 'Codex \u5B50\u667A\u80FD\u4F53' },
description: { 'en': 'Delegate one task to Codex using its native authentication and settings.', 'zh': '\u4F7F\u7528 Codex \u539F\u751F\u8BA4\u8BC1\u548C\u8BBE\u7F6E\u59D4\u6D3E\u5355\u4E2A\u4EFB\u52A1\u3002' },
title: { 'en': 'Codex subagent', 'ja': 'Codex \u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8', 'zh': 'Codex \u5B50\u667A\u80FD\u4F53' },
description: { 'en': 'Delegate one task to Codex using its native authentication and settings.', 'ja': 'Codex \u306E\u30CD\u30A4\u30C6\u30A3\u30D6\u306E\u8A8D\u8A3C\u3068\u8A2D\u5B9A\u3092\u4F7F\u7528\u3057\u3066\u3001\u5358\u72EC\u306E\u30BF\u30B9\u30AF\u3092\u59D4\u6D3E\u3057\u307E\u3059\u3002', 'zh': '\u4F7F\u7528 Codex \u539F\u751F\u8BA4\u8BC1\u548C\u8BBE\u7F6E\u59D4\u6D3E\u5355\u4E2A\u4EFB\u52A1\u3002' },
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNC45ODkzIiB5PSIxNS4xODc3IiB3aWR0aD0iMTIuMjM2NSIgaGVpZ2h0PSIxMi4yMzY1IiByeD0iMiIgZmlsbD0idXJsKCNkc2hfcGx1Z2luX2FydF9SMGEpIiBmaWxsLW9wYWNpdHk9IjAuOCI+PC9yZWN0PjxyZWN0IHg9IjguODcxMDkiIHk9IjkuMDY5NyIgd2lkdGg9IjEyLjIzNjUiIGhlaWdodD0iMTIuMjM2NSIgcng9IjIiIGZpbGw9InVybCgjZHNoX3BsdWdpbl9hcnRfUjBiKSIgZmlsbC1vcGFjaXR5PSIwLjgiPjwvcmVjdD48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYSIgeDE9IjIxLjEwNzUiIHkxPSIxNS4xODc3IiB4Mj0iMjEuMTA3NSIgeTI9IjI3LjQyNDMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjNDVFN0E0Ij48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDU5MDlEIj48L3N0b3A+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYiIgeDE9IjE0Ljk4OTQiIHkxPSI5LjA2OTciIHgyPSIxNC45ODk0IiB5Mj0iMjEuMzA2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiM2OUI5RkYiPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMjRERTIiPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4K',
},
},
@@ -0,0 +1,6 @@
{
"meta": {
"title": "プラグイン設定編集",
"description": "プラグイン設定を保存し、変更を現在の実行設定に適用します。"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "プラグインホットリロード",
"description": "アプリの実行中にプラグインのコードと設定を再読み込みします。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "プラグイン管理サービス",
"description": "現在の実行設定にあるプラグインとパッケージを管理し、インストール、削除、有効化、無効化を行います。"
}
}
@@ -0,0 +1,5 @@
{
"meta": {
"title": "プラグイン管理ツール"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "Plugin market indexes",
"description": "Manage configured market indexes for plugin discovery and the plugin_market tool."
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "プラグインマーケットのインデックス",
"description": "プラグイン検索ページと plugin_market ツールで使う、設定済みのマーケットのインデックスを管理します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "插件市场索引",
"description": "为插件发现页面和 plugin_market 工具管理已配置的市场索引。"
}
}
+2
View File
@@ -35,9 +35,11 @@
"default": "./lib/typert.remote-client.js"
},
"./src/*": "./src/*",
"./locale/*.json": "./locale/*.json",
"./package.json": "./package.json"
},
"files": [
"locale/*.json",
"lib/index.js",
"lib/worker.cjs",
"lib/types/**/*.js",
@@ -0,0 +1,6 @@
{
"meta": {
"title": "Browser provider registry",
"description": "Register browser automation providers by name, with one provider per name."
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ブラウザ提供元レジストリ",
"description": "名前ごとに排他的なブラウザ操作の提供元を登録します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "浏览器服务注册",
"description": "按名称注册独占的浏览器操作提供方。"
}
}
@@ -19,6 +19,7 @@
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./locale/*.json": "./locale/*.json",
"./package.json": "./package.json",
"./brand": {
"types": "./lib/types/brand.d.ts",
@@ -26,6 +27,7 @@
}
},
"files": [
"locale/*.json",
"lib/index.js",
"lib/types/**/*.js",
"lib/types/**/*.d.ts"
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ACP profile",
"description": "Provide automation-only ACP communication and process lifecycle management over dsh-base."
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ACP プロファイル",
"description": "dsh-base 上で自動化専用の ACP 通信とプロセスのライフサイクルを提供します。"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ACP 运行配置",
"description": "在 dsh-base 上提供自动化专用的 ACP 通信与进程生命周期。"
}
}
+2
View File
@@ -20,9 +20,11 @@
},
"./cordis.patch.yml": "./cordis.patch.yml",
"./src/*": "./src/*",
"./locale/*.json": "./locale/*.json",
"./package.json": "./package.json"
},
"files": [
"locale/*.json",
"lib/index.js",
"cordis.patch.yml",
"lib/types/**/*.d.ts"
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "Headless profile",
"description": "Run a single task directly with the core Agent/Session runner over dsh-base, without a Host, HTTP, or browser layer."
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ヘッドレスプロファイル",
"description": "dsh-base 上でコアの Agent/Session ランナーを直接使用して単一タスクを実行し、Host、HTTP、ブラウザのレイヤーを含みません。"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "无界面运行配置",
"description": "在 dsh-base 上直接使用核心 Agent/Session 执行单次任务,不包含 Host、HTTP 或浏览器层。"
}
}
+2
View File
@@ -24,9 +24,11 @@
},
"./cordis.patch.yml": "./cordis.patch.yml",
"./src/*": "./src/*",
"./locale/*.json": "./locale/*.json",
"./package.json": "./package.json"
},
"files": [
"locale/*.json",
"lib/index.js",
"lib/startup.js",
"cordis.patch.yml",
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "SDK profile",
"description": "Provide JSON-RPC over standard input and output and manage the process lifecycle over dsh-base."
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "SDK プロファイル",
"description": "dsh-base 上で標準入出力の JSON-RPC とプロセスのライフサイクル管理を提供します。"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "SDK 运行配置",
"description": "在 dsh-base 上提供标准输入输出 JSON-RPC 与进程生命周期管理。"
}
}
+2
View File
@@ -20,9 +20,11 @@
},
"./cordis.patch.yml": "./cordis.patch.yml",
"./src/*": "./src/*",
"./locale/*.json": "./locale/*.json",
"./package.json": "./package.json"
},
"files": [
"locale/*.json",
"lib/index.js",
"cordis.patch.yml",
"lib/types/**/*.d.ts"
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "Web アプリ実行サポート",
"description": "フロントエンドのページリソースを提供し、Web アプリに必要な実行設定を接続します。"
}
}
@@ -0,0 +1,5 @@
{
"meta": {
"title": "Web 起動設定"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "クライアント接続",
"description": "認証済みクライアントとの通信を確立し、接続のライフサイクルを管理します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ファイルアップロード",
"description": "ブラウザ内のファイルを現在のセッションにアップロードし、メッセージやツールで使用できるようにします。"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "Web クライアントホット更新",
"description": "ブラウザ内のプラグインを同期し、コードの再ビルド後に関連する UI を更新します。"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "表示言語",
"description": "表示言語の設定と各プラグインの翻訳文を管理します。"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "クライアントモジュール読み込み",
"description": "ブラウザのプラグインモジュールと読み込み依存関係を管理します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "デスクトップ利用統計",
"description": "デスクトップの指定された操作イベントを収集し、製品の統計サービスに送信します。"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "クライアントリソースサービス",
"description": "ファイルなどのリソースアドレスを、UI が読み取れるコンテンツに解決します。"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ショートカットサービス",
"description": "アプリのショートカットを登録し、キー操作を対応する機能に振り分けます。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "エージェントプリセット UI",
"description": "現在および新規セッションのエージェントプリセットを選択し、使用するプラグインを編集します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "操作承認 UI",
"description": "承認待ちの操作を表示し、ユーザーの選択を実行中のタスクに返します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "添付ファイル表示",
"description": "メッセージ入力、会話、実行記録に添付ファイルと画像を表示します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "DeepSeek ブランド表示",
"description": "アプリのサイドバーなどに公式ブランドの要素を表示します。"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "チャット表示",
"description": "チャットメッセージ、ツールの内容、関連する詳細を描画します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "コマンド選択 UI",
"description": "入力欄にスラッシュコマンドのメニューと関連する操作 UI を提供します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "会話 UI フレームワーク",
"description": "会話内容、入力欄、メッセージキュー、ビューのナビゲーションを構成します。"
}
}
@@ -772,6 +772,7 @@ export const en = {
/** Japanese copy, checked against the English keys. */
export const ja = {
'quote.remove': '引用を削除',
'shortcut.newline': '改行',
'shortcut.complementary': '補助のキュー / 実行中の指示変更を使用',
'shortcut.slash': 'コマンドメニューを開く',
@@ -1095,9 +1096,9 @@ export const ja = {
'tool.title.glob': 'Glob',
'tool.title.webSearch': '検索',
'tool.title.webFetch': '取得',
'tool.autoReviewRejected': '自動レビューにより拒否されました',
'tool.autoReviewRejected': '自動認可レビューにより拒否されました',
'tool.autoReviewNotExecuted': 'ツールは実行されませんでした。理由: {reason}',
'tool.autoReviewReasonFallback': '自動レビューがこの操作を承認しませんでした',
'tool.autoReviewReasonFallback': '自動認可レビューがこの操作を承認しませんでした',
'diff.collapseAria': '差分を折りたたむ',
'diff.expandAria': '差分の残り{count}行を表示',
'diff.expandRest': '… 残り{count}行',
@@ -0,0 +1,6 @@
{
"meta": {
"title": "DeepSeek 1017 model badge",
"description": "Add a temporary 1017 release badge to official DeepSeek Flash model names."
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "DeepSeek 1017 モデルバッジ",
"description": "公式 DeepSeek Flash モデルの名前に、一時的な 1017 リリースバッジを表示します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "DeepSeek 1017 模型标记",
"description": "为官方 DeepSeek Flash 模型名称添加临时的 1017 版本标记。"
}
}
@@ -23,8 +23,8 @@
"default": "./lib/client.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json",
"./locale/*.json": "./locale/*.json"
"./locale/*.json": "./locale/*.json",
"./package.json": "./package.json"
},
"dsh": {
"client": {
@@ -0,0 +1,6 @@
{
"meta": {
"title": "ファイル変更と成果物の表示",
"description": "変更されたファイル、内容の比較、成果物ファイルへの入口を表示します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "アプリ内ディレクトリ選択 UI",
"description": "アプリ内でワークスペースのディレクトリを参照、作成、選択します。"
}
}
@@ -0,0 +1,6 @@
{
"meta": {
"title": "システムディレクトリ選択 UI",
"description": "ワークスペースの選択操作をシステムのディレクトリ選択ダイアログに接続します。"
}
}
Loaded 100 of 517 files, more files were not shown because too many files have changed in this diff. Show more