@ragnoryok1/dsh-client-locale-ru 0.2.13 → 0.2.14

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/README.en.md CHANGED
@@ -1,598 +1,606 @@
1
- <!-- deepseek-harness-meta
2
- {
3
- "name": "Russian (ru) language pack for DeepSeek Harness",
4
- "version": "0.2.13",
5
- "tags": ["localization", "russian", "i18n", "web-gui", "language-pack"],
6
- "description": "Full Russian interface (i18n) for the DeepSeek Harness web GUI: 130 namespaces, 14086 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
- "icon": "https://raw.githubusercontent.com/Ragnoryok1/dsh-client-locale-ru/main/assets/icon.png",
8
- "compatible_versions": ["v0.2.1-rc.2", "v0.2.1-rc.1", "v0.2.1-alpha.5", "v0.2.1-alpha.4", "v0.2.1-alpha.3", "v0.2.1-alpha.2", "v0.2.1-alpha.1", "v0.2.0-rc.2", "v0.2.0-rc.1", "v0.1.7-rc.2", "v0.1.7-rc.1", "v0.1.7-alpha.2"],
9
- "screenshots": "images",
10
- "install_method": "dsh plugin --profile web add @ragnoryok1/dsh-client-locale-ru"
11
- }
12
- -->
13
-
14
- # @ragnoryok1/dsh-client-locale-ru
15
-
16
- [Русский](README.md) | **English** | [中文](README.zh.md)
17
-
18
- ![Russian language pack for DeepSeek Harness](assets/banner.jpg)
19
-
20
- [![npm](https://img.shields.io/npm/v/%40ragnoryok1%2Fdsh-client-locale-ru)](https://www.npmjs.com/package/@ragnoryok1/dsh-client-locale-ru)
21
- [![license](https://img.shields.io/npm/l/%40ragnoryok1%2Fdsh-client-locale-ru)](LICENSE)
22
- [![verify](https://github.com/Ragnoryok1/dsh-client-locale-ru/actions/workflows/verify.yml/badge.svg)](https://github.com/Ragnoryok1/dsh-client-locale-ru/actions/workflows/verify.yml)
23
- [![dsh](https://img.shields.io/badge/dsh-0.2.1--rc.2%2B-6366f1)](https://github.com/deepseek-ai/deepseek-harness)
24
- [![namespaces](https://img.shields.io/badge/namespaces-130-6466f1)](https://github.com/Ragnoryok1/dsh-client-locale-ru)
25
- [![strings](https://img.shields.io/badge/strings-14086-6466f1)](https://github.com/Ragnoryok1/dsh-client-locale-ru)
26
-
27
- Russian (`ru`) language pack for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) web GUI (`dsh`). It registers `ru` as a selectable client language through the locale service (`ctx.locale.addLanguage`) plus one dictionary per namespace — 130 namespaces, 14,086 strings, built against dsh `0.2.1-rc.2`. Install with `dsh plugin --profile web add @ragnoryok1/dsh-client-locale-ru`, then pick **Русский** in Settings → General. Missing keys fall back to English, so a newer harness keeps working. The scope is the harness's own client interface — 64 namespaces (3 033 strings) — plus the dictionaries of 59 third-party plugins, translated from the English each plugin publishes (9 557 strings in total). MIT, community-maintained, not affiliated with DeepSeek.
28
-
29
- > **If the pack was useful to you, please star the repository.** It is the only signal that a
30
- > translation is wanted, and it is what decides which parts get worked on next.
31
-
32
- ---
33
-
34
- The complete inventory is in [NAMESPACES.md](NAMESPACES.md): 130 namespaces, 14,086 strings, with the bundled plugin dictionaries marked.
35
-
36
- The plugin names and descriptions on the Plugins page cannot come from this pack: the harness
37
- keeps them in `locale/<language>.json` inside each package, builds a translation map from them,
38
- and its locale service resolves that map without consulting dictionaries at all — «maps do not
39
- consult registered dictionaries». 221 harness packages ship `locale/en.json`, 219 of them ship
40
- `locale/zh.json`, and **none ships `locale/ru.json`**, so those 261 strings stay English with any
41
- language pack; only `locale/ru.json` files on the harness side can close them.
42
- `scripts/descriptor-coverage.mjs` counts that gap so it is not mixed into our dictionary coverage; the mechanism and the 32 prepared translations are in `docs/descriptor-upstream.md`.
43
- Plugins outside the harness tree are translated the same way, but their English comes from
44
- their own publication: `plugins/<namespace>.json` records the package, version, licence and the
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.
49
- shows what in your installation is still English: it walks the profile's `node_modules`, reads
50
- each plugin's own dictionary and compares its keys with ours. A plugin without a dictionary is
51
- reported as such — there is nothing to translate until it asks for one — and a plugin that
52
- already carries its own Russian is marked separately. That is how dictionaries
53
- that would otherwise not exist at all get covered — today `update` (43 strings) for
54
- `@linxin666/dsh-update`, `git-graph` (63 strings) for `@linxin666/dsh-client-ui-git-graph`,
55
- `dsh-web-ui-usage` (63 strings) for `@linxin666/dsh-usage` and `dsh-web-ui-session-archive`
56
- (124 strings) for `@linxin666/dsh-session-archive`, `pet` (81 strings) for `@linxin666/dsh-pet`
57
- `remote` (115 strings) for `@linxin666/dsh-remote-web-ui`, `dsh-skill-explorer`
58
- (75 strings) for `@linxin666/dsh-client-ui-skill-explorer` and `dsh-web-ui-market`
59
- (106 strings) for `@linxin666/dsh-client-ui-market`, `liangshen` (42 strings) for
60
- `@linxin666/dsh-liangshen` and `dsh-web-ui-preset-center` (46 strings) for
61
- `@linxin666/dsh-client-ui-preset-center`, `dsh-ssh` (154 strings) for `@linxin666/dsh-ssh` and
62
- `task-board` (437 strings) for `@linxin666/dsh-client-ui-task-board` and `task-board-github`
63
- (113 strings) for `@linxin666/dsh-client-ui-task-board-github` — all thirteen extractable
64
- dictionaries of that family, each translated in full. Another pack's Russian is never copied: there would be nothing to verify it
65
- against, while the audit compares our value with the English template of that same plugin.
66
-
67
- ## What it looks like
68
-
69
- Russian in the language list and the translated settings sections (the screenshots were taken in build `0.1.8`:
70
- the same screen, but the wording and some sections have changed since — the font-size caption, for one, now reads
71
- «Влияет только на содержимое диалога»):):
72
-
73
- ![Settings in Russian: language, appearance, themes](images/settings-ru.png)
74
-
75
- ![The plugin list in Russian: section headings and the «Экспериментально» badges](images/plugins-ru.png)
76
-
77
- Both screenshots are cropped at the left edge of the panel: workspace and session names are
78
- not in frame.
79
-
80
- Russian locale (`ru`) for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) web GUI (`dsh`).
81
-
82
- The plugin adds **Russian** as a selectable language through the locale's language-pack contract: `ctx.locale.addLanguage({ id: 'ru', label: 'Русский', fallback: 'en' })` plus one `ru` dictionary per namespace. Once **Русский** is picked in Settings → General, the interface switches over immediately.
83
-
84
- > **If the package was useful to you, give it a star.** It is the only way to show that a
85
- > translation is wanted, and it genuinely affects which tasks get taken on. Not a single
86
- > “like” button in the interface does that.
87
- >
88
- > The repository, meanwhile, remains the place where the state of the translation is visible:
89
- > a [CHANGELOG](CHANGELOG.md) breaking down every release (including the mistakes that were found
90
- > in it by its own maintainers), the CI checks, and the list of what is deliberately not translated.
91
-
92
- ## Installation
93
-
94
- This is a **hybrid plugin** (`dsh.bundle` + `dsh.client`), so it installs as an ordinary profile bundle through `dsh plugin`:
95
-
96
- ```sh
97
- dsh plugin --profile web add @ragnoryok1/dsh-client-locale-ru
98
- ```
99
-
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
-
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
- 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
-
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 724 kB: the banner ships as JPEG, and the icon and source map stay out of the npm payload.
109
-
110
- ## Why hybrid
111
-
112
- `dsh web` takes client (UI) plugins only from **workspace packages**, and an external npm package carrying just a `dsh.client` is not picked up. For an external plugin with a UI half to be installable, it has to declare **`dsh.bundle`** (the profile layer), whose `cordis.patch.yml` adds the client entry `locale-ru` to the browser roster through `- insert:`. Other third-party plugins are built the same way (for example `@zseven-w/dsh-android`).
113
-
114
- ## How it works
115
-
116
- - **Host half** (`lib/index.js`) — an empty `apply()`: a language plugin needs no server logic, it only provides a valid cordis entry for the Loader.
117
- - **Browser half** (`lib/client.js`) — registers `ru` and the dictionaries through the `locale` service (injected from `ctx`). There are no framework runtime imports — only the service.
118
- - **No runtime dependencies** — `lib/client.js` imports no other packages, so it does not drag in heavy dependencies.
119
- - **Effects** — the language and the dictionaries are registered through `ctx.effect(...)`, so they are unloaded together with the plugin (HMR-safe).
120
- - **Plural forms** — the locale contract carries no pluralization rules: components choose the
121
- form as `count === 1 ? 'key.one' : 'key.other'`, while Russian needs three (1 вызов /
122
- 2 вызова / 5 вызовов). The package takes that over: alongside `.one` and `.other` the
123
- dictionaries carry `.few` and `.many`, and a wrapper around the runtime's `translate`
124
- recomputes the form with `Intl.PluralRules('ru')` for any call carrying a numeric `count`
125
- or `n`. The number decides, not the key the component picked, so «2 вызова инструментов»
126
- and «5 вызовов инструментов» come out of the same call site. The forms are not limited to
127
- paired keys: where the harness passes a number to a key it never paired (`terminal.expand`
128
- with `{n}`), the pack adds its own `.one` / `.few` / `.many` — «Показать ещё 1 строку /
129
- 3 строки / 7 строк» — and the wrapper looks a form up for those keys too. Where a form does
130
- not fit (a sentence carrying several numbers), the value is rewritten to be count-neutral:
131
- «Символов: {total}» instead of «{total} символов», «файлов: {files}» instead of
132
- «{files} файлов». Checked by `npm run check:plural` at counts 0, 1, 2, 3, 5 and 22, on an
133
- unpaired key, on a fractional count, and on a foreign language pack whose forms our rules
134
- must not rewrite.
135
- - **Fallback** — a missing Russian key falls back to `en` (the configured `fallback`).
136
-
137
- ## Typography
138
-
139
- The package brings already-rendered Russian text into line with typographic rules: guillemets
140
- («ёлочки») instead of straight quotes, an em dash instead of a hyphen between words, an ellipsis
141
- instead of three dots, a non-breaking space between a number and the following word or unit of
142
- measurement (so that «187 плагинов» does not break across two lines), and the same space after a
143
- short word — a preposition, a conjunction or a particle — so that «в режим» wraps as a whole
144
- instead of leaving «в» alone at the end of a line. The rules are deliberately conservative: they
145
- fire only on unambiguous patterns, and code, input fields, editable text and elements with
146
- `data-typography="off"` are not touched at all. A second pass changes nothing.
147
-
148
- A rule for numeric ranges (an en dash in «5–10») is **deliberately absent**: the only string in
149
- the whole pack with a hyphen between digits is the cron example `0 9 * * 1-5`, and a dash would
150
- corrupt it. Measured, not assumed.
151
-
152
- The rules are checked by a script rather than by eye: `npm run check:typography`.
153
-
154
- The pass itself sees text nodes only, while a dictionary value also travels into attributes —
155
- `title`, `aria-label`, `placeholder` — that it never reaches. The values are therefore checked
156
- separately: the pack may not contain a straight quote, a hyphen standing for a dash, a dash
157
- without spaces, or three dots. Otherwise the same string would look different depending on
158
- where it landed, and in a hint or a screen-reader label the rules would not work at all.
159
-
160
- Text that changes **after** the first pass — token counters, timers, progress — is rewritten
161
- again, so a live «4 с» keeps its non-breaking space. This is checked on a real document rather
162
- than on strings: `npm run check:typography:dom` (jsdom, six cases — the first pass, a later text
163
- change, an added subtree, disposal, a foreign language, and a language switch).
164
-
165
- The pass runs **only while Russian is active**: the rules are Russian, and applying them to
166
- another language would edit text this pack does not own — `"quotes"` would become «quotes» in an
167
- English interface. The pack listens to `locale/change` and starts or stops the pass on a switch
168
- rather than on installation.
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: the names of the 32 harness plugins and the names and
179
- descriptions of 33 third-party plugins whose dictionaries it translates. Each comes with the
180
- English text the translation was made from and the Russian text. The rendered interface passes
181
- through a pass that puts the Russian text where the English was — in the plugin cards, in the
182
- rows of a bundle, on a plugin's page and in the settings inventory. The accessible name
183
- (`aria-label`) is replaced too, so a card is read in Russian as a whole.
184
-
185
- The pass matches exactly: a string is replaced only when the whole string is one of the titles
186
- or descriptions the pack knows. A sentence that merely mentions a name keeps its wording;
187
- code, input fields and the conversation are not touched. Like the typography pass, it runs
188
- only while Russian is the active language.
189
-
190
- The same 32 names go to the harness as a patch (`docs/harness-descriptors-ru.patch`): in the
191
- harness interface the plugins are named in Russian, and the patch gets there first. The patch
192
- is a build artefact — it is built from the translations and the harness tree, and the gate
193
- `npm run check:descriptor-patch` fails when the built patch and the committed one differ.
194
- The third-party names live in `src/client/plugin-names-ru.json`: the file is built from the
195
- vendored plugin dictionaries (`npm run build:plugin-names`), and the gate
196
- `npm run check:plugin-names:map` fails when the map has drifted from the dictionaries. The same
197
- English name cannot be translated two ways — differently in the harness and in a plugin.
198
- `npm run check:plugin-names` proves on a real document (jsdom, 12 cases) that the pass
199
- translates the title, the description, the accessible name and a bundle row — and leaves what
200
- it must alone.
201
-
202
-
203
- ## Checks
204
-
205
- ```sh
206
- npm run build # the three outputs
207
- npm run check:typography # 18 typography rules, including "leave alone"
208
- npm run check:locale # dictionaries side by side: 6 checks
209
- npm run check:plural # plural forms: 1 вызов / 2 вызова / 5 вызовов
210
- npm run check:composition # composed phrases: «Пн, Ср в 09:00», «Раз в 2 дня»
211
- npm run check:namespaces # NAMESPACES.md matches the built bundle
212
- npm run check:tarball # what gets installed: the archive is unpacked and checked
213
- npm run check:workflows # both workflows run the whole verify chain
214
- npm run check:readmes:pairs # the three READMEs move together (recorded section hashes)
215
- npm run check:plugin-names # plugin names in Russian on a real document
216
- npm run check:descriptor-patch # the harness patch matches the translations
217
- npm run audit -- <harness tree> # the locale audit
218
- npm run verify -- <harness tree> # everything together
219
-
220
- npm run namespaces # regenerate NAMESPACES.md and pack-count.json
221
- npm run readmes:record # re-record section hashes after editing all three READMEs
222
- npm run diff:harness -- <old> <new> # what the harness reworded under us
223
- ```
224
-
225
- A separate check composes phrases the way the harness does and reads the result whole. The
226
- audit compares a value with its own template, so a phrase that is correct in isolation but
227
- broken by substitution stays invisible to it: `frequency.weekly` was filled as «По {weekdays}
228
- в {time} », while the days arrive as «Пн, Ср» — producing «По Пн, Ср в 09:00», which no
229
- line-by-line check can see. The same way «Каждые 2 дня» (now «Раз в 2 дня»), «Каждый день в
230
- 09:00 (Europe/Moscow)» and a composed interval with the plural forms are pinned. The third find
231
- of the same kind: `time.locale` is not text but a tag for `Intl`, and it was `en` — a rule
232
- restricted to a month read «Каждый день в January», and the zone label «Moscow Standard Time»
233
- inside a Russian interface. The tag is `ru` now (the harness's Chinese dictionary uses `zh-CN`
234
- the same way), and the month sits in parentheses: «Каждый день (январь, март)».
235
-
236
- The same part-by-part reading found the cron wording: abbreviations («Каждые {step} мин»,
237
- «Каждые {count} ч») and bookish turns («в минуту {minutes}», «минуты {minutes} в часы
238
- {hours}»). The counted parts gained plural forms — «Каждые 3 минуты» / «Каждые 5 минут»,
239
- «Каждые 2 часа» / «Каждые 6 часов» — and the rest were rewritten into one readable labelled
240
- style: «Каждый час, минуты: 5, 30», «минуты: 5, часы: 09–17». The whole schedule sentence is
241
- asserted end to end: «Пн–Пт каждые 5 минут». The date stamps are pinned the same way: «5 окт. 2026 г., 15:51» and the zone label «Москва, стандартное время» come from Intl with our tag, without a single Latin word.
242
-
243
- The audit runs in CI (`.github/workflows/verify.yml`) against a **pinned harness tag**,
244
- not against some random local checkout. The same run also fires **weekly on a schedule**: a
245
- translation rots without anyone editing it — the upstream moves on, everything is green
246
- locally, and the drift surfaces only when a user builds. The gates cover completeness (keys and
247
- namespaces), plural forms, placeholders, English and Chinese residue, terminology, Latin words in
248
- prose, text hygiene, edge spaces, style, typography of values, «ё», composed phrases, the length
249
- of settings sidebar labels and the advertised numbers — and every one of them has already caught
250
- real defects. Beside them the audit
251
- prints **advisory reports**,
252
- which are not gates because they need a human decision: how many namespaces were compared and
253
- which could not be linked, whether any «number + inflecting word» is left, and whether a short
254
- string grew past 2.8× its English (a layout risk — «Effort» → «Уровень рассуждения» is normal
255
- Russian length, not a defect).
256
-
257
- The audit reads harness templates in the shape the harness writes them: a literal, an **array of
258
- paragraphs** (that is how the preset guides are stored) and a **reference to a constant**
259
- (`'hint.plan': PLAN_NEXT_ACTION_EN`). While only the first shape was read, ten keys — eight of
260
- them whole guide documents — silently fell out of every comparison. And the other way round: an
261
- empty policy file (`keep-english.json`, a `terminology.json` section) now fails the audit instead
262
- of widening a gate unnoticed. The numbers the READMEs advertise are checked against the pack the
263
- same way, because a stale count is simply a wrong claim — and the same numbers are read from the
264
- `package.json` description, which is what npm shows in search and on the package page (that is
265
- where «55 namespaces» lived longest) — including the grouped spelling: the
266
- old leads said `2 489`, and the gate only read the ungrouped form, so the pattern now accepts
267
- thousands separators. The sentence naming the harness version the pack was built against is
268
- checked next to it, against the tag the workflow pins, because that workflow is what verifies
269
- the pack (both leads claimed `0.2.1-alpha.2` while CI had already moved to
270
- `dsh-v0.2.1-rc.2`). The published size is measured there too: `npm run check:size` packs the
271
- archive and compares the advertised kilobytes with the real ones, allowing one kilobyte of slack
272
- because gzip compresses slightly differently across Node builds while the sentence rounds to
273
- whole kilobytes.
274
-
275
- - **dead namespaces** — every namespace the package registers has to
276
- exist in the harness. In 0.1.6 the schedule catalog translations were registered
277
- under the name `schedule.manager`, whereas `schedule.catalog` is what reads them: the per-key
278
- coverage check nonetheless reported «всё переведено»;
279
- - **plural-form pairs** — the `.one`/`.other` sets have to match, because the client
280
- chooses the form as `count === 1 ? '.one' : '.other'`. That is not enough for Russian:
281
- every pair also needs `.few` and `.many`, or the wrapper quietly falls back to the neutral
282
- `.other` and shows «2 д» where «2 дня» was possible. The check runs **per namespace** —
283
- the wrapper looks the form up in the namespace that asked, so a pair missing on one screen
284
- while present on another would otherwise stay invisible. It found the four time units
285
- (`unit.day/hour/minute/second` in `schedule.catalog`) and `trigger` in the task manager;
286
- the units stopped being abbreviations («д», «ч», «мин») and gained full forms in the right
287
- case — «через 2 дня», «через 5 часов», and «Каждые 2 дня» became «Раз в 2 дня». The same
288
- requirement covers the keys the pack added itself (`terminal.expand` and its kin: the
289
- harness passes them a count but never pairs them) — their forms rest on us alone, and
290
- losing one would be invisible to everything else;
291
- - **placeholders** — a Russian value has to carry exactly the `{name}` placeholders its
292
- English template carries: an extra one reaches the screen literally
293
- (`Заняло {duration}7мин 26с`, fixed in #2), a dropped one silently removes the
294
- interpolated value. Packages that ship only the Chinese dictionary are compared
295
- against `zh`. Our **own plural forms** — the `.few`/`.many` we add, and the `.one` of a key
296
- the harness never paired — do not exist upstream, so each is compared with the template of
297
- a sibling form of the same word (`.one`/`.other`) or of the key itself. Without that, 52
298
- values were never checked at all: that is exactly how a `{count}` can disappear from a form
299
- and leave the sentence without its number.
300
- - **residue** — a value has to be Russian: an exact copy of the English template, a leftover
301
- Chinese character, or an empty string where the template has text is a defect. English
302
- copies are allowed only by a decision recorded in `scripts/keep-english.json` (addresses,
303
- format-only strings, protocol and tool names, abbreviations), so a new untranslated line
304
- cannot slip through unnoticed. The decisions themselves are checked too: an entry that no
305
- longer excuses anything fails the build — either its key left the pack, or the value was
306
- translated and the decision went stale. This is how the `deepSeekBaseUrl` entry was found:
307
- the English template gained a note about `DEEPSEEK_BASE_URL`, while the input placeholder
308
- has to be a bare example URL, so that note would have been noise inside the field. The entry
309
- is gone, the value stays. A decision may also name a closed list: `chat · app.*` allows
310
- thirty application names in one line, because a dictionary of application names is a list of
311
- names. And a value with no word outside its placeholders (`{from}–{to}`, `{percent}%`, `, `)
312
- needs no decision at all: there is nothing in it to translate, so the policy stays about
313
- words rather than about punctuation. The residue rule reads **every** template of a key, not
314
- only the `en` block: 60 values in dictionaries with an unrecognised block used to go
315
- unexamined, while now 2911 values are compared, 68 of them covered by decisions (38 entries,
316
- one of them a pattern), and the zone is gone as a class. The rule did not become wider:
317
- an empty value is a defect only where no template is empty either, since Russian justly
318
- drops the prefix that English drops too (`message.stepProcess.sharedPrefix`).
319
- - **dead keys** — a key no harness dictionary defines and no harness source names is a
320
- leftover from an older version: it shows nothing and only misleads. Such keys fail the
321
- build. A key a component still asks for stays in the advisory report, because the pack's
322
- value is what reaches the screen.
323
- - **terminology** — one concept, one rendering. Forbidden variants («кеш» for «кэш», «сеанс»
324
- for «сессия», «этап» for «шаг») fail the build, and `scripts/terminology.json` keeps both
325
- the decision and counts of the chosen forms, so drift shows up before it becomes a defect.
326
- The check immediately found `confirm.description` translated two ways in two namespaces —
327
- «шаги» and «этапы». One word may not be spelled both with and without «ё»: «ещё» next to
328
- «еще» reads as machine translation, so the check folds «ё» to «е» and fails on two spellings
329
- of one folded form. Real distinct pairs such as «все» and «всё» are declared in
330
- `scripts/yo-pairs.json`, and a declaration whose spellings left the pack fails as well. The
331
- same report covers Latin words inside Russian phrases: a product,
332
- command or code token is allowed, an ordinary English word in a sentence is not. A product name
333
- has to be spelled the way the harness spells it: «Github Desktop» for «GitHub Desktop» reads as a
334
- typo in the product's own name, and the pack is the only place it can appear. The reference is
335
- the English templates; a word the harness writes only in lowercase («cursor», «desktop») does not
336
- count, because there a capital is exactly the product name. Next to it
337
- comes mechanical hygiene: double spaces, a space before punctuation, unpaired «ёлочки» and
338
- brackets, a non-breaking space inside a value, and edge spaces — a value that starts or ends
339
- with one is glued to a neighbour by the component, so it has to match the English template
340
- of its own namespace. The same template checks style: a terminator the English carries must
341
- not disappear (this is how `registryCustomHint` was found without its period), and the case
342
- of the first letter has to match — except for acronyms like `JSON` and templates that open
343
- with a placeholder. The address to the reader is checked on its own: inside a sentence the
344
- Russian interface writes «вы» lowercase, and «Вы» with a capital letter only at the start of
345
- a sentence, where it is nothing but spelling. Finally the pack is compared with itself: one
346
- English sentence has to come back as one Russian sentence. When two namespaces render it
347
- differently — templates of four words or more are compared, because a short label legitimately
348
- follows its context — the build fails. This is how `confirm.description` was found: one
349
- rendering promised to «пропускать шаги подтверждения», the other to «уменьшать число шагов»,
350
- and the English «subsequent tasks» had become «текущей задаче» in the second. A deliberate
351
- divergence is declared in `scripts/one-rendering.json` with its reason, and an exception that
352
- no longer matches anything fails as well — otherwise the list of decisions would quietly widen
353
- the check. The opposite direction is printed as an advisory: the same Russian answer to
354
- **different** English sentences of four words or more hints at a copy-paste — a hint carried
355
- over from a neighbouring key, or two distinct facts collapsed into one phrase. It cannot be a
356
- gate, because Russian legitimately merges words English keeps apart, so both sides are printed
357
- together and a human decides.
358
-
359
- A separate workflow watches the tag itself: `watch-upstream` compares the pinned tag with
360
- the harness tags once a week, opens an issue with the steps when a newer one exists, and turns
361
- the run red — so lagging behind is found on a schedule rather than in a user's screenshot. The
362
- version comparison is checked without the network (`npm run check:upstream`: prerelease order,
363
- equal versions, a move between lines) and is
364
- part of `verify`: a release has to outrank its own prereleases, or the watch would fire forever.
365
-
366
- The coverage number is not a gate: some sections are deliberately left in English. Next to it
367
- the audit prints a **per-namespace** report: it rebuilds the namespace-to-dictionary link from
368
- whoever registers it (`NS`, or a literal in `locale.register`; `bind` and `PropsLocale` only read
369
- a namespace) and lists keys missing from both their own namespace and the shared `common`
370
- dictionary the runtime falls back to. A global name comparison cannot see those holes: a
371
- translation filed under another namespace counts as coverage while the screen shows English.
372
- The report is advisory — the same key name can mean different things in different namespaces
373
- (`list.aria` is "Active reminders" in the catalog and "Background jobs" in jobs), so a
374
- translation is copied across namespaces only where one dictionary serves both. On the pinned
375
- tag the report is empty: every string the harness renders in a namespace we register lives in
376
- that namespace. All 130 of the pack's namespaces are compared — the link comes both from a
377
- dictionary imported beside the registration and from the names that registration passes
378
- (`locale.register(NS, { zh, en })`); without the second rule `common` and `settings.locale`,
379
- whose dictionaries sit in a neighbouring directory, would stay uncovered. The other direction is
380
- a failure rather than a report: a namespace the harness registers and fills with keys, which the
381
- pack never registers, leaves that screen in English while key coverage still looks complete.
382
- This is how `settings.sessionLog` was found — the “Upload Session Log” switch on the general
383
- settings page.
384
-
385
- ## Coverage scope: the harness and plugin dictionaries
386
- The package translates **the harness client's own namespaces** — 64 namespaces reconciled
387
- against the pinned tag — and **the dictionaries of third-party plugins**: 66 plugins whose
388
- English text is vendored from their own publication, with the version and the licence.
389
-
390
- Plugin dictionaries were out of scope before, and there was a reason: `dshmarket` is already
391
- at version 1.66.44, its keys change independently of the harness, and a silent promise of
392
- «everything is covered» is not a promise but a debt nobody can collect. The answer turned
393
- out to be simple: the foreign English text is **vendored** (`npm run fetch:plugins`) and
394
- stored next to the translation, with the package name, the version and the licence, and the
395
- audit holds our translation to that snapshot rather than to somebody else's Russian. We
396
- checked the 17 most-downloaded plugins in the ecosystem (npm, `keywords:dsh-plugin`, monthly
397
- downloads) and looked at which of them ship strings of their own at all:
398
-
399
- | What we found | Plugins |
400
- |---|---|
401
- | **No strings of their own** — the interface takes its text from harness namespaces | **11 of 17** |
402
- | A large dictionary of their own (confirmed by hand) | 2 — `dshmarket` (733 keys), `@nanmicoder/dsh-agent-teams` (420) |
403
- | `.d.ts` types only, no strings | 1 — `dsh-plugin-model-proxy` |
404
- | Needs a separate check | 3 |
405
-
406
- For 11 of the 17 most popular plugins the Russian interface already works without a
407
- dictionary of their own: their text lives in the shared harness namespaces, which the package
408
- translates in full. Dictionaries exist where a plugin carries strings of its own — there
409
- are 32 of them, and each records the version its English text was vendored from.
410
-
411
- Such a dictionary is a commitment to track someone else's releases, but it is now a
412
- checkable one: a new version of the plugin with different keys shows up as a difference from
413
- the snapshot instead of as silence. Need a specific plugin? Open an issue: a dictionary for
414
- it is possible as a separate layer pinned to a version, rather than as a silent promise that
415
- «everything is covered».
416
-
417
- ## Development
418
-
419
- ```sh
420
- pnpm install
421
- pnpm run build # tsdown -> lib/index.js (node half) + lib/client.js (client bundle)
422
- npm pack # -> ragnoryok1-dsh-client-locale-ru-<version>.tgz
423
- ```
424
-
425
- `lib/client.js` is built in the format the `dsh` client loader understands (`__ModuleLoader__.load`) and exports `inject`/`apply`.
426
-
427
- ## Releases
428
-
429
- Publishing to npm is done by the `publish` workflow on a tag — the credentials live in a repository
430
- secret (`NPM_TOKEN`), so the token never has to be typed into a terminal or passed around.
431
- Publishing by hand from a machine is possible, but unnecessary.
432
-
433
- ```
434
- npm version 0.3.0 --no-git-tag-version # поднять версию в package.json
435
- # …и синхронно обновить версию в блоке deepseek-harness-meta в этом README
436
- git commit -am "release: 0.3.0" && git tag v0.3.0 && git push origin v0.3.0
437
- ```
438
-
439
- Before publishing, the workflow checks:
440
-
441
- 1. the version in the tag matches the version in `package.json`;
442
- 2. the version in the `deepseek-harness-meta` block matches `package.json` — the marketplace takes it from there,
443
- and reads it **only on first indexing**, so a mismatch does not fix itself;
444
- 3. the typography (18 rules);
445
- 4. the localization audit against the pinned harness tag — dead namespaces, duplicates and plural-form
446
- pairs will fail and stop the publication;
447
- 5. **compatibility with every version in `compatible_versions`**: each one's locale runtime is
448
- downloaded from npm and three suites — language coexistence, plural forms and composed phrases
449
- — run against it (`npm run check:compat`, part of CI), so the promise in the meta block is
450
- checked rather than assumed.
451
-
452
- The declaration itself is checked separately (`npm run check:peers`, part of `verify`): the harness
453
- reads our `peerDependencies` for `@deepseek-ai/dsh*` not as package versions but as a range of
454
- **runtime versions**, with `includePrerelease`, and a range that fails to satisfy the running dsh
455
- makes the user see an incompatibility warning and an exemption prompt. The script runs that rule
456
- over every claimed version — all eight pass today.
457
-
458
- To check all the same things locally, without publishing:
459
-
460
- ```
461
- npm run verify -- путь/к/дереву/харнесса
462
- ```
463
-
464
- The workflow also has a manual run with a «только проверки, без публикации» toggle
465
- (`workflow_dispatch` → `dry-run`).
466
-
467
- ## Contents
468
-
469
- - `src/client/dicts.ts` — the Russian dictionaries (130 namespaces, 14,086 translations, ~15,000 lines). This is where the main value lies.
470
- - `src/client/index.ts` — the client plugin entry point (registration of `ru` + the dictionaries).
471
- - `src/index.ts` — the empty host half (`apply()`).
472
- - `cordis.patch.yml` — the profile patch (`- insert:` of the client entry `locale-ru`).
473
-
474
- ## What is translated
475
-
476
- The current set follows the harness tags: the keys were taken from the matching commit, and keys
477
- added later fall back to `en` on lookup, so the package does not break the interface on newer versions.
478
-
479
- ### 0.1.7 — dead namespace fixed, task manager translated, plural forms rewritten
480
-
481
- The headline item in the release is **a fix for a mistake from 0.1.6**. At the time it was recorded
482
- that the namespace `schedule.catalog` had been retired and that `schedule.manager` had taken its place.
483
- In reality **both** namespaces live in the harness, separately: `schedule.catalog` is the reminder
484
- catalog in the session header with frequencies and units, `schedule.manager` is the new automation-task
485
- manager. Because of that wrong conclusion, ~30 translation strings (reminders, frequency, time
486
- units) were registered under a name nobody reads and were **never shown** in the interface. The
487
- catalog is now registered as `schedule.catalog`.
488
-
489
- - **fixed:** the schedule catalog translations are visible again — `trigger.*`, `list.*`,
490
- `delete.*`, `frequency.*`, `cron.*`, `unit.*`, `relative.*`, `mark.aria`, `hover.more`;
491
- - **`schedule.manager` added** (188 keys) — the automation-task manager entirely in
492
- Russian: the task panel and catalog, search, status filter, task details and its tabs,
493
- the **run log** with retention rules, the schedule editor (date, time, hour, minute,
494
- second, time zone, repeat interval), repeat rules (once, every N minutes/hours/
495
- seconds, daily, Mon–Fri, weekly, custom cron), the cron form with date and minute
496
- pickers, deletion with confirmation, notifications and cards;
497
- - **plural forms rewritten** (17 key pairs): forms that broke on the numbers 2–4
498
- were replaced with ones that are correct for any `n ≥ 2` — for example «2 субагентов» → «Субагентов: 2»,
499
- «каждые 2 часов» → «каждые 2 ч». Another 8 strings with `{count}` before an inflected word
500
- were switched to abbreviations: «ещё 3 строк» → «ещё 3 стр.»;
501
- - **7 missing keys added**: `blocked.composer`,
502
- `defaultWorkspace.title`, `schedule.active`, `settings.transcript.expanded`,
503
- `status.scheduled`, `status.overdue` and the namespace `shortcuts.layout` (the label for
504
- the command that shows the left panel);
505
- - **a new check in the project:** a namespace-name audit. The previous coverage check
506
- compared keys inside a namespace but never compared the names themselves, so an entire
507
- namespace could be registered under a non-existent name and quietly do nothing. Names
508
- are now extracted from every way they can be declared (`const NS`/`SETTINGS_NS`/`namespace`,
509
- `locale.register`, `locale.bind`, `PropsLocale<…>`) and compared as sets.
510
-
511
- 0.1.2 added sections that 0.1.1 did not have: `open-in-app` («Открыть в
512
- приложении»), `sidebarFiles`, `sidebarRight` (the right sidebar with files) and
513
- `sidebarCodePreview` (code preview). It also updated the keys that changed in
514
- harness 0.1.5 and removed 21 keys that no longer exist there.
515
-
516
- In 0.1.3 the package caught up with harness 0.1.6-alpha.1: 139 keys and 7 new
517
- sections were added — `sidebarTerminal` (the terminal in the right sidebar), `settings.archivedSessions`
518
- (archived sessions) and the document-preview family (`sidebarDocumentPreview`,
519
- `documentHtml`, `sidebarImage`, `documentMarkdown`, `sidebarPdf`). Besides that, the experimental
520
- «Автопроверка» was translated (`permission.access`: `auto.*`), along with the command-palette
521
- labels and tokens, the endpoint fields in the model settings, and the
522
- changes in `conversation`, `trajectory`, `chat`, `session-log-download`.
523
- 28 keys that had disappeared from the harness were removed.
524
-
525
- The coverage check runs against the harness sources: for each namespace the
526
- key sets are compared, so the next release starts from an exact
527
- “add / check / remove” list.
528
-
529
- In 0.1.6 the package caught up with harness **0.1.7-rc.2**: **252 keys** added, 18 updated,
530
- 26 removed, and two new namespaces added. The changes:
531
-
532
- - **`shortcuts`** (new namespace, 56 keys) — the hotkey editor: recording
533
- a combination, conflicts, reserved combinations, hints for macOS and
534
- Windows, restoring the default values;
535
- - **`schedule.manager`** (70 keys at the time) — the automation-task manager: list,
536
- deletion, repeat, run history, Cron expressions. **It was wrongly recorded that
537
- the namespace `schedule.catalog` had been retired by this; 0.1.7 fixed that — both
538
- namespaces exist side by side**;
539
- - **`settings.account`** (50 keys) — the app's first launch: the welcome,
540
- the choice of purpose and of how much of the work is shown, topping up the balance, signing out of
541
- the account while tasks are running;
542
- - added the labels for adding and removing tools on the fly
543
- (`message.tools*`, `trajectory.layout.*`), the right sidebar commands
544
- (`sidebarRight`), the «сначала выберите сессию» states in the panels, and updated
545
- the name of the settings section: «Инструменты для кода» instead of «Инструментов
546
- разработчика»;
547
- - 18 keys updated to match the changed English text, 26 removed. A single term,
548
- «рабочая папка», instead of «рабочей области» in one place.
549
-
550
- In 0.1.5 the package caught up with harness **0.1.7-rc.1**: **49 keys** added, 13 updated,
551
- 16 removed, and the namespace `directory-browser` dropped (it is gone in rc.1). There appeared
552
- the labels for the steps tools go through (`message.stepProcess.prepare.*` and `done.*` —
553
- reading images, writing files, preparing for code search, web search,
554
- subagent coordination and others), full-screen image viewing
555
- (`image.*`), the «Стандартный» and «Подробный» modes for showing the work, installing plugins
556
- from GitHub with a timeout and a Chinese mirror, the warning about an incompatible
557
- plugin version, the choice of model download source for voice input, and `Cordis` command
558
- messages. The wording was tightened where upstream replaced “teammate/agent”
559
- with “subagent” and “Beta” with “Experimental”.
560
-
561
- 0.1.5 also fixed the counting unit in the plugin inventory: it was «187 плагины»,
562
- now it is «187 плагинов». The built-in interface screenshots were translated too, npm
563
- keywords were added, and the peer range of `@deepseek-ai/dsh-client-locale`
564
- (`>=0.1.0-rc.2 <0.2.0`) was fixed — previously `^0.1.0` did not resolve, because the package
565
- is published only under prerelease versions.
566
-
567
- In 0.1.4 the package caught up with harness **0.1.7-alpha.2** — the biggest jump:
568
- **905 keys** added, 29 changed, 126 removed. New sections:
569
-
570
- - **`pluginManager`** — the built-in plugin panel: installation, removal,
571
- enabling and disabling components, versions, registries and install hints;
572
- - **`settings.account`** — the DeepSeek account: sign-in, balance, spend,
573
- API key;
574
- - the separate settings sections the former `settings.plugins` split into:
575
- **`settings.agentLoop`**, **`settings.shell`**, **`settings.subagent`**,
576
- **`settings.webSearch`**;
577
- - **`sidebarBrowser`** — the built-in browser in the right panel (address, sandbox,
578
- restoring the page);
579
- - **`sidebarExcel`** and **`sidebarOffice`** — preview of Office spreadsheets and
580
- documents; zooming (`zoom.*`) was added to PDFs and images;
581
- - **`voice-input`** — experimental voice input.
582
-
583
- Also translated were the new texts in `conversation` (+194), `chat` (+53),
584
- `workspace` (+43: session archive, pinning), `deliverables` (+31: the review of
585
- work changes), `job` (+24: live output of background tasks), `settings` (+22:
586
- desktop app update), `plan`, `open-in-app`, `trajectory`,
587
- `model`, `reference`, `session-log-download`, and in the agent-mode
588
- guides (`settings.agentPreset`, 12 long texts). The section
589
- `settings.archivedSessions` was removed: in 0.1.7 upstream dropped that package, and the session
590
- archive moved to `workspace`.
591
-
592
- ## License
593
-
594
- MIT
595
-
596
- ## Changelog
597
-
598
- [CHANGELOG.md](CHANGELOG.md) — what was in each release.
1
+ <!-- deepseek-harness-meta
2
+ {
3
+ "name": "Russian (ru) language pack for DeepSeek Harness",
4
+ "version": "0.2.14",
5
+ "tags": ["localization", "russian", "i18n", "web-gui", "language-pack"],
6
+ "description": "Full Russian interface (i18n) for the DeepSeek Harness web GUI: 584 namespaces, 67724 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
+ "icon": "https://raw.githubusercontent.com/Ragnoryok1/dsh-client-locale-ru/main/assets/icon.png",
8
+ "compatible_versions": ["v0.2.1-rc.2", "v0.2.1-rc.1", "v0.2.1-alpha.5", "v0.2.1-alpha.4", "v0.2.1-alpha.3", "v0.2.1-alpha.2", "v0.2.1-alpha.1", "v0.2.0-rc.2", "v0.2.0-rc.1", "v0.1.7-rc.2", "v0.1.7-rc.1", "v0.1.7-alpha.2"],
9
+ "screenshots": "images",
10
+ "install_method": "dsh plugin --profile web add @ragnoryok1/dsh-client-locale-ru"
11
+ }
12
+ -->
13
+
14
+ # @ragnoryok1/dsh-client-locale-ru
15
+
16
+ [Русский](README.md) | **English** | [中文](README.zh.md)
17
+
18
+ ![Russian language pack for DeepSeek Harness](assets/banner.jpg)
19
+
20
+ [![npm](https://img.shields.io/npm/v/%40ragnoryok1%2Fdsh-client-locale-ru)](https://www.npmjs.com/package/@ragnoryok1/dsh-client-locale-ru)
21
+ [![license](https://img.shields.io/npm/l/%40ragnoryok1%2Fdsh-client-locale-ru)](LICENSE)
22
+ [![verify](https://github.com/Ragnoryok1/dsh-client-locale-ru/actions/workflows/verify.yml/badge.svg)](https://github.com/Ragnoryok1/dsh-client-locale-ru/actions/workflows/verify.yml)
23
+ [![dsh](https://img.shields.io/badge/dsh-0.2.1--rc.2%2B-6366f1)](https://github.com/deepseek-ai/deepseek-harness)
24
+ [![namespaces](https://img.shields.io/badge/namespaces-584-6466f1)](https://github.com/Ragnoryok1/dsh-client-locale-ru)
25
+ [![strings](https://img.shields.io/badge/strings-67724-6466f1)](https://github.com/Ragnoryok1/dsh-client-locale-ru)
26
+
27
+ Russian (`ru`) language pack for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) web GUI (`dsh`). It registers `ru` as a selectable client language through the locale service (`ctx.locale.addLanguage`) plus one dictionary per namespace — 584 namespaces, 67,724 strings, built against dsh `0.2.1-rc.2`. Install with `dsh plugin --profile web add @ragnoryok1/dsh-client-locale-ru`, then pick **Русский** in Settings → General. Missing keys fall back to English, so a newer harness keeps working. The scope is the harness's own client interface — 64 namespaces (2 996 strings) — plus the dictionaries of 520 third-party plugins, translated from the English each plugin publishes (64728 strings in total). MIT, community-maintained, not affiliated with DeepSeek.
28
+
29
+ > **If the pack was useful to you, please star the repository.** It is the only signal that a
30
+ > translation is wanted, and it is what decides which parts get worked on next.
31
+
32
+ ---
33
+
34
+ The complete inventory is in [NAMESPACES.md](NAMESPACES.md): 584 namespaces, 67,724 strings, with the bundled plugin dictionaries marked.
35
+
36
+ The plugin names and descriptions on the Plugins page cannot come from this pack: the harness
37
+ keeps them in `locale/<language>.json` inside each package, builds a translation map from them,
38
+ and its locale service resolves that map without consulting dictionaries at all — «maps do not
39
+ consult registered dictionaries». 221 harness packages ship `locale/en.json`, 219 of them ship
40
+ `locale/zh.json`, and **none ships `locale/ru.json`**, so those 261 strings stay English with any
41
+ language pack; only `locale/ru.json` files on the harness side can close them.
42
+ `scripts/descriptor-coverage.mjs` counts that gap so it is not mixed into our dictionary coverage; the mechanism and the 32 prepared translations are in `docs/descriptor-upstream.md`.
43
+ Plugins outside the harness tree are translated the same way, but their English comes from
44
+ their own publication: `plugins/<namespace>.json` records the package, version, licence and the
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.
49
+ shows what in your installation is still English: it walks the profile's `node_modules`, reads
50
+ each plugin's own dictionary and compares its keys with ours. A plugin without a dictionary is
51
+ reported as such — there is nothing to translate until it asks for one — and a plugin that
52
+ already carries its own Russian is marked separately. That is how dictionaries
53
+ that would otherwise not exist at all get covered — today `update` (43 strings) for
54
+ `@linxin666/dsh-update`, `git-graph` (63 strings) for `@linxin666/dsh-client-ui-git-graph`,
55
+ `dsh-web-ui-usage` (63 strings) for `@linxin666/dsh-usage` and `dsh-web-ui-session-archive`
56
+ (124 strings) for `@linxin666/dsh-session-archive`, `pet` (81 strings) for `@linxin666/dsh-pet`
57
+ `remote` (115 strings) for `@linxin666/dsh-remote-web-ui`, `dsh-skill-explorer`
58
+ (75 strings) for `@linxin666/dsh-client-ui-skill-explorer` and `dsh-web-ui-market`
59
+ (106 strings) for `@linxin666/dsh-client-ui-market`, `liangshen` (42 strings) for
60
+ `@linxin666/dsh-liangshen` and `dsh-web-ui-preset-center` (46 strings) for
61
+ `@linxin666/dsh-client-ui-preset-center`, `dsh-ssh` (154 strings) for `@linxin666/dsh-ssh` and
62
+ `task-board` (437 strings) for `@linxin666/dsh-client-ui-task-board` and `task-board-github`
63
+ (113 strings) for `@linxin666/dsh-client-ui-task-board-github` — all thirteen extractable
64
+ dictionaries of that family, each translated in full. Another pack's Russian is never copied: there would be nothing to verify it
65
+ against, while the audit compares our value with the English template of that same plugin.
66
+
67
+ ## What it looks like
68
+
69
+ Russian in the language list and the translated settings sections (the screenshots were taken in build `0.1.8`:
70
+ the same screen, but the wording and some sections have changed since — the font-size caption, for one, now reads
71
+ «Влияет только на содержимое диалога»):):
72
+
73
+ ![Settings in Russian: language, appearance, themes](images/settings-ru.png)
74
+
75
+ ![The plugin list in Russian: section headings and the «Экспериментально» badges](images/plugins-ru.png)
76
+
77
+ Both screenshots are cropped at the left edge of the panel: workspace and session names are
78
+ not in frame.
79
+
80
+ Russian locale (`ru`) for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) web GUI (`dsh`).
81
+
82
+ The plugin adds **Russian** as a selectable language through the locale's language-pack contract: `ctx.locale.addLanguage({ id: 'ru', label: 'Русский', fallback: 'en' })` plus one `ru` dictionary per namespace. Once **Русский** is picked in Settings → General, the interface switches over immediately.
83
+
84
+ > **If the package was useful to you, give it a star.** It is the only way to show that a
85
+ > translation is wanted, and it genuinely affects which tasks get taken on. Not a single
86
+ > “like” button in the interface does that.
87
+ >
88
+ > The repository, meanwhile, remains the place where the state of the translation is visible:
89
+ > a [CHANGELOG](CHANGELOG.md) breaking down every release (including the mistakes that were found
90
+ > in it by its own maintainers), the CI checks, and the list of what is deliberately not translated.
91
+
92
+ ## Installation
93
+
94
+ This is a **hybrid plugin** (`dsh.bundle` + `dsh.client`), so it installs as an ordinary profile bundle through `dsh plugin`:
95
+
96
+ ```sh
97
+ dsh plugin --profile web add @ragnoryok1/dsh-client-locale-ru
98
+ ```
99
+
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
+
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
+ 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
+
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 2020 kB: the banner ships as JPEG, and the icon and source map stay out of the npm payload.
109
+
110
+ ## Why hybrid
111
+
112
+ `dsh web` takes client (UI) plugins only from **workspace packages**, and an external npm package carrying just a `dsh.client` is not picked up. For an external plugin with a UI half to be installable, it has to declare **`dsh.bundle`** (the profile layer), whose `cordis.patch.yml` adds the client entry `locale-ru` to the browser roster through `- insert:`. Other third-party plugins are built the same way (for example `@zseven-w/dsh-android`).
113
+
114
+ ## How it works
115
+
116
+ - **Host half** (`lib/index.js`) — an empty `apply()`: a language plugin needs no server logic, it only provides a valid cordis entry for the Loader.
117
+ - **Browser half** (`lib/client.js`) — registers `ru` and the dictionaries through the `locale` service (injected from `ctx`). There are no framework runtime imports — only the service.
118
+ - **No runtime dependencies** — `lib/client.js` imports no other packages, so it does not drag in heavy dependencies.
119
+ - **Effects** — the language and the dictionaries are registered through `ctx.effect(...)`, so they are unloaded together with the plugin (HMR-safe).
120
+ - **Plural forms** — the locale contract carries no pluralization rules: components choose the
121
+ form as `count === 1 ? 'key.one' : 'key.other'`, while Russian needs three (1 вызов /
122
+ 2 вызова / 5 вызовов). The package takes that over: alongside `.one` and `.other` the
123
+ dictionaries carry `.few` and `.many`, and a wrapper around the runtime's `translate`
124
+ recomputes the form with `Intl.PluralRules('ru')` for any call carrying a numeric `count`
125
+ or `n`. The number decides, not the key the component picked, so «2 вызова инструментов»
126
+ and «5 вызовов инструментов» come out of the same call site. The forms are not limited to
127
+ paired keys: where the harness passes a number to a key it never paired (`terminal.expand`
128
+ with `{n}`), the pack adds its own `.one` / `.few` / `.many` — «Показать ещё 1 строку /
129
+ 3 строки / 7 строк» — and the wrapper looks a form up for those keys too. Where a form does
130
+ not fit (a sentence carrying several numbers), the value is rewritten to be count-neutral:
131
+ «Символов: {total}» instead of «{total} символов», «файлов: {files}» instead of
132
+ «{files} файлов». Checked by `npm run check:plural` at counts 0, 1, 2, 3, 5 and 22, on an
133
+ unpaired key, on a fractional count, and on a foreign language pack whose forms our rules
134
+ must not rewrite.
135
+ - **Fallback** — a missing Russian key falls back to `en` (the configured `fallback`).
136
+
137
+ ## Typography
138
+
139
+ The package brings already-rendered Russian text into line with typographic rules: guillemets
140
+ («ёлочки») instead of straight quotes, an em dash instead of a hyphen between words, an ellipsis
141
+ instead of three dots, a non-breaking space between a number and the following word or unit of
142
+ measurement (so that «187 плагинов» does not break across two lines), and the same space after a
143
+ short word — a preposition, a conjunction or a particle — so that «в режим» wraps as a whole
144
+ instead of leaving «в» alone at the end of a line. The rules are deliberately conservative: they
145
+ fire only on unambiguous patterns, and code, input fields, editable text and elements with
146
+ `data-typography="off"` are not touched at all. A second pass changes nothing.
147
+
148
+ A rule for numeric ranges (an en dash in «5–10») is **deliberately absent**: the only string in
149
+ the whole pack with a hyphen between digits is the cron example `0 9 * * 1-5`, and a dash would
150
+ corrupt it. Measured, not assumed.
151
+
152
+ The rules are checked by a script rather than by eye: `npm run check:typography`.
153
+
154
+ The pass itself sees text nodes only, while a dictionary value also travels into attributes —
155
+ `title`, `aria-label`, `placeholder` — that it never reaches. The values are therefore checked
156
+ separately: the pack may not contain a straight quote, a hyphen standing for a dash, a dash
157
+ without spaces, or three dots. Otherwise the same string would look different depending on
158
+ where it landed, and in a hint or a screen-reader label the rules would not work at all.
159
+
160
+ Text that changes **after** the first pass — token counters, timers, progress — is rewritten
161
+ again, so a live «4 с» keeps its non-breaking space. This is checked on a real document rather
162
+ than on strings: `npm run check:typography:dom` (jsdom, six cases — the first pass, a later text
163
+ change, an added subtree, disposal, a foreign language, and a language switch).
164
+
165
+ The pass runs **only while Russian is active**: the rules are Russian, and applying them to
166
+ another language would edit text this pack does not own — `"quotes"` would become «quotes» in an
167
+ English interface. The pack listens to `locale/change` and starts or stops the pass on a switch
168
+ rather than on installation.
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: the names of the 32 harness plugins and the names and
179
+ descriptions of 33 third-party plugins whose dictionaries it translates. Each comes with the
180
+ English text the translation was made from and the Russian text. The rendered interface passes
181
+ through a pass that puts the Russian text where the English was — in the plugin cards, in the
182
+ rows of a bundle, on a plugin's page and in the settings inventory. The accessible name
183
+ (`aria-label`) is replaced too, so a card is read in Russian as a whole.
184
+
185
+ The pass matches exactly: a string is replaced only when the whole string is one of the titles
186
+ or descriptions the pack knows. A sentence that merely mentions a name keeps its wording;
187
+ code, input fields and the conversation are not touched. Like the typography pass, it runs
188
+ only while Russian is the active language.
189
+
190
+ The same 32 names go to the harness as a patch (`docs/harness-descriptors-ru.patch`): in the
191
+ harness interface the plugins are named in Russian, and the patch gets there first. The patch
192
+ is a build artefact — it is built from the translations and the harness tree, and the gate
193
+ `npm run check:descriptor-patch` fails when the built patch and the committed one differ.
194
+ The third-party names live in `src/client/plugin-names-ru.json`: the file is built from the
195
+ vendored plugin dictionaries (`npm run build:plugin-names`), and the gate
196
+ `npm run check:plugin-names:map` fails when the map has drifted from the dictionaries. The same
197
+ English name cannot be translated two ways — differently in the harness and in a plugin.
198
+ `npm run check:plugin-names` proves on a real document (jsdom, 12 cases) that the pass
199
+ translates the title, the description, the accessible name and a bundle row — and leaves what
200
+ it must alone.
201
+
202
+
203
+ ## Checks
204
+
205
+ ```sh
206
+ npm run build # the three outputs
207
+ npm run check:typography # 18 typography rules, including "leave alone"
208
+ npm run check:locale # dictionaries side by side: 6 checks
209
+ npm run check:plural # plural forms: 1 вызов / 2 вызова / 5 вызовов
210
+ npm run check:composition # composed phrases: «Пн, Ср в 09:00», «Раз в 2 дня»
211
+ npm run check:namespaces # NAMESPACES.md matches the built bundle
212
+ npm run check:tarball # what gets installed: the archive is unpacked and checked
213
+ npm run check:workflows # both workflows run the whole verify chain
214
+ npm run check:readmes:pairs # the three READMEs move together (recorded section hashes)
215
+ npm run check:plugin-names # plugin names in Russian on a real document
216
+ npm run check:descriptor-patch # the harness patch matches the translations
217
+ npm run audit -- <harness tree> # the locale audit
218
+ npm run verify -- <harness tree> # everything together
219
+
220
+ npm run namespaces # regenerate NAMESPACES.md and pack-count.json
221
+ npm run readmes:record # re-record section hashes after editing all three READMEs
222
+ npm run diff:harness -- <old> <new> # what the harness reworded under us
223
+ ```
224
+
225
+ A separate check composes phrases the way the harness does and reads the result whole. The
226
+ audit compares a value with its own template, so a phrase that is correct in isolation but
227
+ broken by substitution stays invisible to it: `frequency.weekly` was filled as «По {weekdays}
228
+ в {time} », while the days arrive as «Пн, Ср» — producing «По Пн, Ср в 09:00», which no
229
+ line-by-line check can see. The same way «Каждые 2 дня» (now «Раз в 2 дня»), «Каждый день в
230
+ 09:00 (Europe/Moscow)» and a composed interval with the plural forms are pinned. The third find
231
+ of the same kind: `time.locale` is not text but a tag for `Intl`, and it was `en` — a rule
232
+ restricted to a month read «Каждый день в January», and the zone label «Moscow Standard Time»
233
+ inside a Russian interface. The tag is `ru` now (the harness's Chinese dictionary uses `zh-CN`
234
+ the same way), and the month sits in parentheses: «Каждый день (январь, март)».
235
+
236
+ The same part-by-part reading found the cron wording: abbreviations («Каждые {step} мин»,
237
+ «Каждые {count} ч») and bookish turns («в минуту {minutes}», «минуты {minutes} в часы
238
+ {hours}»). The counted parts gained plural forms — «Каждые 3 минуты» / «Каждые 5 минут»,
239
+ «Каждые 2 часа» / «Каждые 6 часов» — and the rest were rewritten into one readable labelled
240
+ style: «Каждый час, минуты: 5, 30», «минуты: 5, часы: 09–17». The whole schedule sentence is
241
+ asserted end to end: «Пн–Пт каждые 5 минут». The date stamps are pinned the same way: «5 окт. 2026 г., 15:51» and the zone label «Москва, стандартное время» come from Intl with our tag, without a single Latin word.
242
+
243
+ The audit runs in CI (`.github/workflows/verify.yml`) against a **pinned harness tag**,
244
+ not against some random local checkout. The same run also fires **weekly on a schedule**: a
245
+ translation rots without anyone editing it — the upstream moves on, everything is green
246
+ locally, and the drift surfaces only when a user builds. The gates cover completeness (keys and
247
+ namespaces), plural forms, placeholders, English and Chinese residue, terminology, Latin words in
248
+ prose, text hygiene, edge spaces, style, typography of values, «ё», composed phrases, the length
249
+ of settings sidebar labels and the advertised numbers — and every one of them has already caught
250
+ real defects. Beside them the audit
251
+ prints **advisory reports**,
252
+ which are not gates because they need a human decision: how many namespaces were compared and
253
+ which could not be linked, whether any «number + inflecting word» is left, and whether a short
254
+ string grew past 2.8× its English (a layout risk — «Effort» → «Уровень рассуждения» is normal
255
+ Russian length, not a defect).
256
+
257
+ The audit reads harness templates in the shape the harness writes them: a literal, an **array of
258
+ paragraphs** (that is how the preset guides are stored) and a **reference to a constant**
259
+ (`'hint.plan': PLAN_NEXT_ACTION_EN`). While only the first shape was read, ten keys — eight of
260
+ them whole guide documents — silently fell out of every comparison. And the other way round: an
261
+ empty policy file (`keep-english.json`, a `terminology.json` section) now fails the audit instead
262
+ of widening a gate unnoticed. The numbers the READMEs advertise are checked against the pack the
263
+ same way, because a stale count is simply a wrong claim — and the same numbers are read from the
264
+ `package.json` description, which is what npm shows in search and on the package page (that is
265
+ where «55 namespaces» lived longest) — including the grouped spelling: the
266
+ old leads said `2 489`, and the gate only read the ungrouped form, so the pattern now accepts
267
+ thousands separators. The sentence naming the harness version the pack was built against is
268
+ checked next to it, against the tag the workflow pins, because that workflow is what verifies
269
+ the pack (both leads claimed `0.2.1-alpha.2` while CI had already moved to
270
+ `dsh-v0.2.1-rc.2`). The published size is measured there too: `npm run check:size` packs the
271
+ archive and compares the advertised kilobytes with the real ones, allowing one kilobyte of slack
272
+ because gzip compresses slightly differently across Node builds while the sentence rounds to
273
+ whole kilobytes.
274
+
275
+ - **dead namespaces** — every namespace the package registers has to
276
+ exist in the harness. In 0.1.6 the schedule catalog translations were registered
277
+ under the name `schedule.manager`, whereas `schedule.catalog` is what reads them: the per-key
278
+ coverage check nonetheless reported «всё переведено»;
279
+ - **plural-form pairs** — the `.one`/`.other` sets have to match, because the client
280
+ chooses the form as `count === 1 ? '.one' : '.other'`. That is not enough for Russian:
281
+ every pair also needs `.few` and `.many`, or the wrapper quietly falls back to the neutral
282
+ `.other` and shows «2 д» where «2 дня» was possible. The check runs **per namespace** —
283
+ the wrapper looks the form up in the namespace that asked, so a pair missing on one screen
284
+ while present on another would otherwise stay invisible. It found the four time units
285
+ (`unit.day/hour/minute/second` in `schedule.catalog`) and `trigger` in the task manager;
286
+ the units stopped being abbreviations («д», «ч», «мин») and gained full forms in the right
287
+ case — «через 2 дня», «через 5 часов», and «Каждые 2 дня» became «Раз в 2 дня». The same
288
+ requirement covers the keys the pack added itself (`terminal.expand` and its kin: the
289
+ harness passes them a count but never pairs them) — their forms rest on us alone, and
290
+ losing one would be invisible to everything else;
291
+ - **placeholders** — a Russian value has to carry exactly the `{name}` placeholders its
292
+ English template carries: an extra one reaches the screen literally
293
+ (`Заняло {duration}7мин 26с`, fixed in #2), a dropped one silently removes the
294
+ interpolated value. Packages that ship only the Chinese dictionary are compared
295
+ against `zh`. Our **own plural forms** — the `.few`/`.many` we add, and the `.one` of a key
296
+ the harness never paired — do not exist upstream, so each is compared with the template of
297
+ a sibling form of the same word (`.one`/`.other`) or of the key itself. Without that, 52
298
+ values were never checked at all: that is exactly how a `{count}` can disappear from a form
299
+ and leave the sentence without its number.
300
+ - **residue** — a value has to be Russian: an exact copy of the English template, a leftover
301
+ Chinese character, or an empty string where the template has text is a defect. English
302
+ copies are allowed only by a decision recorded in `scripts/keep-english.json` (addresses,
303
+ format-only strings, protocol and tool names, abbreviations), so a new untranslated line
304
+ cannot slip through unnoticed. The decisions themselves are checked too: an entry that no
305
+ longer excuses anything fails the build — either its key left the pack, or the value was
306
+ translated and the decision went stale. This is how the `deepSeekBaseUrl` entry was found:
307
+ the English template gained a note about `DEEPSEEK_BASE_URL`, while the input placeholder
308
+ has to be a bare example URL, so that note would have been noise inside the field. The entry
309
+ is gone, the value stays. A decision may also name a closed list: `chat · app.*` allows
310
+ thirty application names in one line, because a dictionary of application names is a list of
311
+ names. And a value with no word outside its placeholders (`{from}–{to}`, `{percent}%`, `, `)
312
+ needs no decision at all: there is nothing in it to translate, so the policy stays about
313
+ words rather than about punctuation. The residue rule reads **every** template of a key, not
314
+ only the `en` block: 60 values in dictionaries with an unrecognised block used to go
315
+ unexamined, while now 2911 values are compared, 68 of them covered by decisions (38 entries,
316
+ one of them a pattern), and the zone is gone as a class. The rule did not become wider:
317
+ an empty value is a defect only where no template is empty either, since Russian justly
318
+ drops the prefix that English drops too (`message.stepProcess.sharedPrefix`).
319
+ - **dead keys** — a key no harness dictionary defines and no harness source names is a
320
+ leftover from an older version: it shows nothing and only misleads. Such keys fail the
321
+ build. A key a component still asks for stays in the advisory report, because the pack's
322
+ value is what reaches the screen.
323
+ - **terminology** — one concept, one rendering. Forbidden variants («кеш» for «кэш», «сеанс»
324
+ for «сессия», «этап» for «шаг») fail the build, and `scripts/terminology.json` keeps both
325
+ the decision and counts of the chosen forms, so drift shows up before it becomes a defect.
326
+ The check immediately found `confirm.description` translated two ways in two namespaces —
327
+ «шаги» and «этапы». One word may not be spelled both with and without «ё»: «ещё» next to
328
+ «еще» reads as machine translation, so the check folds «ё» to «е» and fails on two spellings
329
+ of one folded form. Real distinct pairs such as «все» and «всё» are declared in
330
+ `scripts/yo-pairs.json`, and a declaration whose spellings left the pack fails as well. The
331
+ same report covers Latin words inside Russian phrases: a product,
332
+ command or code token is allowed, an ordinary English word in a sentence is not. A product name
333
+ has to be spelled the way the harness spells it: «Github Desktop» for «GitHub Desktop» reads as a
334
+ typo in the product's own name, and the pack is the only place it can appear. The reference is
335
+ the English templates; a word the harness writes only in lowercase («cursor», «desktop») does not
336
+ count, because there a capital is exactly the product name. Next to it
337
+ comes mechanical hygiene: double spaces, a space before punctuation, unpaired «ёлочки» and
338
+ brackets, a non-breaking space inside a value, and edge spaces — a value that starts or ends
339
+ with one is glued to a neighbour by the component, so it has to match the English template
340
+ of its own namespace. The same template checks style: a terminator the English carries must
341
+ not disappear (this is how `registryCustomHint` was found without its period), and the case
342
+ of the first letter has to match — except for acronyms like `JSON` and templates that open
343
+ with a placeholder. The address to the reader is checked on its own: inside a sentence the
344
+ Russian interface writes «вы» lowercase, and «Вы» with a capital letter only at the start of
345
+ a sentence, where it is nothing but spelling. Finally the pack is compared with itself: one
346
+ English sentence has to come back as one Russian sentence. When two namespaces render it
347
+ differently — templates of four words or more are compared, because a short label legitimately
348
+ follows its context — the build fails. This is how `confirm.description` was found: one
349
+ rendering promised to «пропускать шаги подтверждения», the other to «уменьшать число шагов»,
350
+ and the English «subsequent tasks» had become «текущей задаче» in the second. A deliberate
351
+ divergence is declared in `scripts/one-rendering.json` with its reason, and an exception that
352
+ no longer matches anything fails as well — otherwise the list of decisions would quietly widen
353
+ the check. The opposite direction is printed as an advisory: the same Russian answer to
354
+ **different** English sentences of four words or more hints at a copy-paste — a hint carried
355
+ over from a neighbouring key, or two distinct facts collapsed into one phrase. It cannot be a
356
+ gate, because Russian legitimately merges words English keeps apart, so both sides are printed
357
+ together and a human decides.
358
+
359
+ A separate workflow watches the tag itself: `watch-upstream` compares the pinned tag with
360
+ the harness tags once a week, opens an issue with the steps when a newer one exists, and turns
361
+ the run red — so lagging behind is found on a schedule rather than in a user's screenshot. The
362
+ version comparison is checked without the network (`npm run check:upstream`: prerelease order,
363
+ equal versions, a move between lines) and is
364
+ part of `verify`: a release has to outrank its own prereleases, or the watch would fire forever.
365
+
366
+ The coverage number is not a gate: some sections are deliberately left in English. Next to it
367
+ the audit prints a **per-namespace** report: it rebuilds the namespace-to-dictionary link from
368
+ whoever registers it (`NS`, or a literal in `locale.register`; `bind` and `PropsLocale` only read
369
+ a namespace) and lists keys missing from both their own namespace and the shared `common`
370
+ dictionary the runtime falls back to. A global name comparison cannot see those holes: a
371
+ translation filed under another namespace counts as coverage while the screen shows English.
372
+ The report is advisory — the same key name can mean different things in different namespaces
373
+ (`list.aria` is "Active reminders" in the catalog and "Background jobs" in jobs), so a
374
+ translation is copied across namespaces only where one dictionary serves both. On the pinned
375
+ tag the report is empty: every string the harness renders in a namespace we register lives in
376
+ that namespace. All 584 of the pack's namespaces are compared — the link comes both from a
377
+ dictionary imported beside the registration and from the names that registration passes
378
+ (`locale.register(NS, { zh, en })`); without the second rule `common` and `settings.locale`,
379
+ whose dictionaries sit in a neighbouring directory, would stay uncovered. The other direction is
380
+ a failure rather than a report: a namespace the harness registers and fills with keys, which the
381
+ pack never registers, leaves that screen in English while key coverage still looks complete.
382
+ This is how `settings.sessionLog` was found — the “Upload Session Log” switch on the general
383
+ settings page.
384
+
385
+ ## Coverage scope: the harness and plugin dictionaries
386
+ The package translates **the harness client's own namespaces** — 64 namespaces reconciled
387
+ against the pinned tag — and **the dictionaries of third-party plugins**: 520 plugins whose
388
+ English text is vendored from their own publication, with the version and the licence.
389
+
390
+ Searching the npm registry — six queries instead of one «dsh» — found 40 more plugins with
391
+ a dictionary of their own. 39 are translated; three names (`dsh-automation`, `remote`,
392
+ `settings.ip-pool`) are already taken by other plugins' dictionaries and stay with their
393
+ owners, so that nobody's product gets somebody else's translation. As of 22 October 2026:
394
+ **584 namespaces, 67 724 values, 520 dictionaries, 48 086 distinct Russian strings** in the
395
+ built package. The competitor measurement, by the same method —
396
+ [docs/competitors.md](https://github.com/Ragnoryok1/dsh-client-locale-ru/blob/main/docs/competitors.md).
397
+
398
+ Plugin dictionaries were out of scope before, and there was a reason: `dshmarket` is already
399
+ at version 1.66.44, its keys change independently of the harness, and a silent promise of
400
+ «everything is covered» is not a promise but a debt nobody can collect. The answer turned
401
+ out to be simple: the foreign English text is **vendored** (`npm run fetch:plugins`) and
402
+ stored next to the translation, with the package name, the version and the licence, and the
403
+ audit holds our translation to that snapshot rather than to somebody else's Russian. We
404
+ checked the 17 most-downloaded plugins in the ecosystem (npm, `keywords:dsh-plugin`, monthly
405
+ downloads) and looked at which of them ship strings of their own at all:
406
+
407
+ | What we found | Plugins |
408
+ |---|---|
409
+ | **No strings of their own** — the interface takes its text from harness namespaces | **11 of 17** |
410
+ | A large dictionary of their own (confirmed by hand) | 2 — `dshmarket` (733 keys), `@nanmicoder/dsh-agent-teams` (420) |
411
+ | `.d.ts` types only, no strings | 1 — `dsh-plugin-model-proxy` |
412
+ | Needs a separate check | 3 |
413
+
414
+ For 11 of the 17 most popular plugins the Russian interface already works without a
415
+ dictionary of their own: their text lives in the shared harness namespaces, which the package
416
+ translates in full. Dictionaries exist where a plugin carries strings of its own — there
417
+ are 32 of them, and each records the version its English text was vendored from.
418
+
419
+ Such a dictionary is a commitment to track someone else's releases, but it is now a
420
+ checkable one: a new version of the plugin with different keys shows up as a difference from
421
+ the snapshot instead of as silence. Need a specific plugin? Open an issue: a dictionary for
422
+ it is possible as a separate layer pinned to a version, rather than as a silent promise that
423
+ «everything is covered».
424
+
425
+ ## Development
426
+
427
+ ```sh
428
+ pnpm install
429
+ pnpm run build # tsdown -> lib/index.js (node half) + lib/client.js (client bundle)
430
+ npm pack # -> ragnoryok1-dsh-client-locale-ru-<version>.tgz
431
+ ```
432
+
433
+ `lib/client.js` is built in the format the `dsh` client loader understands (`__ModuleLoader__.load`) and exports `inject`/`apply`.
434
+
435
+ ## Releases
436
+
437
+ Publishing to npm is done by the `publish` workflow on a tag — the credentials live in a repository
438
+ secret (`NPM_TOKEN`), so the token never has to be typed into a terminal or passed around.
439
+ Publishing by hand from a machine is possible, but unnecessary.
440
+
441
+ ```
442
+ npm version 0.3.0 --no-git-tag-version # поднять версию в package.json
443
+ # …и синхронно обновить версию в блоке deepseek-harness-meta в этом README
444
+ git commit -am "release: 0.3.0" && git tag v0.3.0 && git push origin v0.3.0
445
+ ```
446
+
447
+ Before publishing, the workflow checks:
448
+
449
+ 1. the version in the tag matches the version in `package.json`;
450
+ 2. the version in the `deepseek-harness-meta` block matches `package.json` — the marketplace takes it from there,
451
+ and reads it **only on first indexing**, so a mismatch does not fix itself;
452
+ 3. the typography (18 rules);
453
+ 4. the localization audit against the pinned harness tag — dead namespaces, duplicates and plural-form
454
+ pairs will fail and stop the publication;
455
+ 5. **compatibility with every version in `compatible_versions`**: each one's locale runtime is
456
+ downloaded from npm and three suites — language coexistence, plural forms and composed phrases
457
+ — run against it (`npm run check:compat`, part of CI), so the promise in the meta block is
458
+ checked rather than assumed.
459
+
460
+ The declaration itself is checked separately (`npm run check:peers`, part of `verify`): the harness
461
+ reads our `peerDependencies` for `@deepseek-ai/dsh*` not as package versions but as a range of
462
+ **runtime versions**, with `includePrerelease`, and a range that fails to satisfy the running dsh
463
+ makes the user see an incompatibility warning and an exemption prompt. The script runs that rule
464
+ over every claimed version — all eight pass today.
465
+
466
+ To check all the same things locally, without publishing:
467
+
468
+ ```
469
+ npm run verify -- путь/к/дереву/харнесса
470
+ ```
471
+
472
+ The workflow also has a manual run with a «только проверки, без публикации» toggle
473
+ (`workflow_dispatch` → `dry-run`).
474
+
475
+ ## Contents
476
+
477
+ - `src/client/dicts.ts` — the Russian dictionaries (584 namespaces, 67,724 translations, ~46 000 lines). This is where the main value lies.
478
+ - `src/client/index.ts` — the client plugin entry point (registration of `ru` + the dictionaries).
479
+ - `src/index.ts` — the empty host half (`apply()`).
480
+ - `cordis.patch.yml` — the profile patch (`- insert:` of the client entry `locale-ru`).
481
+
482
+ ## What is translated
483
+
484
+ The current set follows the harness tags: the keys were taken from the matching commit, and keys
485
+ added later fall back to `en` on lookup, so the package does not break the interface on newer versions.
486
+
487
+ ### 0.1.7 — dead namespace fixed, task manager translated, plural forms rewritten
488
+
489
+ The headline item in the release is **a fix for a mistake from 0.1.6**. At the time it was recorded
490
+ that the namespace `schedule.catalog` had been retired and that `schedule.manager` had taken its place.
491
+ In reality **both** namespaces live in the harness, separately: `schedule.catalog` is the reminder
492
+ catalog in the session header with frequencies and units, `schedule.manager` is the new automation-task
493
+ manager. Because of that wrong conclusion, ~30 translation strings (reminders, frequency, time
494
+ units) were registered under a name nobody reads and were **never shown** in the interface. The
495
+ catalog is now registered as `schedule.catalog`.
496
+
497
+ - **fixed:** the schedule catalog translations are visible again — `trigger.*`, `list.*`,
498
+ `delete.*`, `frequency.*`, `cron.*`, `unit.*`, `relative.*`, `mark.aria`, `hover.more`;
499
+ - **`schedule.manager` added** (188 keys) — the automation-task manager entirely in
500
+ Russian: the task panel and catalog, search, status filter, task details and its tabs,
501
+ the **run log** with retention rules, the schedule editor (date, time, hour, minute,
502
+ second, time zone, repeat interval), repeat rules (once, every N minutes/hours/
503
+ seconds, daily, Mon–Fri, weekly, custom cron), the cron form with date and minute
504
+ pickers, deletion with confirmation, notifications and cards;
505
+ - **plural forms rewritten** (17 key pairs): forms that broke on the numbers 2–4
506
+ were replaced with ones that are correct for any `n ≥ 2` — for example «2 субагентов» → «Субагентов: 2»,
507
+ «каждые 2 часов» → «каждые 2 ч». Another 8 strings with `{count}` before an inflected word
508
+ were switched to abbreviations: «ещё 3 строк» → «ещё 3 стр.»;
509
+ - **7 missing keys added**: `blocked.composer`,
510
+ `defaultWorkspace.title`, `schedule.active`, `settings.transcript.expanded`,
511
+ `status.scheduled`, `status.overdue` and the namespace `shortcuts.layout` (the label for
512
+ the command that shows the left panel);
513
+ - **a new check in the project:** a namespace-name audit. The previous coverage check
514
+ compared keys inside a namespace but never compared the names themselves, so an entire
515
+ namespace could be registered under a non-existent name and quietly do nothing. Names
516
+ are now extracted from every way they can be declared (`const NS`/`SETTINGS_NS`/`namespace`,
517
+ `locale.register`, `locale.bind`, `PropsLocale<…>`) and compared as sets.
518
+
519
+ 0.1.2 added sections that 0.1.1 did not have: `open-in-app` («Открыть в
520
+ приложении»), `sidebarFiles`, `sidebarRight` (the right sidebar with files) and
521
+ `sidebarCodePreview` (code preview). It also updated the keys that changed in
522
+ harness 0.1.5 and removed 21 keys that no longer exist there.
523
+
524
+ In 0.1.3 the package caught up with harness 0.1.6-alpha.1: 139 keys and 7 new
525
+ sections were added — `sidebarTerminal` (the terminal in the right sidebar), `settings.archivedSessions`
526
+ (archived sessions) and the document-preview family (`sidebarDocumentPreview`,
527
+ `documentHtml`, `sidebarImage`, `documentMarkdown`, `sidebarPdf`). Besides that, the experimental
528
+ «Автопроверка» was translated (`permission.access`: `auto.*`), along with the command-palette
529
+ labels and tokens, the endpoint fields in the model settings, and the
530
+ changes in `conversation`, `trajectory`, `chat`, `session-log-download`.
531
+ 28 keys that had disappeared from the harness were removed.
532
+
533
+ The coverage check runs against the harness sources: for each namespace the
534
+ key sets are compared, so the next release starts from an exact
535
+ “add / check / remove” list.
536
+
537
+ In 0.1.6 the package caught up with harness **0.1.7-rc.2**: **252 keys** added, 18 updated,
538
+ 26 removed, and two new namespaces added. The changes:
539
+
540
+ - **`shortcuts`** (new namespace, 56 keys) — the hotkey editor: recording
541
+ a combination, conflicts, reserved combinations, hints for macOS and
542
+ Windows, restoring the default values;
543
+ - **`schedule.manager`** (70 keys at the time) — the automation-task manager: list,
544
+ deletion, repeat, run history, Cron expressions. **It was wrongly recorded that
545
+ the namespace `schedule.catalog` had been retired by this; 0.1.7 fixed that — both
546
+ namespaces exist side by side**;
547
+ - **`settings.account`** (50 keys) — the app's first launch: the welcome,
548
+ the choice of purpose and of how much of the work is shown, topping up the balance, signing out of
549
+ the account while tasks are running;
550
+ - added the labels for adding and removing tools on the fly
551
+ (`message.tools*`, `trajectory.layout.*`), the right sidebar commands
552
+ (`sidebarRight`), the «сначала выберите сессию» states in the panels, and updated
553
+ the name of the settings section: «Инструменты для кода» instead of «Инструментов
554
+ разработчика»;
555
+ - 18 keys updated to match the changed English text, 26 removed. A single term,
556
+ «рабочая папка», instead of «рабочей области» in one place.
557
+
558
+ In 0.1.5 the package caught up with harness **0.1.7-rc.1**: **49 keys** added, 13 updated,
559
+ 16 removed, and the namespace `directory-browser` dropped (it is gone in rc.1). There appeared
560
+ the labels for the steps tools go through (`message.stepProcess.prepare.*` and `done.*` —
561
+ reading images, writing files, preparing for code search, web search,
562
+ subagent coordination and others), full-screen image viewing
563
+ (`image.*`), the «Стандартный» and «Подробный» modes for showing the work, installing plugins
564
+ from GitHub with a timeout and a Chinese mirror, the warning about an incompatible
565
+ plugin version, the choice of model download source for voice input, and `Cordis` command
566
+ messages. The wording was tightened where upstream replaced “teammate/agent”
567
+ with “subagent” and “Beta” with “Experimental”.
568
+
569
+ 0.1.5 also fixed the counting unit in the plugin inventory: it was «187 плагины»,
570
+ now it is «187 плагинов». The built-in interface screenshots were translated too, npm
571
+ keywords were added, and the peer range of `@deepseek-ai/dsh-client-locale`
572
+ (`>=0.1.0-rc.2 <0.2.0`) was fixed — previously `^0.1.0` did not resolve, because the package
573
+ is published only under prerelease versions.
574
+
575
+ In 0.1.4 the package caught up with harness **0.1.7-alpha.2** — the biggest jump:
576
+ **905 keys** added, 29 changed, 126 removed. New sections:
577
+
578
+ - **`pluginManager`** — the built-in plugin panel: installation, removal,
579
+ enabling and disabling components, versions, registries and install hints;
580
+ - **`settings.account`** — the DeepSeek account: sign-in, balance, spend,
581
+ API key;
582
+ - the separate settings sections the former `settings.plugins` split into:
583
+ **`settings.agentLoop`**, **`settings.shell`**, **`settings.subagent`**,
584
+ **`settings.webSearch`**;
585
+ - **`sidebarBrowser`** — the built-in browser in the right panel (address, sandbox,
586
+ restoring the page);
587
+ - **`sidebarExcel`** and **`sidebarOffice`** — preview of Office spreadsheets and
588
+ documents; zooming (`zoom.*`) was added to PDFs and images;
589
+ - **`voice-input`** — experimental voice input.
590
+
591
+ Also translated were the new texts in `conversation` (+194), `chat` (+53),
592
+ `workspace` (+43: session archive, pinning), `deliverables` (+31: the review of
593
+ work changes), `job` (+24: live output of background tasks), `settings` (+22:
594
+ desktop app update), `plan`, `open-in-app`, `trajectory`,
595
+ `model`, `reference`, `session-log-download`, and in the agent-mode
596
+ guides (`settings.agentPreset`, 12 long texts). The section
597
+ `settings.archivedSessions` was removed: in 0.1.7 upstream dropped that package, and the session
598
+ archive moved to `workspace`.
599
+
600
+ ## License
601
+
602
+ MIT
603
+
604
+ ## Changelog
605
+
606
+ [CHANGELOG.md](CHANGELOG.md) — what was in each release.