@ragnoryok1/dsh-client-locale-ru 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,33 @@
6
6
  Пакет следует за харнессом: целевая версия — `dsh 0.2.1-alpha.1` и новее
7
7
  (`>=0.1.0-rc.2 <0.3.0` для `@deepseek-ai/dsh-client-locale`).
8
8
 
9
+ ## [0.2.8] — 2026-10-19
10
+
11
+ ### Добавлено
12
+
13
+ - **Названия плагинов переводятся без правки харнесса.** Название плагина приходит из его
14
+ собственного файла `locale/<язык>.json`, а служба локализации разрешает эту карту, не
15
+ обращаясь к зарегистрированным словарям, — поэтому словарь мог перевести строки плагина,
16
+ но не его имя. Пакет несёт 32 названия сам: английский текст, из которого сделан перевод,
17
+ и русский. Отрисованный интерфейс проходит через проход, который подставляет русский на
18
+ место английского: карточки плагинов, строки состава bundle, страница плагина, реестр в
19
+ настройках, имя для скринридера. Проход берёт только точное совпадение, поэтому фраза, в
20
+ которой название лишь упомянуто, остаётся как была; код, поля ввода и переписка не
21
+ трогаются. Как и типографский, проход работает только пока активен русский. Проверено на
22
+ настоящем документе: `npm run check:plugin-names` (jsdom, 11 случаев).
23
+ - **Патч для харнесса собирается, а не пишется руками.** `docs/harness-descriptors-ru.patch` —
24
+ 32 файла `locale/ru.json` для переведённых плагинов — теперь строится из переводов и дерева
25
+ харнесса (`npm run build:descriptor-patch`), а гейт `npm run check:descriptor-patch` падает,
26
+ когда собранный патч и лежащий в репозитории разошлись. Гейт доказан в обе стороны: с
27
+ изменённым переводом и с вырезанной строкой в патче.
28
+
29
+ ### Исправлено
30
+
31
+ - **Отчёт о названиях плагинов больше не считает переведённые.** `npm run report:descriptors`
32
+ перечислял все читаемые названия как непереведённые, даже те, у которых в дереве харнесса
33
+ уже лежал `locale/ru.json`. Теперь отчёт делит их на переведённые и оставшиеся и говорит
34
+ число каждых.
35
+
9
36
  ## [0.2.7] — 2026-10-19
10
37
 
11
38
  ### Исправлено
package/README.en.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <!-- deepseek-harness-meta
2
2
  {
3
3
  "name": "Russian (ru) language pack for DeepSeek Harness",
4
- "version": "0.2.7",
4
+ "version": "0.2.8",
5
5
  "tags": ["localization", "russian", "i18n", "web-gui", "language-pack"],
6
6
  "description": "Full Russian interface (i18n) for the DeepSeek Harness web GUI: 81 namespaces, 4949 translated strings, Russian screen typography, and a locale audit in CI pinned to a harness tag. Includes the automation-task manager and the reminder catalog.",
7
7
  "icon": "https://raw.githubusercontent.com/Ragnoryok1/dsh-client-locale-ru/main/assets/icon.png",
@@ -43,9 +43,9 @@ language pack; only `locale/ru.json` files on the harness side can close them.
43
43
  Plugins outside the harness tree are translated the same way, but their English comes from
44
44
  their own publication: `plugins/<namespace>.json` records the package, version, licence and the
45
45
  English strings extracted from its bundle (`npm run fetch:plugins`). `npm run report:plugins`
46
- It also warns about another thing: when the profile holds a second pack that registers
47
- Russian itself, the language belongs to whoever registered it first — the other one silently
48
- skips its namespaces, and the interface ends up showing someone else's translation.
46
+ It also warns about another thing: when the profile holds a second pack that registers
47
+ Russian itself, the language belongs to whoever registered it first — the other one silently
48
+ skips its namespaces, and the interface ends up showing someone else's translation.
49
49
  shows what in your installation is still English: it walks the profile's `node_modules`, reads
50
50
  each plugin's own dictionary and compares its keys with ours. A plugin without a dictionary is
51
51
  reported as such — there is nothing to translate until it asks for one — and a plugin that
@@ -99,13 +99,13 @@ dsh plugin --profile web add @ragnoryok1/dsh-client-locale-ru
99
99
 
100
100
  After that, the next time `dsh web` starts, **Русский** will appear in the settings. A separate `pnpm add` into a built `dsh web` is not needed — the plugin pulls in the built-in locale through the `locale` service.
101
101
 
102
- If pnpm answers `ERR_PNPM_NO_MATURE_MATCHING_VERSION`, that is not a broken package but a
103
- maturity policy: a profile refuses versions published less than 48 hours ago. Wait a day or
104
- two (the message names the version) or drop the `minimumReleaseAge` limit in the profile
105
- settings.
102
+ If pnpm answers `ERR_PNPM_NO_MATURE_MATCHING_VERSION`, that is not a broken package but a
103
+ maturity policy: a profile refuses versions published less than 48 hours ago. Wait a day or
104
+ two (the message names the version) or drop the `minimumReleaseAge` limit in the profile
105
+ settings.
106
106
  The manifest is checked against the harness's own rules rather than by eye (`npm run check:manifest`, part of `verify`). Each rule was read out of the harness sources and each is a failure the user would otherwise meet first: `parseDshClient` accepts `platform` as any string while the consumers keep only `platform === 'web'`, so a typo like `'Web'` would **silently drop** the client half; a declared `dsh.client` without the `./client` export makes `client-modules` throw; and `dsh.bundle.patch` has to point at a file that exists.
107
107
 
108
- Installing from GitHub works too: `dsh plugin --profile web add Ragnoryok1/dsh-client-locale-ru` (the form the plugin catalogue generates, without the npm scope) installs the repository archive, and the built `lib/` is committed — no build runs on your machine, and none is needed. That is exactly why `npm run check:built` (part of `verify`) rebuilds `lib/` and compares it with the commit: a stale bundle in git would reach the user, while the audit would never see it, because the audit reads the sources. The published package is 497 kB: the banner ships as JPEG, and the icon and source map stay out of the npm payload.
108
+ Installing from GitHub works too: `dsh plugin --profile web add Ragnoryok1/dsh-client-locale-ru` (the form the plugin catalogue generates, without the npm scope) installs the repository archive, and the built `lib/` is committed — no build runs on your machine, and none is needed. That is exactly why `npm run check:built` (part of `verify`) rebuilds `lib/` and compares it with the commit: a stale bundle in git would reach the user, while the audit would never see it, because the audit reads the sources. The published package is 504 kB: the banner ships as JPEG, and the icon and source map stay out of the npm payload.
109
109
 
110
110
  ## Why hybrid
111
111
 
@@ -167,6 +167,34 @@ another language would edit text this pack does not own — `"quotes"` would bec
167
167
  English interface. The pack listens to `locale/change` and starts or stops the pass on a switch
168
168
  rather than on installation.
169
169
 
170
+ ## Plugin names
171
+
172
+ A plugin's name comes from its own `locale/<language>.json` file, and the locale service
173
+ resolves that map without consulting registered dictionaries — its own documentation says
174
+ «maps do not consult registered dictionaries». A dictionary therefore cannot translate a
175
+ plugin's name: the pack has no access to another package's map, and the English name stays
176
+ English until the plugin itself ships `locale/ru.json`.
177
+
178
+ The pack carries those names itself: 32 plugins, each with the English title and description
179
+ the translation was made from, and the Russian text. The rendered interface passes through a
180
+ pass that puts the Russian text where the English was — in the plugin cards, in the rows of a
181
+ bundle, on a plugin's page and in the settings inventory. The accessible name (`aria-label`)
182
+ is replaced too, so a card is read in Russian as a whole.
183
+
184
+ The pass matches exactly: a string is replaced only when the whole string is one of the titles
185
+ or descriptions the pack knows. A sentence that merely mentions a name keeps its wording;
186
+ code, input fields and the conversation are not touched. Like the typography pass, it runs
187
+ only while Russian is the active language.
188
+
189
+ The same 32 names go to the harness as a patch (`docs/harness-descriptors-ru.patch`): in the
190
+ harness interface the plugins are named in Russian, and the patch gets there first. The patch
191
+ is a build artefact — it is built from the translations and the harness tree, and the gate
192
+ `npm run check:descriptor-patch` fails when the built patch and the committed one differ.
193
+ `npm run check:plugin-names` proves on a real document (jsdom, 11 cases) that the pass
194
+ translates the title, the description, the accessible name and a bundle row — and leaves what
195
+ it must alone.
196
+
197
+
170
198
  ## Checks
171
199
 
172
200
  ```sh
@@ -179,6 +207,8 @@ npm run check:namespaces # NAMESPACES.md matches the built bundle
179
207
  npm run check:tarball # what gets installed: the archive is unpacked and checked
180
208
  npm run check:workflows # both workflows run the whole verify chain
181
209
  npm run check:readmes:pairs # the three READMEs move together (recorded section hashes)
210
+ npm run check:plugin-names # plugin names in Russian on a real document
211
+ npm run check:descriptor-patch # the harness patch matches the translations
182
212
  npm run audit -- <harness tree> # the locale audit
183
213
  npm run verify -- <harness tree> # everything together
184
214
 
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <!-- deepseek-harness-meta
2
2
  {
3
3
  "name": "Russian (ru) language pack for DeepSeek Harness",
4
- "version": "0.2.7",
4
+ "version": "0.2.8",
5
5
  "tags": ["localization", "russian", "i18n", "web-gui", "language-pack"],
6
6
  "description": "Full Russian interface (i18n) for the DeepSeek Harness web GUI: 81 namespaces, 4949 translated strings, Russian screen typography, and a locale audit in CI pinned to a harness tag. Includes the automation-task manager and the reminder catalog.",
7
7
  "icon": "https://raw.githubusercontent.com/Ragnoryok1/dsh-client-locale-ru/main/assets/icon.png",
@@ -103,10 +103,10 @@ dsh plugin --profile web add @ragnoryok1/dsh-client-locale-ru
103
103
 
104
104
  Дальше при следующем старте `dsh web` в настройках появится **Русский**. Отдельно `pnpm add` в собранный `dsh web` не нужен — плагин тянет штатную локаль через сервис `locale`.
105
105
 
106
- Если pnpm отвечает `ERR_PNPM_NO_MATURE_MATCHING_VERSION` — это не поломка пакета, а
107
- политика зрелости: профиль не ставит версии, опубликованные меньше 48 часов назад.
108
- Подождите сутки-двое (имя версии в сообщении показывает, сколько осталось) или снимите
109
- ограничение `minimumReleaseAge` в настройках профиля.
106
+ Если pnpm отвечает `ERR_PNPM_NO_MATURE_MATCHING_VERSION` — это не поломка пакета, а
107
+ политика зрелости: профиль не ставит версии, опубликованные меньше 48 часов назад.
108
+ Подождите сутки-двое (имя версии в сообщении показывает, сколько осталось) или снимите
109
+ ограничение `minimumReleaseAge` в настройках профиля.
110
110
  Манифест проверяется по правилам харнесса, а не на глаз (`npm run check:manifest`, входит в
111
111
  `verify`). Правила вычитаны из его исходников и каждое — это отказ, который иначе увидел бы
112
112
  пользователь: `parseDshClient` принимает `platform` любой строкой, но потребители отбирают
@@ -114,7 +114,7 @@ dsh plugin --profile web add @ragnoryok1/dsh-client-locale-ru
114
114
  половину; declared `dsh.client` без экспорта `./client` заставляет `client-modules` бросить
115
115
  ошибку; `dsh.bundle.patch` обязан указывать на существующий файл.
116
116
 
117
- Установка из GitHub тоже работает: `dsh plugin --profile web add Ragnoryok1/dsh-client-locale-ru` (в таком виде команду генерирует каталог плагинов, без `@`-скоупа) ставит архив репозитория, а собранный `lib/` лежит в git — сборка на вашей машине не запускается и не требуется. Поэтому `npm run check:built` (входит в `verify`) пересобирает `lib/` и сверяет его с коммитом: устаревший бандл в git иначе получил бы именно пользователь, а аудит этого не увидел бы — он читает исходники. Публикуемый пакет занимает 497 КБ: баннер отдаётся JPEG, а иконка и карта исходников в npm не входят.
117
+ Установка из GitHub тоже работает: `dsh plugin --profile web add Ragnoryok1/dsh-client-locale-ru` (в таком виде команду генерирует каталог плагинов, без `@`-скоупа) ставит архив репозитория, а собранный `lib/` лежит в git — сборка на вашей машине не запускается и не требуется. Поэтому `npm run check:built` (входит в `verify`) пересобирает `lib/` и сверяет его с коммитом: устаревший бандл в git иначе получил бы именно пользователь, а аудит этого не увидел бы — он читает исходники. Публикуемый пакет занимает 504 КБ: баннер отдаётся JPEG, а иконка и карта исходников в npm не входят.
118
118
 
119
119
  ## Почему гибрид
120
120
 
@@ -177,6 +177,33 @@ dsh plugin --profile web add @ragnoryok1/dsh-client-locale-ru
177
177
 
178
178
  Правила проверяются скриптом, а не на глаз: `npm run check:typography`.
179
179
 
180
+ ## Имена плагинов
181
+
182
+ Название плагина приходит из его собственного файла `locale/<язык>.json`, а служба локализации
183
+ разрешает эту карту, не обращаясь к зарегистрированным словарям: её собственная документация
184
+ говорит «maps do not consult registered dictionaries». Поэтому словарём название плагина не
185
+ перевести — у пакета нет доступа к чужой карте, и английское название остаётся английским,
186
+ пока сам плагин не привезёт `locale/ru.json`.
187
+
188
+ Пакет несёт эти названия сам: 32 плагина, у каждого английское название и описание — то, из
189
+ чего перевод сделан, — и русское. Отрисованный интерфейс проходит через проход, который
190
+ подставляет русский текст на место английского: в карточках плагинов, в строках состава
191
+ bundle, на странице плагина и в реестре настроек. Подменяется и имя для скринридера
192
+ (`aria-label`), чтобы карточка читалась по-русски целиком.
193
+
194
+ Проход берёт только точное совпадение: строка целиком равна названию или описанию из пакета.
195
+ Предложение, в котором название лишь упомянуто, остаётся как было; код, поля ввода и переписка
196
+ не трогаются. Как и типографский проход, он работает только пока активен русский.
197
+
198
+ Те же 32 названия уходят в харнесс отдельным патчем (`docs/harness-descriptors-ru.patch`): в
199
+ интерфейсе самого харнесса плагины называются по-русски, и патч этот до начала. Патч —
200
+ собираемый артефакт: он строится из переводов и дерева харнесса, а гейт
201
+ `npm run check:descriptor-patch` падает, когда собранный и лежащий в репозитории разошлись.
202
+ `npm run check:plugin-names` доказывает на настоящем документе (jsdom, 11 случаев), что проход
203
+ переводит название, описание, имя для скринридера и строку bundle — и не трогает то, что не
204
+ должен.
205
+
206
+
180
207
  ## Проверки
181
208
 
182
209
  ```sh
@@ -189,6 +216,8 @@ npm run check:namespaces # NAMESPACES.md совпадает с собранн
189
216
  npm run check:tarball # что установят: архив распаковывается и проверяется
190
217
  npm run check:workflows # оба workflow запускают всю цепочку verify
191
218
  npm run check:readmes:pairs # три README двигаются вместе (хеши разделов)
219
+ npm run check:plugin-names # названия плагинов по-русски на настоящем документе
220
+ npm run check:descriptor-patch # патч для харнесса совпадает с переводами
192
221
  npm run audit -- <дерево харнеса> # аудит локали
193
222
  npm run verify -- <дерево харнеса> # всё вместе
194
223
 
package/README.zh.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <!-- deepseek-harness-meta
2
2
  {
3
3
  "name": "Russian (ru) language pack for DeepSeek Harness",
4
- "version": "0.2.7",
4
+ "version": "0.2.8",
5
5
  "tags": ["localization", "russian", "i18n", "web-gui", "language-pack"],
6
6
  "description": "Full Russian interface (i18n) for the DeepSeek Harness web GUI: 81 namespaces, 4949 translated strings, Russian screen typography, and a locale audit in CI pinned to a harness tag. Includes the automation-task manager and the reminder catalog.",
7
7
  "icon": "https://raw.githubusercontent.com/Ragnoryok1/dsh-client-locale-ru/main/assets/icon.png",
@@ -32,9 +32,9 @@
32
32
 
33
33
  完整清单见 [NAMESPACES.md](NAMESPACES.md):81 个 namespace、4949 条字符串,随包附带的插件词典单独标注。
34
34
 
35
- 如果 pnpm 报 `ERR_PNPM_NO_MATURE_MATCHING_VERSION`,这不是包坏了,而是成熟度策略:配置文件
36
- 不安装发布不足 48 小时的版本。等一两天(报错信息里写着版本号),或在配置文件设置里去掉
37
- `minimumReleaseAge` 限制。
35
+ 如果 pnpm 报 `ERR_PNPM_NO_MATURE_MATCHING_VERSION`,这不是包坏了,而是成熟度策略:配置文件
36
+ 不安装发布不足 48 小时的版本。等一两天(报错信息里写着版本号),或在配置文件设置里去掉
37
+ `minimumReleaseAge` 限制。
38
38
  「插件」页面上的插件名称与说明无法由本包提供:harness 把她们放在每个包的 `locale/<语言>.json`
39
39
  里并据此拼出翻译映射,而 locale 服务的 `resolveText` 根本不查词典(原文注释:maps do not
40
40
  consult registered dictionaries)。221 个 harness 包带有 `locale/en.json`,其中 219 个带有
@@ -43,8 +43,8 @@ harness 侧的 `locale/ru.json` 文件补上。`scripts/descriptor-coverage.mjs`
43
43
  不与我们的词典覆盖率混在一起;机制说明与 32 份已备好的译文见 `docs/descriptor-upstream.md`。
44
44
  harness 树之外的插件用同样的方式翻译,但英文取自它们自己的发布物:`plugins/<namespace>.json`
45
45
  记录包名、版本、许可证,以及从其 bundle 中提取的英文字符串(`npm run fetch:plugins`)。
46
- 它还会提醒另一件事:如果配置文件里还有另一个自己注册俄语的包,语言就归先注册的那个——
47
- 第二个会静默跳过自己的 namespace,界面上出现的就是别人的译文。
46
+ 它还会提醒另一件事:如果配置文件里还有另一个自己注册俄语的包,语言就归先注册的那个——
47
+ 第二个会静默跳过自己的 namespace,界面上出现的就是别人的译文。
48
48
  安装里还剩哪些英文,用 `npm run report:plugins` 查看:它遍历 profile 的 `node_modules`,
49
49
  读出每个插件自己的词典并与我们的对照。没有词典的插件会照实标注——在它请求翻译之前无从翻译;
50
50
  自带俄语的插件单独标出。
@@ -90,7 +90,7 @@ dsh plugin --profile web add @ragnoryok1/dsh-client-locale-ru
90
90
 
91
91
  清单按 harness 自身的规则检查,而不是靠肉眼(`npm run check:manifest`,包含在 `verify` 中)。每一条规则都取自 harness 源码,每一条都对应一种否则会先落到用户头上的失败:`parseDshClient` 接受任意字符串作为 `platform`,但消费方只保留 `platform === 'web'` 的行,因此写成 `'Web'` 这样的笔误会**悄悄关掉**客户端那一半;声明了 `dsh.client` 却没有 `./client` 导出会让 `client-modules` 抛错;`dsh.bundle.patch` 必须指向确实存在的文件。
92
92
 
93
- 从 GitHub 安装同样可用:`dsh plugin --profile web add Ragnoryok1/dsh-client-locale-ru`(插件目录生成的就是这种不带 npm scope 的写法)安装的是仓库归档,而构建好的 `lib/` 已提交进 git——你的机器上不会运行构建,也不需要运行。正因如此,`npm run check:built`(包含在 `verify` 中)会重新构建 `lib/` 并与提交内容比对:git 里过期的构建产物会直接落到用户手里,而审计看不到它——审计读的是源码。发布的包为 497 KB:横幅以 JPEG 提供,图标与源码映射不进入 npm 包。
93
+ 从 GitHub 安装同样可用:`dsh plugin --profile web add Ragnoryok1/dsh-client-locale-ru`(插件目录生成的就是这种不带 npm scope 的写法)安装的是仓库归档,而构建好的 `lib/` 已提交进 git——你的机器上不会运行构建,也不需要运行。正因如此,`npm run check:built`(包含在 `verify` 中)会重新构建 `lib/` 并与提交内容比对:git 里过期的构建产物会直接落到用户手里,而审计看不到它——审计读的是源码。发布的包为 504 KB:横幅以 JPEG 提供,图标与源码映射不进入 npm 包。
94
94
 
95
95
  ## 为什么是混合插件
96
96
 
@@ -119,6 +119,17 @@ dsh plugin --profile web add @ragnoryok1/dsh-client-locale-ru
119
119
 
120
120
  该处理**仅在俄语处于活动状态时**运行:规则是俄语的,把它们用于其他语言会改动本包并不拥有的文本——英文界面里的 `"quotes"` 会变成 «quotes»。本包监听 `locale/change`,在切换语言时启动或停止处理,而不是在安装时。
121
121
 
122
+ ## 插件名称
123
+
124
+ 插件的名称来自它自己的 `locale/<语言>.json` 文件,而本地化服务解析这张映射时不会去查已注册的词典——它自己的文档写着 «maps do not consult registered dictionaries»。因此词典无法翻译插件名称:本包拿不到别的包的映射,英文名称会一直是英文,直到插件自己带上 `locale/ru.json`。
125
+
126
+ 本包自带这些名称:32 个插件,每个都记着翻译所依据的英文标题与说明,以及俄文文本。已经渲染出来的界面会经过一个处理流程,把俄文放到英文的位置上:插件卡片、bundle 的组成行、插件详情页以及设置中的插件清单。朗读用的名称(`aria-label`)也会一并替换,因此整张卡片读起来都是俄文。
127
+
128
+ 这个流程只做精确匹配:整段文本与本包知道的标题或说明完全相同时才会替换。只是提到了某个名称的句子保持原样;代码、输入框和对话不会被触碰。与排版流程一样,它只在俄文是当前语言时工作。
129
+
130
+ 同样这 32 个名称会作为补丁(`docs/harness-descriptors-ru.patch`)送到 harness:在 harness 自己的界面里插件也有俄文名称,而这个补丁先到。补丁是构建产物——它由翻译和 harness 目录树生成,门禁 `npm run check:descriptor-patch` 在生成的补丁与仓库里那份不一致时失败。`npm run check:plugin-names` 在真实文档上证明(jsdom,11 个用例)这个流程会翻译标题、说明、朗读名称和 bundle 的一行,而该不动的它不动。
131
+
132
+
122
133
  ## 检查
123
134
 
124
135
  ```sh
@@ -131,6 +142,8 @@ npm run check:namespaces # NAMESPACES.md 与构建出的 bundle 一致
131
142
  npm run check:tarball # 用户实际安装的:解开归档并检查
132
143
  npm run check:workflows # 两个 workflow 都运行完整的 verify 链
133
144
  npm run check:readmes:pairs # 三个 README 同步推进(记录各节哈希)
145
+ npm run check:plugin-names # 插件名称在真实文档上变成俄文
146
+ npm run check:descriptor-patch # harness 补丁与翻译一致
134
147
  npm run audit -- <harness tree> # 本地化审计
135
148
  npm run verify -- <harness tree> # 全部一起
136
149
 
@@ -145,7 +158,7 @@ npm run diff:harness -- <old> <new> # harness 改写了哪些英文
145
158
 
146
159
  审计在 CI(`.github/workflows/verify.yml`)中针对**锁定的 harness 标签**运行,而不是针对随便一份本地 checkout。同一次运行还会**每周按计划**触发一次:即使没人改动,译文也会腐坏——上游继续前进,本地一切正常,而偏差只在用户构建时才浮现。门禁覆盖完整性(key 与 namespace)、复数形式、占位符、英文与中文残留、术语、正文中的拉丁词、文本卫生、首尾空格、风格、取值的排版、«ё»、组合短语、设置侧栏标签的长度以及对外宣称的数字——每一项都已经抓到过真实缺陷。除此之外,审计还会打印**参考报告**——它们不是门禁,因为需要人来判断:比对了多少个 namespace、哪些无法建立对应关系、是否还留有「数字 + 需变格的名词」,以及短字符串是否长到英文的 2.8 倍以上(排版风险——«Effort» → «Уровень рассуждения» 是俄语的正常长度,不是缺陷)。
147
160
 
148
- 审计按 harness 实际书写的形式读取模板:字符串、**段落数组**(各模式的指南就是这样存储的)以及**常量引用**(`'hint.plan': PLAN_NEXT_ACTION_EN`)。在只读取第一种形式时,有十个 key——其中八个是整篇指南——悄悄退出了所有比对。反过来也一样:空的策略文件(`keep-english.json`、`terminology.json` 的某个分区)现在会让审计失败,而不是在无人察觉的情况下放宽门禁。README 中宣称的数字也按同样方式与包体核对,因为过期的数字就是错误的说法——同样的数字还会从 `package.json` 的 description 中读取,那是 npm 在搜索与包页面上显示的内容(「55 namespaces」正是在那里活得最久)——包括带千位分隔符的写法:旧版开头写的是 `2 489`,而检查此前只读取不带分隔符的形式,所以数字模式现在也接受千位分隔符。紧随其后还核对「针对哪个 harness 版本构建」这句话:它必须与 workflow 中锁定的标签一致,因为真正验证这个包的是那个 workflow(两个 README 的开头都还写着 `0.2.1-alpha.2`,而 CI 早已换成 `dsh-v0.2.1-rc.2`)。发布包的体积也在这里实测:`npm run check:size` 会打包归档,并把声明的千字节数与实际值对比(允许 497 KB 误差——不同 Node 构建下的 gzip 压缩结果略有差别,而这句话本身也只精确到整千字节)。
161
+ 审计按 harness 实际书写的形式读取模板:字符串、**段落数组**(各模式的指南就是这样存储的)以及**常量引用**(`'hint.plan': PLAN_NEXT_ACTION_EN`)。在只读取第一种形式时,有十个 key——其中八个是整篇指南——悄悄退出了所有比对。反过来也一样:空的策略文件(`keep-english.json`、`terminology.json` 的某个分区)现在会让审计失败,而不是在无人察觉的情况下放宽门禁。README 中宣称的数字也按同样方式与包体核对,因为过期的数字就是错误的说法——同样的数字还会从 `package.json` 的 description 中读取,那是 npm 在搜索与包页面上显示的内容(「55 namespaces」正是在那里活得最久)——包括带千位分隔符的写法:旧版开头写的是 `2 489`,而检查此前只读取不带分隔符的形式,所以数字模式现在也接受千位分隔符。紧随其后还核对「针对哪个 harness 版本构建」这句话:它必须与 workflow 中锁定的标签一致,因为真正验证这个包的是那个 workflow(两个 README 的开头都还写着 `0.2.1-alpha.2`,而 CI 早已换成 `dsh-v0.2.1-rc.2`)。发布包的体积也在这里实测:`npm run check:size` 会打包归档,并把声明的千字节数与实际值对比(允许 1 KB 误差——不同 Node 构建下的 gzip 压缩结果略有差别,而这句话本身也只精确到整千字节)。
149
162
 
150
163
  - **无效 namespace**——包注册的每一个 namespace 都必须在 harness 中存在。在 0.1.6 中,日程目录的翻译被注册在 `schedule.manager` 名下,而读取它们的是 `schedule.catalog`:此时按 key 做的覆盖率检查还报告「全部已翻译」;
151
164
  - **复数形式配对**——`.one`/`.other` 的集合必须一致,因为客户端按 `count === 1 ? '.one' : '.other'` 选择形式。这对俄语还不够:每一对还需要 `.few` 与 `.many`,否则包装层会悄悄退回中性的 `.other`,本该是 «2 дня» 的地方显示成 «2 д»。该检查**按 namespace** 进行——包装层在提问的那个 namespace 里查找形式,因此「某个界面上有、另一个界面上缺」这种情况否则不会被发现。它找到了四个时间单位(`schedule.catalog` 中的 `unit.day/hour/minute/second`)和任务管理器里的 `trigger`;同时这些单位不再是缩写(«д»、«ч»、«мин»),而是带正确格位的完整形式——«через 2 дня»、«через 5 часов»,而 «Каждые 2 дня» 现在写作 «Раз в 2 дня»。同样的要求也覆盖本包自己添加的 key(`terminal.expand` 及其同类:harness 会把计数传给它,却从不配对)——这些形式只由我们维护,少一个不会有任何其他检查发现;
package/lib/client.js CHANGED
@@ -5228,6 +5228,266 @@ window.__ModuleLoader__.load({
5228
5228
  return () => release(prototype, installation);
5229
5229
  }
5230
5230
  //#endregion
5231
+ //#region src/client/descriptor-ru.json
5232
+ var descriptor_ru_default = {
5233
+ "@deepseek-ai/dsh-client-ui-schedule": {
5234
+ "title": "Интерфейс задач",
5235
+ "description": "Страница задач автоматизации в боковой панели, карточки задач и отметки напоминаний в сессиях.",
5236
+ "en": {
5237
+ "title": "Task interface",
5238
+ "description": "The Automation tasks page in the sidebar, plus task cards and reminder marks in sessions."
5239
+ }
5240
+ },
5241
+ "@deepseek-ai/dsh-time-context": {
5242
+ "title": "Ощущение времени",
5243
+ "description": "Сообщает модели текущее время, часовой пояс браузера и время с предыдущего сообщения на каждом шаге.",
5244
+ "en": {
5245
+ "title": "Time awareness",
5246
+ "description": "Tells the model the current time, the browser's time zone, and the time since the previous message at each step."
5247
+ }
5248
+ },
5249
+ "@deepseek-ai/dsh-experimental-agent-team": {
5250
+ "title": "Командная работа",
5251
+ "description": "Управление участниками команды, сообщениями участников и общими задачами.",
5252
+ "en": {
5253
+ "title": "Team Collaboration",
5254
+ "description": "Manage team members, member messages, and shared tasks."
5255
+ }
5256
+ },
5257
+ "@deepseek-ai/dsh-experimental-agent-team-profile": {
5258
+ "title": "Команды агентов",
5259
+ "description": "Включает командную работу, инструменты команды, список участников и общую доску задач.",
5260
+ "en": {
5261
+ "title": "Agent Teams",
5262
+ "description": "Enable team collaboration, team tools, the member roster, and the shared task board."
5263
+ }
5264
+ },
5265
+ "@deepseek-ai/dsh-experimental-api-speech-to-text": {
5266
+ "title": "API распознавания речи",
5267
+ "description": "Даёт браузерным клиентам расшифровку речи в текст.",
5268
+ "en": {
5269
+ "title": "Speech transcription API",
5270
+ "description": "Provide speech transcription for browser clients."
5271
+ }
5272
+ },
5273
+ "@deepseek-ai/dsh-experimental-auto-review": {
5274
+ "title": "Автоматическая проверка разрешений",
5275
+ "description": "Добавляет режим разрешений с автоматической проверкой: модель оценивает запрос перед каждым вызовом инструмента.",
5276
+ "en": {
5277
+ "title": "Auto Authorization Review",
5278
+ "description": "Add an Auto review permission mode that uses the model to assess authorization before each tool call."
5279
+ }
5280
+ },
5281
+ "@deepseek-ai/dsh-experimental-badge-skill-bundle": {
5282
+ "title": "Навык значка DSH",
5283
+ "description": "Позволяет агентам добавлять значок «сделано на dsh» в документы, пул-реквесты и другие результаты.",
5284
+ "en": {
5285
+ "title": "DSH badge skill",
5286
+ "description": "Let agents add a “powered by dsh” badge to documents, pull requests, and other outputs."
5287
+ }
5288
+ },
5289
+ "@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp": {
5290
+ "title": "Работа в браузере (Chrome DevTools)",
5291
+ "description": "Позволяет модели просматривать веб-страницы и управлять ими через Chrome DevTools MCP в запущенном Chrome.",
5292
+ "en": {
5293
+ "title": "Browser Use (Chrome DevTools)",
5294
+ "description": "Let the model inspect and operate web pages through Chrome DevTools MCP in a launched Chrome."
5295
+ }
5296
+ },
5297
+ "@deepseek-ai/dsh-experimental-browser-use-playwright-mcp": {
5298
+ "title": "Работа в браузере (Playwright)",
5299
+ "description": "Позволяет модели просматривать веб-страницы и управлять ими через Playwright MCP в запущенном Chromium.",
5300
+ "en": {
5301
+ "title": "Browser Use (Playwright)",
5302
+ "description": "Let the model browse and operate web pages through Playwright MCP in a launched Chromium."
5303
+ }
5304
+ },
5305
+ "@deepseek-ai/dsh-experimental-client-ui-agent-team": {
5306
+ "title": "Панель команды",
5307
+ "description": "Показывает участников команды и общие задачи, позволяет переходить в разговоры участников.",
5308
+ "en": {
5309
+ "title": "Team Panel",
5310
+ "description": "View team members and shared tasks, and navigate to member conversations."
5311
+ }
5312
+ },
5313
+ "@deepseek-ai/dsh-experimental-client-ui-cot-translation": {
5314
+ "title": "Машинный перевод рассуждений",
5315
+ "description": "Показывает развёрнутые рассуждения на другом языке с помощью машинного перевода.",
5316
+ "en": {
5317
+ "title": "Machine translation of reasoning",
5318
+ "description": "Display expanded reasoning in another language using machine translation."
5319
+ }
5320
+ },
5321
+ "@deepseek-ai/dsh-experimental-client-ui-voice-input": {
5322
+ "title": "Запись голоса",
5323
+ "description": "Записывает речь и вставляет редактируемый текст в черновик сообщения.",
5324
+ "en": {
5325
+ "title": "Voice recording",
5326
+ "description": "Record speech and insert editable text into the conversation draft."
5327
+ }
5328
+ },
5329
+ "@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp": {
5330
+ "title": "Управление компьютером (Cua Driver MCP)",
5331
+ "description": "Позволяет модели управлять рабочим столом через отдельно установленный исполняемый файл cua-driver.",
5332
+ "en": {
5333
+ "title": "Computer Use (Cua Driver MCP)",
5334
+ "description": "Let the model operate the desktop through a separately installed cua-driver executable."
5335
+ }
5336
+ },
5337
+ "@deepseek-ai/dsh-experimental-computer-use-cua-driver-native": {
5338
+ "title": "Управление компьютером (Cua Driver)",
5339
+ "description": "Позволяет модели управлять рабочим столом встроенной средой Cua Driver; нужны разрешения рабочего стола для приложения, запускающего dsh.",
5340
+ "en": {
5341
+ "title": "Computer Use (Cua Driver)",
5342
+ "description": "Let the model operate the desktop with the bundled Cua Driver runtime; needs desktop permissions for the app that launches dsh."
5343
+ }
5344
+ },
5345
+ "@deepseek-ai/dsh-experimental-cot-translation-bundle": {
5346
+ "title": "Машинный перевод рассуждений",
5347
+ "description": "Машинный перевод развёрнутых рассуждений; исходный текст отправляется в выбранную службу",
5348
+ "en": {
5349
+ "title": "Machine translation of reasoning",
5350
+ "description": "Machine translation of expanded reasoning; source text is sent to the selected service"
5351
+ }
5352
+ },
5353
+ "@deepseek-ai/dsh-experimental-inspector": {
5354
+ "title": "Инспектор NodeJS",
5355
+ "description": "Открывает Chrome DevTools в нижней панели по Ctrl/Cmd+Shift+. для разработки и отладки.",
5356
+ "en": {
5357
+ "title": "NodeJS Inspector",
5358
+ "description": "Press Ctrl/Cmd+Shift+. to open Chrome DevTools in the bottom panel for development and debugging."
5359
+ }
5360
+ },
5361
+ "@deepseek-ai/dsh-experimental-inspector-profile": {
5362
+ "title": "Инструменты разработчика",
5363
+ "description": "Просмотр и отладка сырых данных сессии, сгруппированных сообщений чата и серверной части на NodeJS.",
5364
+ "en": {
5365
+ "title": "Developer Tools",
5366
+ "description": "Inspect and debug raw session data, grouped chat messages, and the NodeJS backend."
5367
+ }
5368
+ },
5369
+ "@deepseek-ai/dsh-experimental-plugin-market-bundle": {
5370
+ "title": "Свои магазины плагинов",
5371
+ "description": "Добавляет в раздел «Плагины» вкладку «Обзор» для просмотра и установки наборов из указанных вами каталогов магазинов плагинов",
5372
+ "en": {
5373
+ "title": "Custom plugin markets",
5374
+ "description": "Adds a Discover tab to Plugins for browsing and installing bundles from plugin market indexes you add"
5375
+ }
5376
+ },
5377
+ "@deepseek-ai/dsh-experimental-ralph-bundle": {
5378
+ "title": "Циклы Ralph",
5379
+ "description": "Выполняет явно запрошенную повторяющуюся работу свежими агентами.",
5380
+ "en": {
5381
+ "title": "Ralph loops",
5382
+ "description": "Run explicitly requested repeated work with fresh agents."
5383
+ }
5384
+ },
5385
+ "@deepseek-ai/dsh-experimental-session-inspector": {
5386
+ "title": "Журнал сессии",
5387
+ "description": "Включает разбор сырых данных текущей сессии и сгруппированных сообщений чата в боковой панели сессии.",
5388
+ "en": {
5389
+ "title": "Session Log",
5390
+ "description": "Enable analysis of the current session's raw data and grouped chat messages in the session sidebar."
5391
+ }
5392
+ },
5393
+ "@deepseek-ai/dsh-experimental-session-search": {
5394
+ "title": "Поиск по сессиям",
5395
+ "description": "Даёт агентам инструменты для поиска и чтения прежних сессий этой рабочей области.",
5396
+ "en": {
5397
+ "title": "Session search",
5398
+ "description": "Give Agents tools to search and read earlier sessions from the current workspace."
5399
+ }
5400
+ },
5401
+ "@deepseek-ai/dsh-experimental-session-titles-bundle": {
5402
+ "title": "Названия, следующие за разговором",
5403
+ "description": "Обновляет названия сессий по мере поступления новых запросов человека.",
5404
+ "en": {
5405
+ "title": "Conversation-following titles",
5406
+ "description": "Refresh session titles as new human prompts arrive."
5407
+ }
5408
+ },
5409
+ "@deepseek-ai/dsh-skill-badge": {
5410
+ "title": "DSH",
5411
+ "description": "Даёт официальный навык со значком «сделано на DSH».",
5412
+ "en": {
5413
+ "title": "DSH",
5414
+ "description": "Provide the official powered-by-DSH badge skill."
5415
+ }
5416
+ },
5417
+ "@deepseek-ai/dsh-experimental-speech-to-text": {
5418
+ "title": "Распознавание речи",
5419
+ "description": "Управляет расшифровкой речи в текст и выбирает службу распознавания.",
5420
+ "en": {
5421
+ "title": "Speech recognition",
5422
+ "description": "Manage speech transcription and select a recognition service."
5423
+ }
5424
+ },
5425
+ "@deepseek-ai/dsh-experimental-speech-to-text-sensevoice": {
5426
+ "title": "Локальное распознавание SenseVoice",
5427
+ "description": "Расшифровывает записи в текст локально с помощью SenseVoice.",
5428
+ "en": {
5429
+ "title": "Local SenseVoice recognition",
5430
+ "description": "Transcribe recordings into text locally with SenseVoice."
5431
+ }
5432
+ },
5433
+ "@deepseek-ai/dsh-experimental-terminal-bundle": {
5434
+ "title": "Постоянные терминалы",
5435
+ "description": "Сохраняет оболочки живыми между вызовами инструментов.",
5436
+ "en": {
5437
+ "title": "Persistent terminals",
5438
+ "description": "Keep shell processes alive across tool calls."
5439
+ }
5440
+ },
5441
+ "@deepseek-ai/dsh-experimental-tool-agent-team": {
5442
+ "title": "Инструменты команды",
5443
+ "description": "Даёт агентам инструменты для согласования участников, обмена сообщениями и управления общими задачами.",
5444
+ "en": {
5445
+ "title": "Team Tools",
5446
+ "description": "Give agents tools to coordinate members, exchange messages, and manage shared tasks."
5447
+ }
5448
+ },
5449
+ "@deepseek-ai/dsh-experimental-tool-worktree": {
5450
+ "title": "Рабочие деревья Git",
5451
+ "description": "Позволяет агенту создать новую ветку и рабочее дерево Git и продолжить работу в нём.",
5452
+ "en": {
5453
+ "title": "Git Worktrees",
5454
+ "description": "Let the agent create a new Git branch and worktree, then continue there."
5455
+ }
5456
+ },
5457
+ "@deepseek-ai/dsh-experimental-voice-input-bundle": {
5458
+ "title": "Голосовой ввод",
5459
+ "description": "Расшифровывает записи локально с помощью SenseVoice; для первого запуска нужно установить зависимости",
5460
+ "en": {
5461
+ "title": "Voice input",
5462
+ "description": "Transcribe recordings locally with SenseVoice; first use requires installing dependencies"
5463
+ }
5464
+ },
5465
+ "@deepseek-ai/dsh-schedule": {
5466
+ "title": "Планирование задач",
5467
+ "description": "Хранит задачи автоматизации, запускает каждую в её исходной сессии по расписанию и даёт агенту инструменты управления.",
5468
+ "en": {
5469
+ "title": "Task scheduling",
5470
+ "description": "Stores automation tasks, runs each in its original session when it is due, and gives the agent tools to manage them."
5471
+ }
5472
+ },
5473
+ "@deepseek-ai/dsh-subagent-claude-code": {
5474
+ "title": "Субагент Claude Code",
5475
+ "description": "Передаёт одну задачу в Claude Code с его собственными настройками и авторизацией.",
5476
+ "en": {
5477
+ "title": "Claude Code subagent",
5478
+ "description": "Delegate one task to Claude Code using its native authentication and settings."
5479
+ }
5480
+ },
5481
+ "@deepseek-ai/dsh-subagent-codex": {
5482
+ "title": "Субагент Codex",
5483
+ "description": "Передаёт одну задачу в Codex с его собственными настройками и авторизацией.",
5484
+ "en": {
5485
+ "title": "Codex subagent",
5486
+ "description": "Delegate one task to Codex using its native authentication and settings."
5487
+ }
5488
+ }
5489
+ };
5490
+ //#endregion
5231
5491
  //#region src/client/typography.ts
5232
5492
  /**
5233
5493
  * Russian screen typography for translated strings.
@@ -5284,7 +5544,7 @@ window.__ModuleLoader__.load({
5284
5544
  "[data-conversation-region]"
5285
5545
  ].join(",");
5286
5546
  /** Whether the node sits inside content that must not be rewritten. */
5287
- function isSkipped(node) {
5547
+ function isSkipped$1(node) {
5288
5548
  const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
5289
5549
  return element === null || element.closest(SKIP_SELECTOR) !== null;
5290
5550
  }
@@ -5314,14 +5574,14 @@ window.__ModuleLoader__.load({
5314
5574
  if (after !== before) node.data = after;
5315
5575
  return;
5316
5576
  }
5317
- if (node.nodeType !== Node.ELEMENT_NODE || isSkipped(node)) return;
5577
+ if (node.nodeType !== Node.ELEMENT_NODE || isSkipped$1(node)) return;
5318
5578
  for (const child of node.childNodes) rewriteNode(child);
5319
5579
  }
5320
5580
  rewriteNode(root);
5321
5581
  const observer = new MutationObserver((records) => {
5322
5582
  for (const record of records) {
5323
5583
  if (record.type === "characterData") {
5324
- if (isSkipped(record.target)) continue;
5584
+ if (isSkipped$1(record.target)) continue;
5325
5585
  processed.delete(record.target);
5326
5586
  rewriteNode(record.target);
5327
5587
  continue;
@@ -5337,6 +5597,149 @@ window.__ModuleLoader__.load({
5337
5597
  return () => observer.disconnect();
5338
5598
  }
5339
5599
  //#endregion
5600
+ //#region src/client/plugin-names.ts
5601
+ /**
5602
+ * Russian names and descriptions for the plugins the harness cannot translate.
5603
+ *
5604
+ * A plugin's title comes from `locale/<language>.json` inside its own package, and the
5605
+ * locale service resolves that map without consulting registered dictionaries — its own
5606
+ * documentation says «maps do not consult registered dictionaries». A language pack
5607
+ * therefore cannot translate a plugin's name while the plugin ships no Russian locale:
5608
+ * the English title is the only title there is.
5609
+ *
5610
+ * The rendered DOM is the one place where that title passes through, so this module
5611
+ * rewrites it in place, the way the typography rules rewrite punctuation. The pack
5612
+ * carries the translations itself, with the English text they were made from, so a
5613
+ * match is exact: a title is replaced only when the whole string is one of the names
5614
+ * this pack knows. Text that merely mentions a name is left alone, and so is code,
5615
+ * form fields and the conversation.
5616
+ *
5617
+ * The pass follows the active language like the typography pass: the names are Russian,
5618
+ * so an English interface must keep its English names.
5619
+ */
5620
+ /**
5621
+ * The texts the pass replaces, longest English first: a shorter name that is a prefix
5622
+ * of a longer one must not claim the longer one's place.
5623
+ */
5624
+ function collectTexts() {
5625
+ const texts = [];
5626
+ for (const fields of Object.values(descriptor_ru_default)) for (const field of ["title", "description"]) {
5627
+ const english = fields.en?.[field];
5628
+ const russian = fields[field];
5629
+ if (english === void 0 || russian === void 0 || english === russian) continue;
5630
+ texts.push({
5631
+ english,
5632
+ russian
5633
+ });
5634
+ }
5635
+ return texts.sort((left, right) => right.english.length - left.english.length);
5636
+ }
5637
+ /** Every text the pass knows, by its English form. */
5638
+ const TEXTS = collectTexts();
5639
+ /** The English text of each plugin title and description, longest first. */
5640
+ const ENGLISH = TEXTS.map((text) => text.english);
5641
+ /** The Russian text of each plugin title and description, by its English form. */
5642
+ const RUSSIAN = new Map(TEXTS.map((text) => [text.english, text.russian]));
5643
+ /** The attributes that carry a plugin name: an accessible name and a tooltip. */
5644
+ const NAME_ATTRIBUTES = ["aria-label", "title"];
5645
+ /**
5646
+ * Whether the node sits inside content that must not be rewritten: code, data, user
5647
+ * input, and the conversation.
5648
+ *
5649
+ * @param node - the text node or element to test.
5650
+ * @returns whether the node is off limits.
5651
+ */
5652
+ function isSkipped(node) {
5653
+ const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
5654
+ return element === null || element.closest(SKIP_SELECTOR) !== null;
5655
+ }
5656
+ /**
5657
+ * The Russian text for an exact English title or description.
5658
+ *
5659
+ * @param text - the rendered text.
5660
+ * @returns the Russian text, or undefined when the text is not a known plugin name.
5661
+ */
5662
+ function russianFor(text) {
5663
+ const trimmed = text.trim();
5664
+ if (trimmed === "") return void 0;
5665
+ return RUSSIAN.get(trimmed);
5666
+ }
5667
+ /**
5668
+ * Replace a name inside a longer string, such as an accessible name that reads
5669
+ * «Открыть Session Log».
5670
+ *
5671
+ * @param value - the attribute value.
5672
+ * @returns the value with every known name replaced.
5673
+ */
5674
+ function russianNamesIn(value) {
5675
+ let out = value;
5676
+ for (const english of ENGLISH) {
5677
+ if (!out.includes(english)) continue;
5678
+ out = out.split(english).join(RUSSIAN.get(english));
5679
+ }
5680
+ return out;
5681
+ }
5682
+ /**
5683
+ * Start the plugin-name pass on a document and keep it current as the app renders.
5684
+ * Returns a disposer that stops observing.
5685
+ *
5686
+ * @param doc - document whose body carries the rendered interface.
5687
+ * @param isActive - whether the pass may run; the names are Russian, so a pack
5688
+ * installed while another language is active must not translate that language's
5689
+ * plugin names. Checked before every rewrite, so a language switch takes effect on
5690
+ * the next render.
5691
+ * @returns disposer removing the observer.
5692
+ */
5693
+ function startRussianPluginNames(doc, isActive = () => true) {
5694
+ const root = doc.body;
5695
+ if (root === null) return () => {};
5696
+ function rewriteElement(element) {
5697
+ if (!isActive()) return;
5698
+ for (const name of NAME_ATTRIBUTES) {
5699
+ const value = element.getAttribute(name);
5700
+ if (value === null) continue;
5701
+ const after = russianNamesIn(value);
5702
+ if (after !== value) element.setAttribute(name, after);
5703
+ }
5704
+ for (const child of element.childNodes) rewriteNode(child);
5705
+ }
5706
+ function rewriteNode(node) {
5707
+ if (!isActive()) return;
5708
+ if (node.nodeType === Node.TEXT_NODE) {
5709
+ const text = node;
5710
+ const russian = russianFor(text.data);
5711
+ if (russian !== void 0) text.data = text.data.replace(text.data.trim(), russian);
5712
+ return;
5713
+ }
5714
+ if (node.nodeType !== Node.ELEMENT_NODE || isSkipped(node)) return;
5715
+ rewriteElement(node);
5716
+ }
5717
+ rewriteNode(root);
5718
+ const observer = new MutationObserver((records) => {
5719
+ for (const record of records) {
5720
+ if (record.type === "characterData") {
5721
+ rewriteNode(record.target);
5722
+ continue;
5723
+ }
5724
+ if (record.type === "attributes") {
5725
+ const element = record.target;
5726
+ if (element.nodeType !== Node.ELEMENT_NODE || isSkipped(element)) continue;
5727
+ rewriteElement(element);
5728
+ continue;
5729
+ }
5730
+ for (const added of record.addedNodes) rewriteNode(added);
5731
+ }
5732
+ });
5733
+ observer.observe(root, {
5734
+ childList: true,
5735
+ characterData: true,
5736
+ subtree: true,
5737
+ attributes: true,
5738
+ attributeFilter: NAME_ATTRIBUTES
5739
+ });
5740
+ return () => observer.disconnect();
5741
+ }
5742
+ //#endregion
5340
5743
  //#region src/client/index.ts
5341
5744
  /**
5342
5745
  * Skip only occupied slots; validation and unexpected service errors still fail.
@@ -5357,6 +5760,35 @@ window.__ModuleLoader__.load({
5357
5760
  /** Required service: the locale registry that owns languages and dictionaries. */
5358
5761
  const inject = ["locale"];
5359
5762
  /**
5763
+ * Run a Russian-only pass while Russian is the active language, and stop it on a switch.
5764
+ *
5765
+ * The typography rules and the plugin names are Russian: applying them while another
5766
+ * language is active would edit text this pack does not own — `"quotes"` would become
5767
+ * «quotes» in an English interface, and a plugin would be renamed in a language the
5768
+ * user did not choose. `locale/change` fires on a language switch only, not on
5769
+ * dictionary registration.
5770
+ *
5771
+ * @param ctx - client root context.
5772
+ * @param start - the pass to run, given the check that says whether Russian is active.
5773
+ * @returns a disposer stopping the pass and the subscription.
5774
+ */
5775
+ function followLanguage(ctx, start) {
5776
+ const isRussian = () => ctx.locale.getLocale().active === "ru";
5777
+ let stop = isRussian() ? start(isRussian) : null;
5778
+ const off = ctx.on("locale/change", () => {
5779
+ if (isRussian() && stop === null) stop = start(isRussian);
5780
+ else if (!isRussian() && stop !== null) {
5781
+ stop();
5782
+ stop = null;
5783
+ }
5784
+ });
5785
+ return () => {
5786
+ off();
5787
+ stop?.();
5788
+ stop = null;
5789
+ };
5790
+ }
5791
+ /**
5360
5792
  * Client plugin body: add `ru` to the selectable language catalog, register
5361
5793
  * a ru dictionary for every namespace this pack contributes, and start the
5362
5794
  * Russian plural and typography passes. Registration is an effect, so the
@@ -5388,22 +5820,8 @@ window.__ModuleLoader__.load({
5388
5820
  console.warn(`[dsh-client-locale-ru] ${parts.join("; ")}. В интерфейсе останется перевод другого пакета — оставьте в профиле один пакет с русским языком.`);
5389
5821
  }
5390
5822
  ctx.effect(() => startRussianPlurals(ctx.locale), "locale-ru: plurals");
5391
- ctx.effect(() => {
5392
- const isRussian = () => ctx.locale.getLocale().active === "ru";
5393
- let stop = isRussian() ? startRussianTypography(document, isRussian) : null;
5394
- const off = ctx.on("locale/change", () => {
5395
- if (isRussian() && stop === null) stop = startRussianTypography(document, isRussian);
5396
- else if (!isRussian() && stop !== null) {
5397
- stop();
5398
- stop = null;
5399
- }
5400
- });
5401
- return () => {
5402
- off();
5403
- stop?.();
5404
- stop = null;
5405
- };
5406
- }, "locale-ru: typography");
5823
+ ctx.effect(() => followLanguage(ctx, (isRussian) => startRussianTypography(document, isRussian)), "locale-ru: typography");
5824
+ ctx.effect(() => followLanguage(ctx, (isRussian) => startRussianPluginNames(document, isRussian)), "locale-ru: plugin names");
5407
5825
  }
5408
5826
  //#endregion
5409
5827
  exports.apply = apply;
@@ -15,6 +15,8 @@
15
15
  */
16
16
  /** Guillemets, em dash, ellipsis, and non-breaking spaces in running text. */
17
17
  declare function applyRussianTypography(text: string): string;
18
+ /** Elements whose text is code, data or user input and must stay verbatim. */
19
+ declare const SKIP_SELECTOR: string;
18
20
  /**
19
21
  * Start the typography pass on a document and keep it current as the app
20
22
  * renders. Returns a disposer that stops observing.
@@ -28,4 +30,4 @@ declare function applyRussianTypography(text: string): string;
28
30
  */
29
31
  declare function startRussianTypography(doc: Document, isActive?: () => boolean): () => void;
30
32
  //#endregion
31
- export { applyRussianTypography, startRussianTypography };
33
+ export { SKIP_SELECTOR, applyRussianTypography, startRussianTypography };
package/lib/typography.js CHANGED
@@ -107,4 +107,4 @@ function startRussianTypography(doc, isActive = () => true) {
107
107
  return () => observer.disconnect();
108
108
  }
109
109
  //#endregion
110
- export { applyRussianTypography, startRussianTypography };
110
+ export { SKIP_SELECTOR, applyRussianTypography, startRussianTypography };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ragnoryok1/dsh-client-locale-ru",
3
3
  "description": "dsh language pack — i18n for the DeepSeek Harness Web GUI: Russian (ru) locale, 81 namespaces, 4949 strings, 17 third-party plugin dictionaries, ru plural forms",
4
- "version": "0.2.7",
4
+ "version": "0.2.8",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "deepseek-harness",
@@ -80,7 +80,7 @@
80
80
  "check:built": "node scripts/check-built.mjs",
81
81
  "check:size": "node scripts/check-size.mjs",
82
82
  "audit": "node scripts/audit.mjs",
83
- "verify": "npm run check:readmes && npm run check:readmes:pairs && npm run check:changelog && npm run check:manifest && npm run check:meta && npm run check:built && npm run check:size && npm run check:tarball && npm run check:typography && npm run check:typography:dom && npm run check:locale && npm run check:plural && npm run check:composition && npm run check:namespaces && npm run check:upstream && npm run check:peers && npm run check:workflows && npm run check:workflow-yaml && npm run check:release-race && npm run check:conflict-notice && npm run check:descriptors && node scripts/audit.mjs",
83
+ "verify": "npm run check:readmes && npm run check:readmes:pairs && npm run check:changelog && npm run check:manifest && npm run check:meta && npm run check:built && npm run check:size && npm run check:tarball && npm run check:typography && npm run check:typography:dom && npm run check:locale && npm run check:plural && npm run check:composition && npm run check:namespaces && npm run check:upstream && npm run check:peers && npm run check:workflows && npm run check:workflow-yaml && npm run check:release-race && npm run check:conflict-notice && npm run check:descriptors && npm run check:descriptor-patch && npm run check:plugin-names && node scripts/audit.mjs",
84
84
  "prepublishOnly": "npm run build",
85
85
  "check:composition": "node scripts/check-composition.mjs",
86
86
  "namespaces": "node scripts/namespaces.mjs",
@@ -96,6 +96,9 @@
96
96
  "report:descriptors": "node scripts/descriptor-coverage.mjs",
97
97
  "apply:descriptors": "node scripts/apply-descriptor-ru.mjs",
98
98
  "check:descriptors": "node scripts/apply-descriptor-ru.mjs --check",
99
+ "check:descriptor-patch": "node scripts/check-descriptor-patch.mjs",
100
+ "check:plugin-names": "node scripts/check-plugin-names-dom.mjs",
101
+ "build:descriptor-patch": "node scripts/build-descriptor-patch.mjs --write",
99
102
  "release:status": "node scripts/release-status.mjs",
100
103
  "check:meta": "node scripts/check-meta.mjs",
101
104
  "check:workflow-yaml": "node scripts/check-workflow-yaml.mjs",