@goodandready/dsh-russian-lang 0.1.3 → 0.1.4

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/lib/client.js CHANGED
@@ -296,6 +296,9 @@ window.__ModuleLoader__.load({
296
296
  "context-doctor": {
297
297
  "cd.attention": "Стоит посмотреть",
298
298
  "cd.catalog": "{n} скиллов",
299
+ "cd.catalog.few": "{n} скилла",
300
+ "cd.catalog.many": "{n} скиллов",
301
+ "cd.catalog.one": "{n} скилл",
299
302
  "cd.empty": "Данных пока нет",
300
303
  "cd.error": "Проверка не удалась",
301
304
  "cd.files": "файлов",
@@ -307,12 +310,18 @@ window.__ModuleLoader__.load({
307
310
  "cd.loading": "Проверка…",
308
311
  "cd.mcp": "Инструменты MCP",
309
312
  "cd.mcpTools": "{n} инструментов",
313
+ "cd.mcpTools.few": "{n} инструмента",
314
+ "cd.mcpTools.many": "{n} инструментов",
315
+ "cd.mcpTools.one": "{n} инструмент",
310
316
  "cd.refresh": "Обновить",
311
317
  "cd.residentTokens": "Проверка бюджета контекста",
312
318
  "cd.review": "Стоит посмотреть",
313
319
  "cd.reviewHint": "Некоторые части контекста стоит пересмотреть, пока они не стали дорогими.",
314
320
  "cd.skills": "Каталог скиллов",
315
321
  "cd.suggestions": "{n} рекомендаций",
322
+ "cd.suggestions.few": "{n} рекомендации",
323
+ "cd.suggestions.many": "{n} рекомендаций",
324
+ "cd.suggestions.one": "{n} рекомендация",
316
325
  "cd.title": "Аудит контекста",
317
326
  "cd.tools": "Схемы инструментов",
318
327
  "cd.toolsCount": "инструментов",
@@ -1758,6 +1767,8 @@ window.__ModuleLoader__.load({
1758
1767
  "rename.workspace.title": "Переименовать рабочую папку",
1759
1768
  "search.clear": "Очистить поиск",
1760
1769
  "search.hasMore": "Показаны первые {n} результатов. Уточните запрос.",
1770
+ "search.hasMore.few": "Показаны первые {n} результата. Уточните запрос.",
1771
+ "search.hasMore.one": "Найден один подходящий результат.",
1761
1772
  "search.noMatches": "Подходящих сессий нет",
1762
1773
  "search.pending": "Поиск по истории сессий…",
1763
1774
  "search.placeholder": "Поиск по сессиям...",
@@ -1830,13 +1841,24 @@ window.__ModuleLoader__.load({
1830
1841
  return params ? fill(overrides[key], params) : overrides[key]
1831
1842
  }
1832
1843
  // 2. Плюрализация для русского.
1833
- const m = /^(.*)[.](one|other)$/.exec(key)
1834
- if (m && runtime.getLocale().active === 'ru' && params) {
1844
+ if (runtime.getLocale().active === 'ru' && params) {
1835
1845
  const n = params.n ?? params.count
1836
1846
  if (typeof n === 'number') {
1837
1847
  const form = pluralRules.select(n)
1838
- if (form === 'few' || form === 'many') {
1839
- const pluralKey = m[1] + '.' + form
1848
+ const m = /^(.*)[.](one|other)$/.exec(key)
1849
+ if (m) {
1850
+ // Ядро выбирает .one/.other по n===1; русскому нужны few/many.
1851
+ if (form === 'few' || form === 'many') {
1852
+ const pluralKey = m[1] + '.' + form
1853
+ const template = this.lookup(ns, pluralKey) ?? this.lookup('common', pluralKey)
1854
+ if (template !== undefined) {
1855
+ return fill(template, params)
1856
+ }
1857
+ }
1858
+ } else if (form !== 'other' && !/[.](one|other|few|many)$/.test(key)) {
1859
+ // Счётный ключ без суффикса: t('X', {n}). Если словарь даёт формы
1860
+ // X.one / X.few / X.many - берём подходящую, иначе как раньше.
1861
+ const pluralKey = key + '.' + form
1840
1862
  const template = this.lookup(ns, pluralKey) ?? this.lookup('common', pluralKey)
1841
1863
  if (template !== undefined) {
1842
1864
  return fill(template, params)
@@ -1947,6 +1969,166 @@ window.__ModuleLoader__.load({
1947
1969
  const unsubscribeLang = runtime.subscribe(syncLang)
1948
1970
  ctx.effect(() => unsubscribeLang, 'dsh-russian-lang: html-lang')
1949
1971
  syncLang()
1972
+
1973
+ // 5. Орфография (russian-lang.spellcheck): при активном русском включаем
1974
+ // браузерный спелчек на текстовых полях. Код-редакторы и поля команд не
1975
+ // трогаем - отличаем их по моноширинному шрифту. Исходные значения
1976
+ // сохраняем в data-атрибутах и возвращаем при уходе с русского.
1977
+ const SPELL_ON = 'data-russian-lang-spell-on'
1978
+ const SPELL_WAS = 'data-russian-lang-spell-was'
1979
+ const LANG_WAS = 'data-russian-lang-lang-was'
1980
+ const EDITABLE = 'textarea, input[type=text], input[type=search], [contenteditable=""], [contenteditable="true"]'
1981
+ const MONO_RE = /mono|consol|courier/i
1982
+ const isMonoField = (el) => {
1983
+ try { return MONO_RE.test(getComputedStyle(el).fontFamily || '') }
1984
+ catch (err) { return false }
1985
+ }
1986
+ const spellOn = (el) => {
1987
+ if (el.hasAttribute(SPELL_ON) || isMonoField(el)) return
1988
+ el.setAttribute(SPELL_ON, '1')
1989
+ el.setAttribute(SPELL_WAS, el.getAttribute('spellcheck') ?? '')
1990
+ el.setAttribute(LANG_WAS, el.getAttribute('lang') ?? '')
1991
+ el.setAttribute('spellcheck', 'true')
1992
+ el.setAttribute('lang', 'ru-RU')
1993
+ }
1994
+ const spellOff = (el) => {
1995
+ if (!el.hasAttribute(SPELL_ON)) return
1996
+ const was = el.getAttribute(SPELL_WAS)
1997
+ if (was === '') el.removeAttribute('spellcheck')
1998
+ else el.setAttribute('spellcheck', was)
1999
+ const lang = el.getAttribute(LANG_WAS)
2000
+ if (lang === '') el.removeAttribute('lang')
2001
+ else el.setAttribute('lang', lang)
2002
+ el.removeAttribute(SPELL_ON)
2003
+ el.removeAttribute(SPELL_WAS)
2004
+ el.removeAttribute(LANG_WAS)
2005
+ }
2006
+ let spellObserver = null
2007
+ const syncSpell = () => {
2008
+ try {
2009
+ if (typeof document === 'undefined') return
2010
+ const ru = runtime.getLocale().active === 'ru'
2011
+ if (!ru) {
2012
+ if (spellObserver) { spellObserver.disconnect(); spellObserver = null }
2013
+ document.querySelectorAll('[' + SPELL_ON + ']').forEach(spellOff)
2014
+ return
2015
+ }
2016
+ document.querySelectorAll(EDITABLE).forEach(spellOn)
2017
+ if (spellObserver) return
2018
+ // ponytail: реагируем только на добавленные узлы; полям, сменившим
2019
+ // шрифт на месте, поможет следующая перезагрузка страницы.
2020
+ spellObserver = new MutationObserver((records) => {
2021
+ for (const record of records) {
2022
+ for (const node of record.addedNodes) {
2023
+ if (node.nodeType !== 1) continue
2024
+ if (node.matches(EDITABLE)) spellOn(node)
2025
+ node.querySelectorAll ? node.querySelectorAll(EDITABLE).forEach(spellOn) : null
2026
+ }
2027
+ }
2028
+ })
2029
+ spellObserver.observe(document.body, { childList: true, subtree: true })
2030
+ } catch (err) { /* ignore */ }
2031
+ }
2032
+ const unsubscribeSpell = runtime.subscribe(syncSpell)
2033
+ ctx.effect(() => {
2034
+ unsubscribeSpell()
2035
+ if (spellObserver) spellObserver.disconnect()
2036
+ try { document.querySelectorAll('[' + SPELL_ON + ']').forEach(spellOff) } catch (err) { /* ignore */ }
2037
+ }, 'dsh-russian-lang: spellcheck')
2038
+ syncSpell()
2039
+
2040
+ // 6. Типографика (russian-lang.typography { enabled, yo }): постпроцессор
2041
+ // текстовых узлов при активном русском - ёлочки, тире, неразрывные
2042
+ // пробелы перед короткими словами, опционально ё (безопасный список).
2043
+ // Код, ссылки, кнопки и поля ввода не трогаем. Правила идемпотентны,
2044
+ // повторный проход по своим же правкам ничего не меняет.
2045
+ const TYPO_SKIP = new Set(['CODE', 'PRE', 'A', 'SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'SELECT', 'OPTION', 'KBD', 'SAMP', 'BUTTON'])
2046
+ const typoQuotes = (text) => text.replace(/"([^"\n]{1,200})"/g, '\u00AB$1\u00BB')
2047
+ const typoDash = (text) => text
2048
+ .replace(/(^|[\s(\[\u00AB])--(?=\s|$)/g, '$1\u2014')
2049
+ .replace(/(^|[\s(\[\u00AB])-(?=\s)/g, '$1\u2014')
2050
+ const TYPO_SHORT = new Set(['в', 'с', 'к', 'о', 'у', 'а', 'и', 'но', 'не', 'ни', 'на', 'по', 'до', 'из', 'за', 'от', 'об'])
2051
+ const typoNbsp = (text) => text.replace(/(^|[\s(\[\u00AB])([а-яё]{1,2})(\s+)/g, (match, lead, word) => (
2052
+ TYPO_SHORT.has(word) ? lead + word + '\u00A0' : match
2053
+ ))
2054
+ const TYPO_YO = [
2055
+ [/еще/g, 'ещё'], [/Еще/g, 'Ещё'], [/ЕЩЕ/g, 'ЕЩЁ'],
2056
+ [/\bее\b/g, 'её'], [/\bЕе\b/g, 'Её'],
2057
+ [/\bчерный\b/g, 'чёрный'], [/\bчерная\b/g, 'чёрная'], [/\bчерные\b/g, 'чёрные'],
2058
+ [/\bзеленый\b/g, 'зелёный'], [/\bжелтый\b/g, 'жёлтый'],
2059
+ [/\bлегкий\b/g, 'лёгкий'], [/\bтяжелый\b/g, 'тяжёлый'],
2060
+ [/\bнадежный\b/g, 'надёжный'], [/\bдешевый\b/g, 'дешёвый'],
2061
+ [/\bидет\b/g, 'идёт'], [/\bдает\b/g, 'даёт'], [/\bберет\b/g, 'берёт'],
2062
+ [/\bведет\b/g, 'ведёт'], [/\bнесет\b/g, 'несёт'], [/\bживет\b/g, 'живёт'],
2063
+ [/\bпривел\b/g, 'привёл'], [/\bшел\b/g, 'шёл']
2064
+ ]
2065
+ const typoYo = (text) => {
2066
+ for (const pair of TYPO_YO) text = text.replace(pair[0], pair[1])
2067
+ return text
2068
+ }
2069
+ const getTypoConf = () => {
2070
+ try {
2071
+ const t = scope.getSnapshot().value && scope.getSnapshot().value.typography
2072
+ if (!t || t.enabled === false) return null
2073
+ return { yo: t.yo === true }
2074
+ } catch (err) { return null }
2075
+ }
2076
+ const typoNode = (node, conf) => {
2077
+ const before = node.nodeValue
2078
+ if (!before || !before.match || (before.match(/[\u0400-\u04FF]/g) || []).length < 3) return
2079
+ if (node.parentElement && node.parentElement.closest('code, pre, a, script, style, textarea, input, select, button, kbd, samp')) return
2080
+ let after = typoQuotes(before)
2081
+ after = typoDash(after)
2082
+ after = typoNbsp(after)
2083
+ if (conf.yo) after = typoYo(after)
2084
+ if (after !== before) node.nodeValue = after
2085
+ }
2086
+ const typoWalk = (root, conf) => {
2087
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
2088
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) typoNode(node, conf)
2089
+ }
2090
+ let typoObserver = null
2091
+ let typoQueued = null
2092
+ const flushTypo = () => {
2093
+ typoQueued = null
2094
+ try {
2095
+ const conf = getTypoConf()
2096
+ if (!conf || !typoPending.size) return
2097
+ for (const root of typoPending) {
2098
+ if (root.nodeType === 3) typoNode(root, conf)
2099
+ else typoWalk(root, conf)
2100
+ }
2101
+ typoPending.clear()
2102
+ } catch (err) { /* ignore */ }
2103
+ }
2104
+ const typoPending = new Set()
2105
+ const queueTypo = (roots) => {
2106
+ for (const r of roots) typoPending.add(r)
2107
+ if (!typoQueued) typoQueued = requestAnimationFrame(flushTypo)
2108
+ }
2109
+ const syncTypo = () => {
2110
+ try {
2111
+ if (typeof document === 'undefined') return
2112
+ if (runtime.getLocale().active !== 'ru' || !getTypoConf()) {
2113
+ if (typoObserver) { typoObserver.disconnect(); typoObserver = null }
2114
+ return
2115
+ }
2116
+ if (typoObserver) return
2117
+ typoWalk(document.body, getTypoConf())
2118
+ typoObserver = new MutationObserver((records) => {
2119
+ const roots = []
2120
+ for (const record of records) roots.push(record.nodeType ? record.target : record)
2121
+ queueTypo(roots)
2122
+ })
2123
+ typoObserver.observe(document.body, { childList: true, characterData: true, subtree: true })
2124
+ } catch (err) { /* ignore */ }
2125
+ }
2126
+ const unsubscribeTypo = runtime.subscribe(syncTypo)
2127
+ ctx.effect(() => {
2128
+ unsubscribeTypo()
2129
+ if (typoObserver) typoObserver.disconnect()
2130
+ }, 'dsh-russian-lang: typography')
2131
+ syncTypo()
1950
2132
  }
1951
2133
 
1952
2134
  module.exports = { apply, inject: ['locale', 'connection', 'remote', 'settingsScope'] }
package/lib/index.js CHANGED
@@ -11,7 +11,11 @@ export const name = '@goodandready/dsh-russian-lang'
11
11
 
12
12
  const RussianLangSettingsSchema = z.object({
13
13
  enabled: z.boolean().required(false),
14
- overrides: z.dict(z.string()).required(false)
14
+ overrides: z.dict(z.string()).required(false),
15
+ typography: z.object({
16
+ enabled: z.boolean().required(false),
17
+ yo: z.boolean().required(false)
18
+ }).required(false)
15
19
  })
16
20
 
17
21
  export function apply(ctx) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-russian-lang",
3
- "version": "0.1.3",
4
- "description": "Russian localization for the DeepSeek Harness web UI: adds Русский as the third option in the native Settings - General - Language menu. Translates 30 core namespaces (722 keys, 100% coverage of the DSH 0.1.1-rc.2 surface) and 9 plugin namespaces (962 keys). The locale runtime snapshot is extended at runtime on unpatched cores; no files in the DSH installation are modified. The choice is persisted through the plugin-owned russian-lang namespace.",
3
+ "version": "0.1.4",
4
+ "description": "Russian localization for the DeepSeek Harness web UI: adds Русский as the third option in the native Settings - General - Language menu. Translates the full core surface (100% coverage of DSH 0.1.1-rc.2) plus 9 community plugin namespaces - 39 namespaces, 1709 strings, with Russian plural forms, count-key plurals and an optional typography pass (guillemets, em dash, nbsp). The locale runtime snapshot is extended at runtime on unpatched cores; no files in the DSH installation are modified. The choice is persisted through the plugin-owned russian-lang namespace.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -126,6 +126,8 @@
126
126
  "rename.workspace.title": "Переименовать рабочую папку",
127
127
  "search.clear": "Очистить поиск",
128
128
  "search.hasMore": "Показаны первые {n} результатов. Уточните запрос.",
129
+ "search.hasMore.one": "Найден один подходящий результат.",
130
+ "search.hasMore.few": "Показаны первые {n} результата. Уточните запрос.",
129
131
  "search.noMatches": "Подходящих сессий нет",
130
132
  "search.pending": "Поиск по истории сессий…",
131
133
  "search.placeholder": "Поиск по сессиям...",
@@ -6,20 +6,29 @@
6
6
  "cd.skills": "Каталог скиллов",
7
7
  "cd.tools": "Схемы инструментов",
8
8
  "cd.mcp": "Инструменты MCP",
9
- "cd.suggestions": "{n} рекомендаций",
10
- "cd.refresh": "Обновить",
11
- "cd.loading": "Проверка…",
12
- "cd.error": "Проверка не удалась",
13
- "cd.empty": "Данных пока нет",
14
- "cd.healthy": "В норме",
15
- "cd.attention": "Стоит посмотреть",
16
- "cd.review": "Стоит посмотреть",
17
- "cd.healthyHint": "Контекст расходуется экономно и укладывается в рекомендованный бюджет.",
18
- "cd.reviewHint": "Некоторые части контекста стоит пересмотреть, пока они не стали дорогими.",
19
- "cd.guideline": "из 50k",
20
- "cd.updated": "Обновлено",
21
- "cd.catalog": "{n} скиллов",
22
- "cd.mcpTools": "{n} инструментов",
9
+ "cd.suggestions": "{n} рекомендаций",
10
+ "cd.suggestions.one": "{n} рекомендация",
11
+ "cd.suggestions.few": "{n} рекомендации",
12
+ "cd.suggestions.many": "{n} рекомендаций",
13
+ "cd.refresh": "Обновить",
14
+ "cd.loading": "Проверка…",
15
+ "cd.error": "Проверка не удалась",
16
+ "cd.empty": "Данных пока нет",
17
+ "cd.healthy": "В норме",
18
+ "cd.attention": "Стоит посмотреть",
19
+ "cd.review": "Стоит посмотреть",
20
+ "cd.healthyHint": "Контекст расходуется экономно и укладывается в рекомендованный бюджет.",
21
+ "cd.reviewHint": "Некоторые части контекста стоит пересмотреть, пока они не стали дорогими.",
22
+ "cd.guideline": "из 50k",
23
+ "cd.updated": "Обновлено",
24
+ "cd.catalog": "{n} скиллов",
25
+ "cd.catalog.one": "{n} скилл",
26
+ "cd.catalog.few": "{n} скилла",
27
+ "cd.catalog.many": "{n} скиллов",
28
+ "cd.mcpTools": "{n} инструментов",
29
+ "cd.mcpTools.one": "{n} инструмент",
30
+ "cd.mcpTools.few": "{n} инструмента",
31
+ "cd.mcpTools.many": "{n} инструментов",
23
32
  "cd.files": "файлов",
24
33
  "cd.toolsCount": "инструментов",
25
34
  "cd.hint": "Открыть аудит контекста"