@goodandready/dsh-russian-lang 0.2.26 → 0.3.1

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.
Files changed (52) hide show
  1. package/README.md +1 -1
  2. package/bin/dsh-i18n.mjs +247 -0
  3. package/lib/client.js +785 -410
  4. package/lib/index.js +69 -13
  5. package/lib/locales/plugins/01-token-dashboard.json +1 -1
  6. package/lib/locales/plugins/10-kanban.json +1 -1
  7. package/lib/locales/plugins/13-agent-teams.json +1 -1
  8. package/lib/locales/plugins/16-usage-stats.json +1 -1
  9. package/lib/locales/plugins/23-moa.json +1 -1
  10. package/lib/locales/plugins/24-personal-directive.json +1 -1
  11. package/lib/locales/plugins/33-messenger-gateway.json +1 -1
  12. package/lib/locales/plugins/35-gitea.json +1 -1
  13. package/lib/locales/plugins/36-cost-meter.json +1 -1
  14. package/lib/locales/plugins/37-time-machine.json +1 -1
  15. package/lib/locales/plugins/38-context-lens.json +1 -1
  16. package/lib/locales/plugins/39-shadow-auditor.json +1 -1
  17. package/lib/locales/plugins/40-clinebot.json +1 -1
  18. package/lib/locales/plugins/41-lanmode.json +1 -1
  19. package/lib/locales/plugins/42-plugin-console.json +1 -1
  20. package/lib/locales/plugins/46-memory-brain.json +1 -1
  21. package/lib/locales/plugins/48-task-tracker.json +1 -1
  22. package/lib/locales/plugins/49-dsml-guard.json +1 -1
  23. package/lib/locales/plugins/49-usage-guard.json +1 -1
  24. package/lib/locales/plugins/50-automode.json +1 -1
  25. package/lib/locales/plugins/54-workshop-market.json +1 -1
  26. package/lib/locales/plugins/56-cron.json +1 -1
  27. package/lib/locales/plugins/57-live-canvas.json +1 -1
  28. package/lib/locales/plugins/58-grok-xsearch.json +1 -1
  29. package/lib/locales/plugins/59-goal.json +1 -1
  30. package/lib/locales/plugins/60-model-sync.json +1 -1
  31. package/lib/locales/plugins/61-issue-reporter.json +1 -1
  32. package/lib/locales/plugins/62-model-search.json +1 -1
  33. package/lib/locales/plugins/63-agent-orchestrator.json +1 -1
  34. package/lib/locales/plugins/64-key-limits.json +1 -1
  35. package/lib/locales/plugins/65-subscriptions.json +1 -1
  36. package/lib/locales/plugins/66-tts.json +1 -1
  37. package/lib/locales/plugins/67-voice.json +1 -1
  38. package/lib/locales/plugins/68-image-gen.json +1 -1
  39. package/lib/locales/plugins/69-plugin-notify.json +1 -1
  40. package/lib/locales/plugins/70-hypotheses.json +1 -1
  41. package/lib/locales/plugins/71-memory-meter.json +1 -1
  42. package/lib/locales/plugins/72-server-monitor.json +1 -70
  43. package/lib/locales/plugins/73-agentrouter.json +1 -1
  44. package/lib/locales/plugins/75-test-pilot.json +1 -0
  45. package/lib/locales/plugins/76-visualize.json +1 -0
  46. package/lib/locales/plugins/77-github-ops.json +1 -0
  47. package/lib/locales/plugins/78-web-gateway.json +1 -0
  48. package/lib/locales/plugins/79-approval-gate.json +1 -0
  49. package/lib/locales.js +50 -12
  50. package/lib/pure.js +251 -13
  51. package/package.json +7 -3
  52. package/lib/locales/plugins/44-goal.json +0 -1
@@ -0,0 +1 @@
1
+ {"@goodandready/dsh-web-gateway":{"allowInternalUrls":"Разрешить внутренние URL для извлечения","allowInternalUrlsDesc":"Опасно: разрешает извлечение адресов, разрешающихся в loopback или приватные сети. Оставляйте выключенным без явной необходимости.","cacheTtlMs":"Время жизни кэша (мс)","cacheTtlMsDesc":"Срок хранения результатов в оперативной памяти. 0 отключает кэширование.","crawl4aiTokenEnv":"Имя учётных данных Crawl4AI","crawl4aiUrl":"Базовый URL Crawl4AI","crawl4aiUrlDesc":"Необязательный Crawl4AI для извлечения страниц. Пустое значение отключает его.","defaultLimit":"Лимит результатов по умолчанию","defaultLimitDesc":"Количество результатов по умолчанию для вызова web_gateway_search.","exaApiKeyEnv":"Имя учётных данных Exa","firecrawlApiKeyEnv":"Имя учётных данных Firecrawl","header_desc":"Резервный веб-поиск (Tavily → Firecrawl → Exa → SearXNG) и извлечение страниц при исчерпании лимитов встроенных web_search / web_fetch.","keysDesc":"Храните секретные значения в Настройки → Учётные данные под этими именами. Не вставляйте сами API-ключи сюда.","maxLimit":"Жёсткий лимит результатов","maxLimitDesc":"Максимальное количество результатов за один поисковый запрос.","save":"Сохранить","saved":"Настройки сохранены","saving":"Сохранение…","searxngUrl":"Базовый URL SearXNG","searxngUrlDesc":"Необязательный локальный SearXNG (например http://127.0.0.1:8080). Пустое значение отключает этот резерв.","settings.loading":"Загрузка настроек Web Gateway…","settings.retry":"Повторить","settings.unavailable":"Область настроек недоступна (пространство имён хоста не готово).","subtitle":"Отказоустойчивая цепочка поиска и извлечения веб-страниц","tavilyApiKeyEnv":"Имя учётных данных Tavily","timeoutMs":"Таймаут провайдера (мс)","timeoutMsDesc":"HTTP-таймаут на одного провайдера в миллисекундах.","title":"Веб-шлюз (Web Gateway)","updater_check":"Проверить обновления","updater_checking":"Проверка…","updater_current":"Текущая","updater_failed":"Сбой проверки обновлений","updater_latest":"Последняя","updater_success":"Обновлено. Перезапустите DSH для загрузки новой версии.","updater_title":"Версия плагина","updater_unavailable":"Автоматическое обновление недоступно для этого профиля.","updater_up_to_date":"Актуальная версия","updater_update_now":"Обновить до {version}","updater_updating":"Обновление…"},"dsh-web-gateway":{"allowInternalUrls":"Разрешить внутренние URL для извлечения","allowInternalUrlsDesc":"Опасно: разрешает извлечение адресов, разрешающихся в loopback или приватные сети. Оставляйте выключенным без явной необходимости.","cacheTtlMs":"Время жизни кэша (мс)","cacheTtlMsDesc":"Срок хранения результатов в оперативной памяти. 0 отключает кэширование.","crawl4aiTokenEnv":"Имя учётных данных Crawl4AI","crawl4aiUrl":"Базовый URL Crawl4AI","crawl4aiUrlDesc":"Необязательный Crawl4AI для извлечения страниц. Пустое значение отключает его.","defaultLimit":"Лимит результатов по умолчанию","defaultLimitDesc":"Количество результатов по умолчанию для вызова web_gateway_search.","exaApiKeyEnv":"Имя учётных данных Exa","firecrawlApiKeyEnv":"Имя учётных данных Firecrawl","header_desc":"Резервный веб-поиск (Tavily → Firecrawl → Exa → SearXNG) и извлечение страниц при исчерпании лимитов встроенных web_search / web_fetch.","keysDesc":"Храните секретные значения в Настройки → Учётные данные под этими именами. Не вставляйте сами API-ключи сюда.","maxLimit":"Жёсткий лимит результатов","maxLimitDesc":"Максимальное количество результатов за один поисковый запрос.","save":"Сохранить","saved":"Настройки сохранены","saving":"Сохранение…","searxngUrl":"Базовый URL SearXNG","searxngUrlDesc":"Необязательный локальный SearXNG (например http://127.0.0.1:8080). Пустое значение отключает этот резерв.","settings.loading":"Загрузка настроек Web Gateway…","settings.retry":"Повторить","settings.unavailable":"Область настроек недоступна (пространство имён хоста не готово).","subtitle":"Отказоустойчивая цепочка поиска и извлечения веб-страниц","tavilyApiKeyEnv":"Имя учётных данных Tavily","timeoutMs":"Таймаут провайдера (мс)","timeoutMsDesc":"HTTP-таймаут на одного провайдера в миллисекундах.","title":"Веб-шлюз (Web Gateway)","updater_check":"Проверить обновления","updater_checking":"Проверка…","updater_current":"Текущая","updater_failed":"Сбой проверки обновлений","updater_latest":"Последняя","updater_success":"Обновлено. Перезапустите DSH для загрузки новой версии.","updater_title":"Версия плагина","updater_unavailable":"Автоматическое обновление недоступно для этого профиля.","updater_up_to_date":"Актуальная версия","updater_update_now":"Обновить до {version}","updater_updating":"Обновление…"}}
@@ -0,0 +1 @@
1
+ {"@goodandready/dsh-approval-gate":{"approvalRequired":"Требуется подтверждение","askCredentialInArgv":"Раскрытые данные авторизации передаются как аргументы процесса; используйте безопасный хелпер или проверьте перед разрешением","askDynamicArguments":"Раскрытые аргументы чувствительной команды не могут быть проверены; проверьте перед разрешением","askDynamicCommand":"Раскрытое имя команды не может быть проверено; проверьте перед разрешением","askDynamicRedirect":"Раскрытая цель перенаправления не может быть проверена; проверьте перед разрешением","askInterpreterPayload":"Побочные эффекты интерпретатора не проанализированы полностью; проверьте перед разрешением","askScriptFile":"Содержимое файла скрипта недоступно для проверки; проверьте перед разрешением","askShellStdin":"Ввод shell не полностью доступен для проверки; проверьте перед разрешением","askUnknown":"Команда не может быть полностью проверена; проверьте перед разрешением","askUnknownFormat":"Формат вызова инструмента не может быть проверен; проверьте перед разрешением","askUnknownWriteTarget":"Целевой файл для записи не определён; проверьте перед разрешением","askUnparseable":"Синтаксис shell не может быть полностью проверен; проверьте перед разрешением","blockedPrefix":"Заблокировано правилом dsh-approval-gate","commandLabel":"Команда","detailDanglingEscape":"незавершённое экранирование","detailInvalidHeredoc":"недопустимый разделитель here-document","detailProcessSubstitution":"подстановка процесса требует подтверждения","detailUnclosedBacktick":"незакрытая подстановка в обратных кавычках","detailUnclosedCommandSubstitution":"незакрытая подстановка команды","detailUnclosedDoubleQuote":"незакрытая двойная кавычка","detailUnclosedParameterExpansion":"незакрытое раскрытие параметров","detailUnclosedProcessSubstitution":"незакрытая подстановка процесса","detailUnclosedSingleQuote":"незакрытая одинарная кавычка","detailUnsupported":"неподдерживаемый синтаксис shell","detailUnsupportedGrouping":"синтаксис группировки shell не поддерживается анализатором","detailUnterminatedHeredoc":"терминатор here-document не найден","detectedLabel":"Обнаружено","parseReasonLabel":"Причина разбора","ruleCurlPipe":"Передача загруженного содержимого в конвейер shell","ruleDestructiveSql":"Разрушающий SQL-запрос","ruleDeviceWrite":"Запись в устройство или защищённый файл","ruleEval":"Динамическое выполнение shell (eval)","ruleFindDelete":"find -delete","ruleGitClean":"Разрушающий git clean","ruleGitResetHard":"Разрушающий git reset --hard","ruleInterpreterPayload":"Небезопасный код интерпретатора","ruleMkfsDevice":"Форматирование файловой системы","ruleNestedShell":"Превышен лимит глубины вложенного shell","ruleProcessSignal":"Завершение процесса","ruleRecursiveRm":"Рекурсивное удаление файлов","ruleSecretWrite":"Запись в защищённый файл с секретами","ruleServiceControl":"Изменение состояния службы","ruleUnknown":"Сработало блокирующее правило безопасности"},"dsh-approval-gate":{"approvalRequired":"Требуется подтверждение","askCredentialInArgv":"Раскрытые данные авторизации передаются как аргументы процесса; используйте безопасный хелпер или проверьте перед разрешением","askDynamicArguments":"Раскрытые аргументы чувствительной команды не могут быть проверены; проверьте перед разрешением","askDynamicCommand":"Раскрытое имя команды не может быть проверено; проверьте перед разрешением","askDynamicRedirect":"Раскрытая цель перенаправления не может быть проверена; проверьте перед разрешением","askInterpreterPayload":"Побочные эффекты интерпретатора не проанализированы полностью; проверьте перед разрешением","askScriptFile":"Содержимое файла скрипта недоступно для проверки; проверьте перед разрешением","askShellStdin":"Ввод shell не полностью доступен для проверки; проверьте перед разрешением","askUnknown":"Команда не может быть полностью проверена; проверьте перед разрешением","askUnknownFormat":"Формат вызова инструмента не может быть проверен; проверьте перед разрешением","askUnknownWriteTarget":"Целевой файл для записи не определён; проверьте перед разрешением","askUnparseable":"Синтаксис shell не может быть полностью проверен; проверьте перед разрешением","blockedPrefix":"Заблокировано правилом dsh-approval-gate","commandLabel":"Команда","detailDanglingEscape":"незавершённое экранирование","detailInvalidHeredoc":"недопустимый разделитель here-document","detailProcessSubstitution":"подстановка процесса требует подтверждения","detailUnclosedBacktick":"незакрытая подстановка в обратных кавычках","detailUnclosedCommandSubstitution":"незакрытая подстановка команды","detailUnclosedDoubleQuote":"незакрытая двойная кавычка","detailUnclosedParameterExpansion":"незакрытое раскрытие параметров","detailUnclosedProcessSubstitution":"незакрытая подстановка процесса","detailUnclosedSingleQuote":"незакрытая одинарная кавычка","detailUnsupported":"неподдерживаемый синтаксис shell","detailUnsupportedGrouping":"синтаксис группировки shell не поддерживается анализатором","detailUnterminatedHeredoc":"терминатор here-document не найден","detectedLabel":"Обнаружено","parseReasonLabel":"Причина разбора","ruleCurlPipe":"Передача загруженного содержимого в конвейер shell","ruleDestructiveSql":"Разрушающий SQL-запрос","ruleDeviceWrite":"Запись в устройство или защищённый файл","ruleEval":"Динамическое выполнение shell (eval)","ruleFindDelete":"find -delete","ruleGitClean":"Разрушающий git clean","ruleGitResetHard":"Разрушающий git reset --hard","ruleInterpreterPayload":"Небезопасный код интерпретатора","ruleMkfsDevice":"Форматирование файловой системы","ruleNestedShell":"Превышен лимит глубины вложенного shell","ruleProcessSignal":"Завершение процесса","ruleRecursiveRm":"Рекурсивное удаление файлов","ruleSecretWrite":"Запись в защищённый файл с секретами","ruleServiceControl":"Изменение состояния службы","ruleUnknown":"Сработало блокирующее правило безопасности"}}
package/lib/locales.js CHANGED
@@ -7,6 +7,29 @@ const CORE_FILE = join(__dirname, "locales", "core.json")
7
7
  const PLUGINS_DIR = join(__dirname, "locales", "plugins")
8
8
  const ZH_RU_FILE = join(__dirname, "locales", "zh-ru.json")
9
9
 
10
+ let _pluginCache = null
11
+ let _pluginIndex = null
12
+
13
+ function ensurePluginCache() {
14
+ if (_pluginCache && _pluginIndex) return
15
+ _pluginCache = {}
16
+ _pluginIndex = new Map()
17
+ if (existsSync(PLUGINS_DIR)) {
18
+ for (const file of readdirSync(PLUGINS_DIR).sort()) {
19
+ if (!file.endsWith(".json")) continue
20
+ try {
21
+ const data = JSON.parse(readFileSync(join(PLUGINS_DIR, file), "utf8"))
22
+ for (const [ns, entries] of Object.entries(data)) {
23
+ _pluginCache[ns] = Object.assign(_pluginCache[ns] || {}, entries)
24
+ _pluginIndex.set(ns.toLowerCase(), ns)
25
+ const baseName = ns.replace(/^@[^/]+\//, "")
26
+ _pluginIndex.set(baseName.toLowerCase(), ns)
27
+ }
28
+ } catch (_) { /* bestEffort */ void _; }
29
+ }
30
+ }
31
+ }
32
+
10
33
  export function getCoreDictionaries() {
11
34
  if (existsSync(CORE_FILE)) {
12
35
  try {
@@ -17,19 +40,34 @@ export function getCoreDictionaries() {
17
40
  }
18
41
 
19
42
  export function getPluginDictionaries() {
20
- const merged = {}
21
- if (existsSync(PLUGINS_DIR)) {
22
- for (const file of readdirSync(PLUGINS_DIR).sort()) {
23
- if (!file.endsWith(".json")) continue
24
- try {
25
- const data = JSON.parse(readFileSync(join(PLUGINS_DIR, file), "utf8"))
26
- for (const [ns, entries] of Object.entries(data)) {
27
- merged[ns] = Object.assign(merged[ns] || {}, entries)
28
- }
29
- } catch (_) { /* bestEffort */ void _; }
43
+ ensurePluginCache()
44
+ return Object.assign(
45
+ {},
46
+ _pluginCache
47
+ )
48
+ }
49
+
50
+ export function getPluginDictionariesByNames(names) {
51
+ ensurePluginCache()
52
+ if (!names || (Array.isArray(names) && names.length === 0)) {
53
+ return Object.assign(
54
+ {},
55
+ _pluginCache
56
+ )
57
+ }
58
+ const nameList = Array.isArray(names) ? names : String(names).split(",")
59
+ const result = {}
60
+ for (const rawName of nameList) {
61
+ const trimmed = String(rawName).trim()
62
+ if (!trimmed) continue
63
+ const targetNs = _pluginIndex.get(trimmed.toLowerCase())
64
+ if (targetNs && _pluginCache[targetNs]) {
65
+ result[targetNs] = _pluginCache[targetNs]
66
+ } else if (_pluginCache[trimmed]) {
67
+ result[trimmed] = _pluginCache[trimmed]
30
68
  }
31
69
  }
32
- return merged
70
+ return result
33
71
  }
34
72
 
35
73
  export function getAllDictionaries() {
@@ -41,7 +79,7 @@ export function getAllDictionaries() {
41
79
  export function getZhRuMap() {
42
80
  if (existsSync(ZH_RU_FILE)) {
43
81
  try {
44
- return JSON.parse(readFileSync(ZH_RU_FILE, "utf8"))
82
+ return JSON.parse(readFileSync(ZG_RU_FILE, "utf8"))
45
83
  } catch (_) { /* bestEffort */ void _; }
46
84
  }
47
85
  return {}
package/lib/pure.js CHANGED
@@ -17,7 +17,50 @@
17
17
  // ---------------------------------------------------------------- форматы
18
18
 
19
19
  const numberFormat = new Intl.NumberFormat('ru-RU')
20
+ const compactNumberFormat = new Intl.NumberFormat('ru-RU', { notation: 'compact', compactDisplay: 'short' })
20
21
  const relativeTimeFormat = new Intl.RelativeTimeFormat('ru-RU', { numeric: 'auto' })
22
+
23
+ const datePresetFormats = {
24
+ short: new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' }),
25
+ shortDateTime: new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }),
26
+ medium: new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }),
27
+ long: new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }),
28
+ time: new Intl.DateTimeFormat('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
29
+ }
30
+
31
+ export const formatDate = (val, style = 'short') => {
32
+ if (val === null || val === undefined || val === '') return ''
33
+ const d = val instanceof Date ? val : new Date(typeof val === 'number' && val < 1e12 ? val * 1000 : val)
34
+ if (isNaN(d.getTime())) return String(val)
35
+ if (typeof style === 'string' && datePresetFormats[style]) {
36
+ return datePresetFormats[style].format(d)
37
+ }
38
+ if (typeof style === 'object' && style !== null) {
39
+ try {
40
+ return new Intl.DateTimeFormat('ru-RU', style).format(d)
41
+ } catch (_) {
42
+ return datePresetFormats.short.format(d)
43
+ }
44
+ }
45
+ return datePresetFormats.short.format(d)
46
+ }
47
+
48
+ export const formatCompactNumber = (val) => {
49
+ if (val === null || val === undefined || val === '') return ''
50
+ const n = typeof val === 'number' ? val : Number(val)
51
+ return isNaN(n) ? String(val) : compactNumberFormat.format(n)
52
+ }
53
+
54
+ export const formatTokens = (count, compact = false) => {
55
+ if (count === null || count === undefined || count === '') return ''
56
+ const n = typeof count === 'number' ? count : Number(count)
57
+ if (isNaN(n)) return String(count)
58
+ const tokenWord = plural(n, ['токен', 'токена', 'токенов'])
59
+ if (compact && n >= 1000) {
60
+ return `${compactNumberFormat.format(n)} токенов`
61
+ }
62
+ return `${numberFormat.format(n)} ${tokenWord}`
63
+ }
21
64
  const currencyFormats = new Map()
22
65
 
23
66
  const getCurrencyFormat = (cur) => {
@@ -119,24 +162,64 @@ export const inflect = (phrase, cName) => {
119
162
 
120
163
  // ------------------------------------------------- подстановка и плюрализация
121
164
 
165
+ const pluralRules = new Intl.PluralRules('ru-RU')
166
+
167
+ /** Русская форма числительного: one | few | many | other. */
168
+ export const pluralForm = (n) => pluralRules.select(n)
169
+
170
+ /**
171
+ * Универсальная русская плюрализация 1, 2-4, 5+
172
+ * @param {number|string} count - Число элементов
173
+ * @param {string[]|string} forms - Массив [one, few, many] или строка первого варианта
174
+ * @param {boolean} [withCount=false] - Включать ли число в результат ("5 задач" vs "задач")
175
+ */
176
+ export function plural(count, forms, withCount = false) {
177
+ const n = typeof count === 'number' ? count : (Number(count) || 0)
178
+ let one = '', few = '', many = ''
179
+ if (Array.isArray(forms)) {
180
+ one = forms[0] || ''
181
+ few = forms[1] || one
182
+ many = forms[2] || few || one
183
+ } else if (typeof forms === 'string') {
184
+ one = forms
185
+ few = arguments[2] || one
186
+ many = arguments[3] || few || one
187
+ withCount = arguments[4] || false
188
+ } else {
189
+ return ''
190
+ }
191
+ const form = pluralRules.select(n)
192
+ let word = many
193
+ if (form === 'one') word = one
194
+ else if (form === 'few') word = few
195
+ else if (form === 'many') word = many
196
+
197
+ return withCount ? `${formatNumber(n)} ${word}` : word
198
+ }
199
+
122
200
  export const fill = (template, params) => {
123
201
  if (!params || typeof params !== 'object') return String(template)
124
- return String(template).replace(/\{(\w+)(?::(\w+))?\}/g, (match, name, spec) => {
202
+ return String(template).replace(/\{(\w+)(?::([^}]+))?\}/g, (match, name, spec) => {
125
203
  if (!(name in params)) return match
126
204
  const val = params[name]
205
+ if (!spec) return String(val)
127
206
  if (spec === 'number') return formatNumber(val)
207
+ if (spec === 'compact') return formatCompactNumber(val)
208
+ if (spec === 'tokens') return formatTokens(val)
209
+ if (spec === 'tokens:compact') return formatTokens(val, true)
128
210
  if (spec === 'reltime') return formatRelativeTime(val)
129
211
  if (spec === 'currency') return formatCurrency(val, params.currency || 'RUB')
212
+ if (spec === 'date') return formatDate(val)
213
+ if (spec.startsWith('date:')) return formatDate(val, spec.slice(5))
214
+ if (spec.startsWith('plural:')) {
215
+ const parts = spec.slice(7).split(',')
216
+ return plural(val, parts)
217
+ }
130
218
  if (INFLECT_CASES.has(spec)) return inflect(String(val), spec)
131
219
  return String(val)
132
220
  })
133
221
  }
134
222
 
135
- const pluralRules = new Intl.PluralRules('ru-RU')
136
-
137
- /** Русская форма числительного: one | few | many | other. */
138
- export const pluralForm = (n) => pluralRules.select(n)
139
-
140
223
  // ------------------------------------------------------ поисковая морфология
141
224
 
142
225
  export const stemRussian = (word) => {
@@ -197,7 +280,13 @@ export const ERROR_MAP = {
197
280
  500: { title: 'Внутренняя ошибка сервера', message: 'На стороне сервера произошел сбой', hint: 'Попробуйте повторить запрос позже или проверьте серверные логи.' },
198
281
  502: { title: 'Ошибочный шлюз (Bad Gateway)', message: 'Промежуточный прокси не получил корректный ответ', hint: 'Проверьте работу нижележащей службы или upstream-сервера.' },
199
282
  503: { title: 'Служба временно недоступна', message: 'Сервер перегружен или находится на обслуживании', hint: 'Попробуйте повторить операцию через некоторое время.' },
200
- 504: { title: 'Шлюз не отвечает (Gateway Timeout)', message: 'Превышено время ожидания ответа от upstream-сервера', hint: 'Попробуйте повторить запрос позже.' }
283
+ 504: { title: 'Шлюз не отвечает (Gateway Timeout)', message: 'Превышено время ожидания ответа от upstream-сервера', hint: 'Попробуйте повторить запрос позже.' },
284
+ FETCH_FAILED: { title: 'Сетевой запрос не удался', message: 'Не удалось связаться с сервером (Failed to fetch)', hint: 'Проверьте сетевое подключение и доступность адреса.' },
285
+ CORS_ERROR: { title: 'Ошибка CORS', message: 'Запрос заблокирован политикой безопасности браузера', hint: 'Проверьте заголовки разрешённых источников на сервере.' },
286
+ WS_CLOSED: { title: 'Разрыв WebSocket', message: 'Связь с сервером по WebSocket прервана', hint: 'Проверьте работу сервера и сетевой статус.' },
287
+ QUOTA_EXCEEDED: { title: 'Исчерпана квота API', message: 'Превышен лимит запросов или баланс провайдера', hint: 'Проверьте баланс API-ключа в кабинете провайдера.' },
288
+ CONTEXT_OVERFLOW: { title: 'Превышен контекст', message: 'Объём диалога превышает размер контекстного окна', hint: 'Очистите или сожмите историю сообщений сессии.' },
289
+ MODEL_NOT_FOUND: { title: 'Модель не найдена', message: 'Указанная модель отсутствует или недоступна', hint: 'Проверьте идентификатор модели в настройках.' }
201
290
  }
202
291
 
203
292
  const fromMap = (key, rawMsg) => ({
@@ -213,9 +302,25 @@ export const humanizeError = (err) => {
213
302
  if (lookupKey && ERROR_MAP[lookupKey]) return fromMap(lookupKey, rawMsg)
214
303
  if (/rate limit|too many requests/i.test(rawMsg)) return fromMap(429, rawMsg)
215
304
  if (/unauthorized|invalid token|invalid api key/i.test(rawMsg)) return fromMap(401, rawMsg)
305
+ if (/failed to fetch|network\s*error|load\s*failed|net::err_/i.test(rawMsg)) return fromMap('FETCH_FAILED', rawMsg)
306
+ if (/cors|cross-origin/i.test(rawMsg)) return fromMap('CORS_ERROR', rawMsg)
307
+ if (/websocket.*(?:closed|failed)/i.test(rawMsg)) return fromMap('WS_CLOSED', rawMsg)
308
+ if (/insufficient_quota|quota\s*exceeded/i.test(rawMsg)) return fromMap('QUOTA_EXCEEDED', rawMsg)
309
+ if (/context.*(?:exceeded|overflow|length)/i.test(rawMsg)) return fromMap('CONTEXT_OVERFLOW', rawMsg)
310
+ if (/model.*not.*found|model.*does not exist/i.test(rawMsg)) return fromMap('MODEL_NOT_FOUND', rawMsg)
216
311
  return { code: 'UNKNOWN', title: 'Ошибка операции', message: rawMsg, hint: 'Проверьте параметры операции и логи.', raw: rawMsg }
217
312
  }
218
313
 
314
+ export const formatErrorToast = (err) => {
315
+ const h = humanizeError(err)
316
+ if (!h) return null
317
+ return {
318
+ type: 'error',
319
+ title: h.title,
320
+ description: h.message + (h.hint ? ' ' + h.hint : '')
321
+ }
322
+ }
323
+
219
324
  // -------------------------------------------------------- отчёт об ошибке
220
325
 
221
326
  /** Версию передаёт вызывающий: в бандл её подставляет build.py из package.json,
@@ -486,20 +591,38 @@ export const SYSTEM_PROMPT_PRESETS = {
486
591
  technical_expert: {
487
592
  id: 'technical_expert',
488
593
  label: 'Технический эксперт',
489
- desc: 'Строгая русская инженерная терминология, чистый код и русские комментарии',
490
- text: 'Отвечай пользователю на русском языке. Используй точную русскую инженерную терминологию, пиши чистый, идиоматичный код с русскими комментариями и следуй лучшим практикам разработки. Если пользователь пишет на другом языке, отвечай на его языке.'
594
+ desc: 'Точная терминология, чистый код и русские комментарии',
595
+ text: 'Отвечай пользователю на русском языке. Используй точную инженерную терминологию, пиши чистый код и русские комментарии.'
491
596
  },
492
597
  tech_writer: {
493
598
  id: 'tech_writer',
494
599
  label: 'Технический писатель',
495
- desc: 'Структурированные тексты, Markdown, ГОСТ/RFC и выверенная типографика',
496
- text: 'Отвечай пользователю на русском языке. Оформляй документацию и пояснения в структурированном виде (Markdown, списки, таблицы), строго соблюдай правила русской типографики («кавычки-ёлочки», длинное тире, буква «ё») и требования к техническим текстам.'
600
+ desc: 'Структурированные тексты, Markdown и выверенная типографика',
601
+ text: 'Отвечай пользователю на русском языке. Оформляй документацию в Markdown, строго соблюдай правила русской типографики («ёлочки», тире, «ё»).'
497
602
  },
498
603
  concise: {
499
604
  id: 'concise',
500
605
  label: 'Лаконичный режим',
501
- desc: 'Краткие ёмкие ответы без лишней вводной воды и повторов',
502
- text: 'Отвечай пользователю на русском языке максимально кратко и ёмко. Без лишних вводных слов, повторов и пространных рассуждений. Сразу переходи к сути, коду или прямому решению задачи.'
606
+ desc: 'Краткие ёмкие ответы без вводных слов и воды',
607
+ text: 'Отвечай пользователю на русском языке максимально кратко и ёмко. Без вводных слов и воды, сразу код или решение.'
608
+ },
609
+ code_reviewer: {
610
+ id: 'code_reviewer',
611
+ label: 'Код-ревьюер',
612
+ desc: 'Поиск багов, безопасность и архитектурный аудит',
613
+ text: 'Анализируй код и отвечай на русском языке. Ищи уязвимости, граничные случаи и нарушения контрактов, предлагая исправления.'
614
+ },
615
+ architect: {
616
+ id: 'architect',
617
+ label: 'Системный архитектор',
618
+ desc: 'Контракты API/CLI, модульность и надёжность',
619
+ text: 'Отвечай на русском языке как архитектор. Проектируй модульные системы, чёткие контракты API/CLI и надёжные решения без лишней сложности.'
620
+ },
621
+ tutor: {
622
+ id: 'tutor',
623
+ label: 'Наставник (ментор)',
624
+ desc: 'Понятные объяснения сложных концепций и пошаговый разбор',
625
+ text: 'Отвечай на русском языке как наставник. Объясняй сложные темы простыми словами, наглядными аналогиями и пошаговыми примерами.'
503
626
  }
504
627
  }
505
628
 
@@ -548,3 +671,118 @@ export const exportSessionToMarkdown = (session, options = {}) => {
548
671
  md += `*Сгенерировано с помощью [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) и @goodandready/dsh-russian-lang*\n`
549
672
  return md
550
673
  }
674
+
675
+ // -------------------------------------------------- инспектор переводов (#298)
676
+
677
+ export const findTranslationKey = (text, dicts = {}, overrides = {}, zhRu = {}) => {
678
+ if (!text || typeof text !== 'string') return null
679
+ const trimmed = text.trim()
680
+ if (!trimmed) return null
681
+
682
+ if (overrides && typeof overrides === 'object') {
683
+ for (const [k, v] of Object.entries(overrides)) {
684
+ if (v === text || (typeof v === 'string' && v.trim() === trimmed)) {
685
+ return { key: k, value: v, source: 'override' }
686
+ }
687
+ }
688
+ }
689
+
690
+ if (dicts && typeof dicts === 'object') {
691
+ for (const [ns, map] of Object.entries(dicts)) {
692
+ if (!map || typeof map !== 'object') continue
693
+ for (const [k, v] of Object.entries(map)) {
694
+ if (v === text || (typeof v === 'string' && v.trim() === trimmed)) {
695
+ return { ns, key: k, value: v, source: 'dictionary' }
696
+ }
697
+ }
698
+ }
699
+ }
700
+
701
+ if (zhRu && typeof zhRu === 'object') {
702
+ for (const [zh, ru] of Object.entries(zhRu)) {
703
+ if (ru === text || (typeof ru === 'string' && ru.trim() === trimmed)) {
704
+ return { zh, key: zh, value: ru, source: 'dom_zh' }
705
+ }
706
+ }
707
+ }
708
+
709
+ return null
710
+ }
711
+
712
+ // ------------------------------------------ двуязычный поиск команд (#300)
713
+
714
+ export const bilingualCommandMatch = (query, command, options = {}) => {
715
+ if (!query || !command) return { matched: false, score: 0 }
716
+ const q = String(query).trim().toLowerCase()
717
+ if (!q) return { matched: false, score: 0 }
718
+
719
+ const cmdObj = typeof command === 'string' ? { title: command } : command
720
+ const candidates = [
721
+ cmdObj.title,
722
+ cmdObj.label,
723
+ cmdObj.name,
724
+ cmdObj.originalTitle,
725
+ cmdObj.enTitle,
726
+ cmdObj.id,
727
+ cmdObj.description,
728
+ cmdObj.category,
729
+ ...(Array.isArray(cmdObj.keywords) ? cmdObj.keywords : [])
730
+ ].filter((c) => typeof c === 'string' && c.trim())
731
+
732
+ if (!candidates.length) return { matched: false, score: 0 }
733
+
734
+ const queryVariants = [{ text: q, kind: 'direct' }]
735
+
736
+ const lat2cyr = translit(q, LAYOUT_LAT_TO_CYR)
737
+ if (lat2cyr !== q) queryVariants.push({ text: lat2cyr, kind: 'layout' })
738
+
739
+ const cyr2lat = translit(q, LAYOUT_CYR_TO_LAT)
740
+ if (cyr2lat !== q) queryVariants.push({ text: cyr2lat, kind: 'layout' })
741
+
742
+ const phonLat2Cyr = phoneticTranslit(q, 'lat2cyr')
743
+ if (phonLat2Cyr && phonLat2Cyr !== q && phonLat2Cyr !== lat2cyr) {
744
+ queryVariants.push({ text: phonLat2Cyr, kind: 'phonetic' })
745
+ }
746
+
747
+ const phonCyr2Lat = phoneticTranslit(q, 'cyr2lat')
748
+ if (phonCyr2Lat && phonCyr2Lat !== q && phonCyr2Lat !== cyr2lat) {
749
+ queryVariants.push({ text: phonCyr2Lat, kind: 'phonetic' })
750
+ }
751
+
752
+ let bestScore = 0
753
+ let bestVariant = null
754
+ let bestCandidate = null
755
+
756
+ for (const variant of queryVariants) {
757
+ for (const cand of candidates) {
758
+ const score = fuzzyMatchRu(variant.text, cand)
759
+ if (score > bestScore) {
760
+ bestScore = score
761
+ bestVariant = variant
762
+ bestCandidate = cand
763
+ }
764
+ }
765
+ }
766
+
767
+ const threshold = options.threshold ?? 50
768
+ return {
769
+ matched: bestScore >= threshold,
770
+ score: bestScore,
771
+ matchedText: bestCandidate,
772
+ variant: bestVariant ? bestVariant.text : q,
773
+ kind: bestVariant ? bestVariant.kind : 'none'
774
+ }
775
+ }
776
+
777
+ export const filterCommands = (query, commands = [], options = {}) => {
778
+ if (!Array.isArray(commands)) return []
779
+ if (!query || !String(query).trim()) return commands
780
+ const results = []
781
+ for (const cmd of commands) {
782
+ const res = bilingualCommandMatch(query, cmd, options)
783
+ if (res.matched) {
784
+ results.push({ item: cmd, ...res })
785
+ }
786
+ }
787
+ return results.sort((a, b) => b.score - a.score).map((r) => r.item)
788
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-russian-lang",
3
- "version": "0.2.26",
3
+ "version": "0.3.1",
4
4
  "description": "Russian localization for DeepSeek Harness (DSH v0.1.6-alpha.1+) web UI: adds Русский as native language option. 100.0% coverage across 11,295 keys (127 namespaces). Features full DSH v0.1.6-alpha.1 core support (terminal, archive, command palette, Auto review, trajectory, PTC/Creator presets) and complete GoodAndReady plugin catalog (subscriptions, tts, image-gen, voice, key-limits, notify, commandcode, plugin-console).",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "files": [
14
14
  "lib",
15
+ "bin",
15
16
  "cordis.patch.yml",
16
17
  "README.md",
17
18
  "README.ru.md",
@@ -43,7 +44,7 @@
43
44
  },
44
45
  "scripts": {
45
46
  "build": "python3 build.py",
46
- "check": "python3 tools/lint_translations.py && python3 check-coverage.py && python3 tools/term_check.py --ci && python3 tools/check_pack_size.py",
47
+ "check": "python3 build.py --check && python3 tools/lint_translations.py && python3 check-coverage.py && python3 tools/term_check.py --ci && python3 tools/check_pack_size.py && python3 tools/check_data_integrity.py",
47
48
  "test": "node --test test/*.mjs && node test/_repro.cjs && python3 test/test_tools.py && python3 test/test_extract.py && python3 test/test_overflow.py",
48
49
  "smoke": "python3 test/smoke.py",
49
50
  "lint": "python3 tools/lint_translations.py"
@@ -65,5 +66,8 @@
65
66
  "homepage": "https://github.com/GooDAnDReaDY/dsh-russian-lang#readme",
66
67
  "bugs": {
67
68
  "url": "https://github.com/GooDAnDReaDY/dsh-russian-lang/issues"
69
+ },
70
+ "bin": {
71
+ "dsh-i18n": "./bin/dsh-i18n.mjs"
68
72
  }
69
- }
73
+ }
@@ -1 +0,0 @@
1
- {"goal":{"checkUpdate":"Проверить","checking":"Проверка...","upToDate":"Установлена последняя версия","updateAvailable":"Доступно обновление","updateBtn":"Обновить","updateDone":"Обновление завершено! Перезапустите harness для применения.","updateFailed":"Ошибка обновления:","updating":"Обновление...","version":"Версия"}}