@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.
- package/README.md +1 -1
- package/bin/dsh-i18n.mjs +247 -0
- package/lib/client.js +785 -410
- package/lib/index.js +69 -13
- package/lib/locales/plugins/01-token-dashboard.json +1 -1
- package/lib/locales/plugins/10-kanban.json +1 -1
- package/lib/locales/plugins/13-agent-teams.json +1 -1
- package/lib/locales/plugins/16-usage-stats.json +1 -1
- package/lib/locales/plugins/23-moa.json +1 -1
- package/lib/locales/plugins/24-personal-directive.json +1 -1
- package/lib/locales/plugins/33-messenger-gateway.json +1 -1
- package/lib/locales/plugins/35-gitea.json +1 -1
- package/lib/locales/plugins/36-cost-meter.json +1 -1
- package/lib/locales/plugins/37-time-machine.json +1 -1
- package/lib/locales/plugins/38-context-lens.json +1 -1
- package/lib/locales/plugins/39-shadow-auditor.json +1 -1
- package/lib/locales/plugins/40-clinebot.json +1 -1
- package/lib/locales/plugins/41-lanmode.json +1 -1
- package/lib/locales/plugins/42-plugin-console.json +1 -1
- package/lib/locales/plugins/46-memory-brain.json +1 -1
- package/lib/locales/plugins/48-task-tracker.json +1 -1
- package/lib/locales/plugins/49-dsml-guard.json +1 -1
- package/lib/locales/plugins/49-usage-guard.json +1 -1
- package/lib/locales/plugins/50-automode.json +1 -1
- package/lib/locales/plugins/54-workshop-market.json +1 -1
- package/lib/locales/plugins/56-cron.json +1 -1
- package/lib/locales/plugins/57-live-canvas.json +1 -1
- package/lib/locales/plugins/58-grok-xsearch.json +1 -1
- package/lib/locales/plugins/59-goal.json +1 -1
- package/lib/locales/plugins/60-model-sync.json +1 -1
- package/lib/locales/plugins/61-issue-reporter.json +1 -1
- package/lib/locales/plugins/62-model-search.json +1 -1
- package/lib/locales/plugins/63-agent-orchestrator.json +1 -1
- package/lib/locales/plugins/64-key-limits.json +1 -1
- package/lib/locales/plugins/65-subscriptions.json +1 -1
- package/lib/locales/plugins/66-tts.json +1 -1
- package/lib/locales/plugins/67-voice.json +1 -1
- package/lib/locales/plugins/68-image-gen.json +1 -1
- package/lib/locales/plugins/69-plugin-notify.json +1 -1
- package/lib/locales/plugins/70-hypotheses.json +1 -1
- package/lib/locales/plugins/71-memory-meter.json +1 -1
- package/lib/locales/plugins/72-server-monitor.json +1 -70
- package/lib/locales/plugins/73-agentrouter.json +1 -1
- package/lib/locales/plugins/75-test-pilot.json +1 -0
- package/lib/locales/plugins/76-visualize.json +1 -0
- package/lib/locales/plugins/77-github-ops.json +1 -0
- package/lib/locales/plugins/78-web-gateway.json +1 -0
- package/lib/locales/plugins/79-approval-gate.json +1 -0
- package/lib/locales.js +50 -12
- package/lib/pure.js +251 -13
- package/package.json +7 -3
- 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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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(
|
|
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+)(?::(
|
|
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
|
|
496
|
-
text: 'Отвечай пользователю на русском языке. Оформляй документацию
|
|
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.
|
|
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":"Версия"}}
|