@goodandready/dsh-russian-lang 0.3.8 → 0.3.10

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
@@ -15,8 +15,7 @@ if (typeof style === 'string' && datePresetFormats[style]) {return datePresetFor
15
15
  }
16
16
  if (typeof style === 'object' && style !== null) {try {return new Intl.DateTimeFormat('ru-RU', style).format(d)
17
17
  } catch (_) {return datePresetFormats.short.format(d)
18
- }
19
- }
18
+ }}
20
19
  return datePresetFormats.short.format(d)
21
20
  }
22
21
  const formatCompactNumber = (val) => {if (val === null || val === undefined || val === '') return ''
@@ -35,8 +34,7 @@ const currencyFormats = new Map()
35
34
  const getCurrencyFormat = (cur) => {const c = (cur || 'RUB').toUpperCase()
36
35
  if (!currencyFormats.has(c)) {try {currencyFormats.set(c, new Intl.NumberFormat('ru-RU', { style: 'currency', currency: c }))
37
36
  } catch (e) {currencyFormats.set(c, numberFormat)
38
- }
39
- }
37
+ }}
40
38
  return currencyFormats.get(c)
41
39
  }
42
40
  const formatNumber = (val) => {if (val === null || val === undefined || val === '') return ''
@@ -62,8 +60,7 @@ if (absSec < 2592000) return relativeTimeFormat.format(Math.round(diffSec / 8640
62
60
  if (absSec < 31536000) return relativeTimeFormat.format(Math.round(diffSec / 2592000), 'month')
63
61
  return relativeTimeFormat.format(Math.round(diffSec / 31536000), 'year')
64
62
  }
65
- const INFLECT_CUSTOM = {'пользователь': { gen: 'пользователя', dat: 'пользователю', acc: 'пользователя', ins: 'пользователем', pre: 'пользователе' },'агент': { gen: 'агента', dat: 'агенту', acc: 'агента', ins: 'агентом', pre: 'агенте' },'субагент': { gen: 'субагента', dat: 'субагенту', acc: 'субагента', ins: 'субагентом', pre: 'субагенте' },'модель': { gen: 'модели', dat: 'модели', acc: 'модель', ins: 'моделью', pre: 'модели' },'промпт': { gen: 'промпта', dat: 'промпту', acc: 'промпт', ins: 'промптом', pre: 'промпте' },'инструмент': { gen: 'инструмента', dat: 'инструменту', acc: 'инструмент', ins: 'инструментом', pre: 'инструменте' },'сессия': { gen: 'сессии', dat: 'сессии', acc: 'сессию', ins: 'сессией', pre: 'сессии' },'ветка': { gen: 'ветки', dat: 'ветке', acc: 'ветку', ins: 'веткой', pre: 'ветке' },'файл': { gen: 'файла', dat: 'файлу', acc: 'файл', ins: 'файлом', pre: 'файле' },'папка': { gen: 'папки', dat: 'папке', acc: 'папку', ins: 'папкой', pre: 'папке' }
66
- }
63
+ const INFLECT_CUSTOM = {'пользователь': { gen: 'пользователя', dat: 'пользователю', acc: 'пользователя', ins: 'пользователем', pre: 'пользователе' },'агент': { gen: 'агента', dat: 'агенту', acc: 'агента', ins: 'агентом', pre: 'агенте' },'субагент': { gen: 'субагента', dat: 'субагенту', acc: 'субагента', ins: 'субагентом', pre: 'субагенте' },'модель': { gen: 'модели', dat: 'модели', acc: 'модель', ins: 'моделью', pre: 'модели' },'промпт': { gen: 'промпта', dat: 'промпту', acc: 'промпт', ins: 'промптом', pre: 'промпте' },'инструмент': { gen: 'инструмента', dat: 'инструменту', acc: 'инструмент', ins: 'инструментом', pre: 'инструменте' },'сессия': { gen: 'сессии', dat: 'сессии', acc: 'сессию', ins: 'сессией', pre: 'сессии' },'ветка': { gen: 'ветки', dat: 'ветке', acc: 'ветку', ins: 'веткой', pre: 'ветке' },'файл': { gen: 'файла', dat: 'файлу', acc: 'файл', ins: 'файлом', pre: 'файле' },'папка': { gen: 'папки', dat: 'папке', acc: 'папку', ins: 'папкой', pre: 'папке' }}
67
64
  const keepCase = (src, out) => (src[0] === src[0].toUpperCase() ? out[0].toUpperCase() + out.slice(1) : out)
68
65
  const inflectWord = (word, cName) => {if (!word || typeof word !== 'string') return word
69
66
  const lower = word.toLowerCase()
@@ -72,10 +69,8 @@ if (INFLECT_CUSTOM[lower] && INFLECT_CUSTOM[lower][cName]) {return keepCase(word
72
69
  if (/[a-zA-Z0-9_-]/.test(word) || /^[А-ЯЁ]{2,}$/.test(word)) return word
73
70
  if (/[оеиую]$/i.test(word) && !/(ко|ло|но|то|во|ро|до|по|со|мо|го)$/i.test(word)) return word
74
71
  const w = lower
75
- const endings = [
76
- ['ия', 2, (s) => ({ gen: s + 'ии', dat: s + 'ии', acc: s + 'ию', ins: s + 'ией', pre: s + 'ии' })],['а', 1, (s) => {const genEnd = /[гкхжшчщ]/.test(s.slice(-1)) ? 'и' : 'ы'
77
- return { gen: s + genEnd, dat: s + 'е', acc: s + 'у', ins: s + 'ой', pre: s + 'е' }
78
- }],['я', 1, (s) => ({ gen: s + 'и', dat: s + 'е', acc: s + 'ю', ins: s + 'ей', pre: s + 'е' })],['ь', 1, (s) => ({ gen: s + 'и', dat: s + 'и', acc: s + 'ь', ins: s + 'ью', pre: s + 'и' })],['й', 1, (s) => ({ gen: s + 'я', dat: s + 'ю', acc: s + 'я', ins: s + 'ем', pre: s + 'е' })]
72
+ const endings = [['ия', 2, (s) => ({ gen: s + 'ии', dat: s + 'ии', acc: s + 'ию', ins: s + 'ией', pre: s + 'ии' })],['а', 1, (s) => {const genEnd = /[гкхжшчщ]/.test(s.slice(-1)) ? 'и' : 'ы'
73
+ return { gen: s + genEnd, dat: s + 'е', acc: s + 'у', ins: s + 'ой', pre: s + 'е' }}],['я', 1, (s) => ({ gen: s + 'и', dat: s + 'е', acc: s + 'ю', ins: s + 'ей', pre: s + 'е' })],['ь', 1, (s) => ({ gen: s + 'и', dat: s + 'и', acc: s + 'ь', ins: s + 'ью', pre: s + 'и' })],['й', 1, (s) => ({ gen: s + 'я', dat: s + 'ю', acc: s + 'я', ins: s + 'ем', pre: s + 'е' })]
79
74
  ]
80
75
  for (const [suffix, cut, build] of endings) {if (w.endsWith(suffix)) return keepCase(word, build(w.slice(0, -cut))[cName] || w)
81
76
  }
@@ -156,8 +151,7 @@ if (matched === qStems.length && qStems.length > 0) return 80
156
151
  if (matched > 0) return 50
157
152
  return 0
158
153
  }
159
- const ERROR_MAP = {ENOENT: { title: 'Файл не найден', message: 'Указанный файл или директория не существуют', hint: 'Проверьте правильность указанного пути к файлу.' },EACCES: { title: 'Отказано в доступе', message: 'Недостаточно прав для чтения или записи', hint: 'Проверьте права доступа к файлу или директории (chmod/chown).' },EPERM: { title: 'Операция запрещена', message: 'Недостаточно системных привилегий', hint: 'Запустите процесс с соответствующими правами.' },ECONNREFUSED: { title: 'Соединение отклонено', message: 'Целевой сервер или сервис не отвечает', hint: 'Убедитесь, что локальный или удаленный сервис запущен и слушает порт.' },ECONNRESET: { title: 'Сброс соединения', message: 'Соединение было принудительно разорвано удаленной стороной', hint: 'Проверьте стабильность сети и работу целевого сервера.' },ETIMEDOUT: { title: 'Таймаут соединения', message: 'Превышено время ожидания ответа', hint: 'Проверьте стабильность сети или увеличьте лимит ожидания.' },ENOTFOUND: { title: 'Хост не найден', message: 'Не удалось разрешить сетевой адрес', hint: 'Проверьте правильность URL или настройки DNS.' },EADDRINUSE: { title: 'Порт уже занят', message: 'Сетевой порт используется другим процессом', hint: 'Остановите конфликтующий процесс или выберите другой порт.' },ENOSPC: { title: 'Недостаточно места на диске', message: 'На устройстве закончилось свободное пространство', hint: 'Освободите место на диске и повторите операцию.' },400: { title: 'Некорректный запрос (Bad Request)', message: 'Параметры запроса не соответствуют ожидаемому формату', hint: 'Проверьте синтаксис команды или переданные аргументы.' },401: { title: 'Требуется авторизация', message: 'API-ключ или токен отсутствуют или недействительны', hint: 'Проверьте настройки учетных данных и актуальность токена.' },403: { title: 'Доступ запрещен', message: 'Недостаточно прав для выполнения операции', hint: 'Проверьте область действия токена или права роли.' },404: { title: 'Ресурс не найден', message: 'Запрошенный адрес или объект не существует', hint: 'Проверьте правильность пути или идентификатора ресурса.' },429: { title: 'Превышен лимит запросов', message: 'Слишком много запросов (Rate Limit)', hint: 'Подождите несколько минут перед повторным запросом.' },500: { title: 'Внутренняя ошибка сервера', message: 'На стороне сервера произошел сбой', hint: 'Попробуйте повторить запрос позже или проверьте серверные логи.' },502: { title: 'Ошибочный шлюз (Bad Gateway)', message: 'Промежуточный прокси не получил корректный ответ', hint: 'Проверьте работу нижележащей службы или upstream-сервера.' },503: { title: 'Служба временно недоступна', message: 'Сервер перегружен или находится на обслуживании', hint: 'Попробуйте повторить операцию через некоторое время.' },504: { title: 'Шлюз не отвечает (Gateway Timeout)', message: 'Превышено время ожидания ответа от upstream-сервера', hint: 'Попробуйте повторить запрос позже.' },FETCH_FAILED: { title: 'Сетевой запрос не удался', message: 'Не удалось связаться с сервером (Failed to fetch)', hint: 'Проверьте сетевое подключение и доступность адреса.' },CORS_ERROR: { title: 'Ошибка CORS', message: 'Запрос заблокирован политикой безопасности браузера', hint: 'Проверьте заголовки разрешённых источников на сервере.' },WS_CLOSED: { title: 'Разрыв WebSocket', message: 'Связь с сервером по WebSocket прервана', hint: 'Проверьте работу сервера и сетевой статус.' },QUOTA_EXCEEDED: { title: 'Исчерпана квота API', message: 'Превышен лимит запросов или баланс провайдера', hint: 'Проверьте баланс API-ключа в кабинете провайдера.' },CONTEXT_OVERFLOW: { title: 'Превышен контекст', message: 'Объём диалога превышает размер контекстного окна', hint: 'Очистите или сожмите историю сообщений сессии.' },MODEL_NOT_FOUND: { title: 'Модель не найдена', message: 'Указанная модель отсутствует или недоступна', hint: 'Проверьте идентификатор модели в настройках.' }
160
- }
154
+ const ERROR_MAP = {ENOENT: { title: 'Файл не найден', message: 'Указанный файл или директория не существуют', hint: 'Проверьте правильность указанного пути к файлу.' },EACCES: { title: 'Отказано в доступе', message: 'Недостаточно прав для чтения или записи', hint: 'Проверьте права доступа к файлу или директории (chmod/chown).' },EPERM: { title: 'Операция запрещена', message: 'Недостаточно системных привилегий', hint: 'Запустите процесс с соответствующими правами.' },ECONNREFUSED: { title: 'Соединение отклонено', message: 'Целевой сервер или сервис не отвечает', hint: 'Убедитесь, что локальный или удаленный сервис запущен и слушает порт.' },ECONNRESET: { title: 'Сброс соединения', message: 'Соединение было принудительно разорвано удаленной стороной', hint: 'Проверьте стабильность сети и работу целевого сервера.' },ETIMEDOUT: { title: 'Таймаут соединения', message: 'Превышено время ожидания ответа', hint: 'Проверьте стабильность сети или увеличьте лимит ожидания.' },ENOTFOUND: { title: 'Хост не найден', message: 'Не удалось разрешить сетевой адрес', hint: 'Проверьте правильность URL или настройки DNS.' },EADDRINUSE: { title: 'Порт уже занят', message: 'Сетевой порт используется другим процессом', hint: 'Остановите конфликтующий процесс или выберите другой порт.' },ENOSPC: { title: 'Недостаточно места на диске', message: 'На устройстве закончилось свободное пространство', hint: 'Освободите место на диске и повторите операцию.' },400: { title: 'Некорректный запрос (Bad Request)', message: 'Параметры запроса не соответствуют ожидаемому формату', hint: 'Проверьте синтаксис команды или переданные аргументы.' },401: { title: 'Требуется авторизация', message: 'API-ключ или токен отсутствуют или недействительны', hint: 'Проверьте настройки учетных данных и актуальность токена.' },403: { title: 'Доступ запрещен', message: 'Недостаточно прав для выполнения операции', hint: 'Проверьте область действия токена или права роли.' },404: { title: 'Ресурс не найден', message: 'Запрошенный адрес или объект не существует', hint: 'Проверьте правильность пути или идентификатора ресурса.' },429: { title: 'Превышен лимит запросов', message: 'Слишком много запросов (Rate Limit)', hint: 'Подождите несколько минут перед повторным запросом.' },500: { title: 'Внутренняя ошибка сервера', message: 'На стороне сервера произошел сбой', hint: 'Попробуйте повторить запрос позже или проверьте серверные логи.' },502: { title: 'Ошибочный шлюз (Bad Gateway)', message: 'Промежуточный прокси не получил корректный ответ', hint: 'Проверьте работу нижележащей службы или upstream-сервера.' },503: { title: 'Служба временно недоступна', message: 'Сервер перегружен или находится на обслуживании', hint: 'Попробуйте повторить операцию через некоторое время.' },504: { title: 'Шлюз не отвечает (Gateway Timeout)', message: 'Превышено время ожидания ответа от upstream-сервера', hint: 'Попробуйте повторить запрос позже.' },FETCH_FAILED: { title: 'Сетевой запрос не удался', message: 'Не удалось связаться с сервером (Failed to fetch)', hint: 'Проверьте сетевое подключение и доступность адреса.' },CORS_ERROR: { title: 'Ошибка CORS', message: 'Запрос заблокирован политикой безопасности браузера', hint: 'Проверьте заголовки разрешённых источников на сервере.' },WS_CLOSED: { title: 'Разрыв WebSocket', message: 'Связь с сервером по WebSocket прервана', hint: 'Проверьте работу сервера и сетевой статус.' },QUOTA_EXCEEDED: { title: 'Исчерпана квота API', message: 'Превышен лимит запросов или баланс провайдера', hint: 'Проверьте баланс API-ключа в кабинете провайдера.' },CONTEXT_OVERFLOW: { title: 'Превышен контекст', message: 'Объём диалога превышает размер контекстного окна', hint: 'Очистите или сожмите историю сообщений сессии.' },MODEL_NOT_FOUND: { title: 'Модель не найдена', message: 'Указанная модель отсутствует или недоступна', hint: 'Проверьте идентификатор модели в настройках.' }}
161
155
  const fromMap = (key, rawMsg) => ({code: String(key), title: ERROR_MAP[key].title, message: ERROR_MAP[key].message, hint: ERROR_MAP[key].hint, raw: rawMsg
162
156
  })
163
157
  const humanizeError = (err) => {if (!err) return null
@@ -174,19 +168,16 @@ if (/websocket.*(?:closed|failed)/i.test(rawMsg)) return fromMap('WS_CLOSED', ra
174
168
  if (/insufficient_quota|quota\s*exceeded/i.test(rawMsg)) return fromMap('QUOTA_EXCEEDED', rawMsg)
175
169
  if (/context.*(?:exceeded|overflow|length)/i.test(rawMsg)) return fromMap('CONTEXT_OVERFLOW', rawMsg)
176
170
  if (/model.*not.*found|model.*does not exist/i.test(rawMsg)) return fromMap('MODEL_NOT_FOUND', rawMsg)
177
- return { code: 'UNKNOWN', title: 'Ошибка операции', message: rawMsg, hint: 'Проверьте параметры операции и логи.', raw: rawMsg }
178
- }
171
+ return { code: 'UNKNOWN', title: 'Ошибка операции', message: rawMsg, hint: 'Проверьте параметры операции и логи.', raw: rawMsg }}
179
172
  const formatErrorToast = (err) => {const h = humanizeError(err)
180
173
  if (!h) return null
181
174
  return {type: 'error',title: h.title,description: h.message + (h.hint ? ' ' + h.hint : '')
182
- }
183
- }
175
+ }}
184
176
  const makeIssueUrl = (opts = {}, version = '') => {const repo = 'GooDAnDReaDY/dsh-russian-lang'
185
177
  const title = opts.title || (opts.plugin
186
178
  ? `[Перевод] Запрос локализации для плагина ${opts.plugin}`
187
179
  : '[Ошибка перевода] Неточный перевод фразы')
188
- const bodyLines = [
189
- '### Описание проблемы',opts.description || (opts.plugin
180
+ const bodyLines = ['### Описание проблемы',opts.description || (opts.plugin
190
181
  ? `Просьба добавить русскую локализацию для плагина \`${opts.plugin}\`.`
191
182
  : 'Обнаружена неточность в переводе интерфейса.'),'','### Технический контекст',opts.ns ? `- **Namespace**: \`${opts.ns}\`` : null,opts.key ? `- **Ключ**: \`${opts.key}\`` : null,opts.en ? `- **Оригинал (EN)**: ${opts.en}` : null,opts.ru ? `- **Текущий перевод (RU)**: ${opts.ru}` : null,opts.plugin ? `- **Плагин**: \`${opts.plugin}\`` : null,version ? `- **Версия dsh-russian-lang**: \`${version}\`` : null,typeof navigator !== 'undefined' ? `- **User Agent**: \`${navigator.userAgent}\`` : null,'','### Предлагаемый вариант перевода',opts.proposal || '_Опишите ваш вариант перевода..._'
192
183
  ].filter(Boolean)
@@ -201,16 +192,12 @@ const count = Object.keys((dicts && dicts[ns]) || {}).length
201
192
  return count > 0 ? { status: 'full', count, label: `RU: ${count} строк` } : none
202
193
  }
203
194
  const typoQuotes = (text) => text
204
- .replace(/"([^"\n]{1,200})"/g, '«$1»')
205
- .replace(/[“„]([^“”\n]{1,200})[”"]/g, '«$1»')
206
- .replace(/[『「]([^』」\n]{1,200})[』」]/g, '«$1»')
195
+ .replace(/"([^"\n]{1,200})"/g, '«$1»').replace(/[“„]([^“”\n]{1,200})[”"]/g, '«$1»').replace(/[『「]([^』」\n]{1,200})[』」]/g, '«$1»')
207
196
  const typoDash = (text) => text
208
- .replace(/(^|[\s(\[«])--(?=\s|$)/g, '$1—')
209
- .replace(/(^|[\s(\[«])-(?=\s)/g, '$1—')
197
+ .replace(/(^|[\s(\[«])--(?=\s|$)/g, '$1—').replace(/(^|[\s(\[«])-(?=\s)/g, '$1—')
210
198
  const typoPunct = (text) => text.replace(/\s+([,.:;!?])(?=\s|$)/g, '$1')
211
199
  const TYPO_SHORT = new Set(['в', 'с', 'к', 'о', 'у', 'а', 'и', 'но', 'не', 'ни', 'на', 'по', 'до', 'из', 'за', 'от', 'об'])
212
- const typoNbsp = (text) => text.replace(/(^|[\s(\[«])([а-яё]{1,2})(\s+)/g, (match, lead, word) => (
213
- TYPO_SHORT.has(word) ? lead + word + ' ' : match
200
+ const typoNbsp = (text) => text.replace(/(^|[\s(\[«])([а-яё]{1,2})(\s+)/g, (match, lead, word) => (TYPO_SHORT.has(word) ? lead + word + ' ' : match
214
201
  ))
215
202
  const YO_EDGE_L = '(?<![а-яёА-ЯЁ])'
216
203
  const YO_EDGE_R = '(?![а-яёА-ЯЁ])'
@@ -224,8 +211,7 @@ const makeTypoYo = (pairs) => {const list = typeof pairs === 'string'
224
211
  const compiled = list.map((p) => [new RegExp(YO_EDGE_L + p[0] + YO_EDGE_R, 'gi'), p[1]])
225
212
  return (text) => {for (const [re, repl] of compiled) text = text.replace(re, (match) => yoCase(match, repl))
226
213
  return text
227
- }
228
- }
214
+ }}
229
215
  const LAYOUT_LAT_TO_CYR = { ...EN_RU_KEYS, '/': '.', '`': 'ё' }
230
216
  const LAYOUT_CYR_TO_LAT = (() => {const out = {}
231
217
  for (const k in LAYOUT_LAT_TO_CYR) out[LAYOUT_LAT_TO_CYR[k]] = k
@@ -242,25 +228,26 @@ return words.filter((w) => freq.has(w) || localDict.has(w)).length / words.lengt
242
228
  const candidate = (value, direction) => {if (direction === 'lat2cyr') {const converted = translit(value, LAYOUT_LAT_TO_CYR)
243
229
  if (!/[а-яё]{2}/.test(converted)) return null
244
230
  if (ruWordFraction(converted) < 0.7) return null
245
- return { converted }
246
- }
231
+ return { converted }}
247
232
  const converted = translit(value, LAYOUT_CYR_TO_LAT)
248
233
  return converted.startsWith('/') ? { converted } : null
249
234
  }
250
235
  const learnWords = (text) => {for (const w of text.toLowerCase().split(/[^а-яё]+/).filter(Boolean)) {if (w.length >= 3) localDict.add(w)
251
- }
252
- }
253
- return { ruWordFraction, candidate, learnWords, localDict }
254
- }
236
+ }}
237
+ return { ruWordFraction, candidate, learnWords, localDict }}
255
238
  const SHORT_WORDS_RE = /(^|[\s(«])([вВнНсСпПоОкКуУиИаА]|из|от|до|за|по|со|во|ко|об|на|под|над|при|про|без|для|не|ни|но)( )/g
256
239
  const formatInputLive = (text) => {if (!text || typeof text !== 'string') return text
257
240
  const parts = text.split(/(```[\s\S]*?```|`[^`\n]*`)/g)
258
241
  for (let i = 0; i < parts.length; i += 2) {let s = parts[i]
259
242
  if (!s) continue
243
+ const lq = [], cf = []
244
+ s = s.replace(/"([^"\n]*[a-zA-Z][^"\n]*)"/g, (m) => `\x01L${lq.push(m) - 1}\x01`)
245
+ s = s.replace(/(^|\s)(--[a-zA-Z0-9_\u0400-\u04FF-]+)/g, (m, p1, p2) => `${p1}\x01C${cf.push(p2) - 1}\x01`)
246
+ s = s.replace(/(^|\s)(--)(?=\s|$)/g, (m, p1, p2) => /[a-zA-Z]/.test(text) ? `${p1}\x01C${cf.push(p2) - 1}\x01` : m)
260
247
  s = s.replace(/--/g, '—')
261
- s = s.replace(/(^|[\s([{-])"/g, '$1«')
262
- s = s.replace(/"/g, '»')
248
+ s = s.replace(/(^|[\s([{-])"/g, '$1«').replace(/"/g, '»')
263
249
  s = s.replace(SHORT_WORDS_RE, '$1$2\u00A0')
250
+ s = s.replace(/\x01C(\d+)\x01/g, (_, j) => cf[+j]).replace(/\x01L(\d+)\x01/g, (_, j) => lq[+j])
264
251
  parts[i] = s
265
252
  }
266
253
  return parts.join('')
@@ -276,11 +263,9 @@ if (RUSSIAN_SLASH_ALIASES[lowerCmd]) {return RUSSIAN_SLASH_ALIASES[lowerCmd] + r
276
263
  }
277
264
  return input
278
265
  }
279
- const PHONETIC_LAT_TO_CYR = [
280
- ['shch', 'щ'], ['yo', 'ё'], ['zh', 'ж'], ['ch', 'ч'], ['sh', 'ш'],['yu', 'ю'], ['ya', 'я'], ['ts', 'ц'],['a', 'а'], ['b', 'б'], ['v', 'в'], ['g', 'г'], ['d', 'д'], ['e', 'е'],['z', 'з'], ['i', 'и'], ['j', 'й'], ['k', 'к'], ['l', 'л'], ['m', 'м'],['n', 'н'], ['o', 'о'], ['p', 'п'], ['r', 'р'], ['s', 'с'], ['t', 'т'],['u', 'у'], ['f', 'ф'], ['h', 'х'], ['c', 'ц'], ['y', 'ы'], ['x', 'кс']
266
+ const PHONETIC_LAT_TO_CYR = [['shch', 'щ'], ['yo', 'ё'], ['zh', 'ж'], ['ch', 'ч'], ['sh', 'ш'],['yu', 'ю'], ['ya', 'я'], ['ts', 'ц'],['a', 'а'], ['b', 'б'], ['v', 'в'], ['g', 'г'], ['d', 'д'], ['e', 'е'],['z', 'з'], ['i', 'и'], ['j', 'й'], ['k', 'к'], ['l', 'л'], ['m', 'м'],['n', 'н'], ['o', 'о'], ['p', 'п'], ['r', 'р'], ['s', 'с'], ['t', 'т'],['u', 'у'], ['f', 'ф'], ['h', 'х'], ['c', 'ц'], ['y', 'ы'], ['x', 'кс']
281
267
  ]
282
- const PHONETIC_CYR_TO_LAT = [
283
- ['щ', 'shch'], ['ё', 'yo'], ['ж', 'zh'], ['ч', 'ch'], ['ш', 'sh'],['ю', 'yu'], ['я', 'ya'], ['ц', 'ts'],['а', 'a'], ['б', 'b'], ['в', 'v'], ['г', 'g'], ['д', 'd'], ['е', 'e'],['з', 'z'], ['и', 'i'], ['й', 'j'], ['к', 'k'], ['л', 'l'], ['м', 'm'],['н', 'n'], ['о', 'o'], ['п', 'p'], ['р', 'r'], ['с', 's'], ['т', 't'],['у', 'u'], ['ф', 'f'], ['х', 'h'], ['ы', 'y'], ['э', 'e'], ['ъ', ''], ['ь', '']
268
+ const PHONETIC_CYR_TO_LAT = [['щ', 'shch'], ['ё', 'yo'], ['ж', 'zh'], ['ч', 'ch'], ['ш', 'sh'],['ю', 'yu'], ['я', 'ya'], ['ц', 'ts'],['а', 'a'], ['б', 'b'], ['в', 'v'], ['г', 'g'], ['д', 'd'], ['е', 'e'],['з', 'z'], ['и', 'i'], ['й', 'j'], ['к', 'k'], ['л', 'l'], ['м', 'm'],['н', 'n'], ['о', 'o'], ['п', 'p'], ['р', 'r'], ['с', 's'], ['т', 't'],['у', 'u'], ['ф', 'f'], ['х', 'h'], ['ы', 'y'], ['э', 'e'], ['ъ', ''], ['ь', '']
284
269
  ]
285
270
  const phoneticTranslit = (text, direction = 'lat2cyr') => {if (!text || typeof text !== 'string') return text
286
271
  let res = text
@@ -294,8 +279,7 @@ res = res.replaceAll(fromTitle, toTitle)
294
279
  }
295
280
  res = res.replaceAll(from, to)
296
281
  if (from.length === 1) {res = res.replaceAll(fromUpper, toUpper)
297
- }
298
- }
282
+ }}
299
283
  return res
300
284
  }
301
285
  const detectInputLayout = (text) => {if (!text || typeof text !== 'string') return null
@@ -316,8 +300,7 @@ const SYSTEM_PROMPT_PRESETS = {technical_expert: {id: 'technical_expert',label:
316
300
  },code_reviewer: {id: 'code_reviewer',label: 'Код-ревьюер',desc: 'Поиск багов, безопасность и архитектурный аудит',text: 'Анализируй код и отвечай на русском языке. Ищи уязвимости, граничные случаи и нарушения контрактов, предлагая исправления.'
317
301
  },architect: {id: 'architect',label: 'Системный архитектор',desc: 'Контракты API/CLI, модульность и надёжность',text: 'Отвечай на русском языке как архитектор. Проектируй модульные системы, чёткие контракты API/CLI и надёжные решения без лишней сложности.'
318
302
  },tutor: {id: 'tutor',label: 'Наставник (ментор)',desc: 'Понятные объяснения сложных концепций и пошаговый разбор',text: 'Отвечай на русском языке как наставник. Объясняй сложные темы простыми словами, наглядными аналогиями и пошаговыми примерами.'
319
- }
320
- }
303
+ }}
321
304
  const exportSessionToMarkdown = (session, options = {}) => {if (!session) return ''
322
305
  const title = session.title || session.name || 'Диалог DSH'
323
306
  const date = session.createdAt ? new Date(session.createdAt) : new Date()
@@ -350,53 +333,31 @@ return md
350
333
  const findTranslationKey = (text, dicts = {}, overrides = {}, zhRu = {}, domEn = {}, options = {}) => {if (!text || typeof text !== 'string') return null
351
334
  const trimmed = text.trim()
352
335
  if (!trimmed) return null
353
- if (overrides && typeof overrides === 'object') {if (overrides[trimmed] !== undefined) {return { key: trimmed, value: overrides[trimmed], source: 'override' }
354
- }
355
- for (const [k, v] of Object.entries(overrides)) {if (v === text || (typeof v === 'string' && v.trim() === trimmed)) {return { key: k, value: v, source: 'override' }
356
- }
357
- }
358
- }
336
+ if (overrides && typeof overrides === 'object') {if (overrides[trimmed] !== undefined) {return { key: trimmed, value: overrides[trimmed], source: 'override' }}
337
+ for (const [k, v] of Object.entries(overrides)) {if (v === text || (typeof v === 'string' && v.trim() === trimmed)) {return { key: k, value: v, source: 'override' }}}}
359
338
  if (dicts && typeof dicts === 'object') {for (const [ns, map] of Object.entries(dicts)) {if (!map || typeof map !== 'object') continue
360
- if (map[trimmed] !== undefined) {return { ns, key: trimmed, value: map[trimmed], source: 'dictionary' }
361
- }
362
- for (const [k, v] of Object.entries(map)) {if (v === text || (typeof v === 'string' && v.trim() === trimmed)) {return { ns, key: k, value: v, source: 'dictionary' }
363
- }
364
- }
365
- }
366
- }
367
- if (zhRu && typeof zhRu === 'object') {for (const [zh, ru] of Object.entries(zhRu)) {if (ru === text || (typeof ru === 'string' && ru.trim() === trimmed)) {return { zh, key: zh, value: ru, source: 'dom_zh' }
368
- }
369
- }
370
- if (zhRu[trimmed] !== undefined) {return { zh: trimmed, key: trimmed, value: zhRu[trimmed], source: 'dom_zh_original' }
371
- }
372
- }
373
- if (domEn && typeof domEn === 'object') {for (const [en, ru] of Object.entries(domEn)) {if (ru === text || (typeof ru === 'string' && ru.trim() === trimmed)) {return { en, key: en, value: ru, source: 'dom_en' }
374
- }
375
- }
376
- if (domEn[trimmed] !== undefined) {return { en: trimmed, key: trimmed, value: domEn[trimmed], source: 'dom_en_original' }
377
- }
378
- }
339
+ if (map[trimmed] !== undefined) {return { ns, key: trimmed, value: map[trimmed], source: 'dictionary' }}
340
+ for (const [k, v] of Object.entries(map)) {if (v === text || (typeof v === 'string' && v.trim() === trimmed)) {return { ns, key: k, value: v, source: 'dictionary' }}}}}
341
+ if (zhRu && typeof zhRu === 'object') {for (const [zh, ru] of Object.entries(zhRu)) {if (ru === text || (typeof ru === 'string' && ru.trim() === trimmed)) {return { zh, key: zh, value: ru, source: 'dom_zh' }}}
342
+ if (zhRu[trimmed] !== undefined) {return { zh: trimmed, key: trimmed, value: zhRu[trimmed], source: 'dom_zh_original' }}}
343
+ if (domEn && typeof domEn === 'object') {for (const [en, ru] of Object.entries(domEn)) {if (ru === text || (typeof ru === 'string' && ru.trim() === trimmed)) {return { en, key: en, value: ru, source: 'dom_en' }}}
344
+ if (domEn[trimmed] !== undefined) {return { en: trimmed, key: trimmed, value: domEn[trimmed], source: 'dom_en_original' }}}
379
345
  if (options && options.detectUntranslated) {const hasRu = /[а-яё]/i.test(trimmed)
380
346
  const hasZh = /[㐀-鿿豈-﫿]/.test(trimmed)
381
347
  const hasEn = /[a-z]/i.test(trimmed)
382
- if (!hasRu && hasZh) {return { key: trimmed, value: trimmed, source: 'untranslated_zh' }
383
- }
384
- if (!hasRu && hasEn) {return { key: trimmed, value: trimmed, source: 'untranslated_en' }
385
- }
386
- }
348
+ if (!hasRu && hasZh) {return { key: trimmed, value: trimmed, source: 'untranslated_zh' }}
349
+ if (!hasRu && hasEn) {return { key: trimmed, value: trimmed, source: 'untranslated_en' }}}
387
350
  return null
388
351
  }
389
352
  const generateBugReportSnippet = (info = {}) => {const { key, ns, original, current, override, pkgVersion = '0.3.1' } = info
390
- const lines = [
391
- '### 🌐 Неточность перевода / Пользовательское предложение','- **Пакет:** `@goodandready/dsh-russian-lang@v' + pkgVersion + '`',ns ? '- **Пространство имён:** `' + ns + '`' : null,key ? '- **Ключ:** `' + key + '`' : null,original ? '- **Оригинальный текст:** `' + original + '`' : null,current ? '- **Текущий перевод:** `' + current + '`' : null,override ? '- **Предлагаемый перевод:** `' + override + '`' : null,].filter(Boolean)
353
+ const lines = ['### 🌐 Неточность перевода / Пользовательское предложение','- **Пакет:** `@goodandready/dsh-russian-lang@v' + pkgVersion + '`',ns ? '- **Пространство имён:** `' + ns + '`' : null,key ? '- **Ключ:** `' + key + '`' : null,original ? '- **Оригинальный текст:** `' + original + '`' : null,current ? '- **Текущий перевод:** `' + current + '`' : null,override ? '- **Предлагаемый перевод:** `' + override + '`' : null,].filter(Boolean)
392
354
  return lines.join('\n')
393
355
  }
394
356
  const bilingualCommandMatch = (query, command, options = {}) => {if (!query || !command) return { matched: false, score: 0 }
395
357
  const q = String(query).trim().toLowerCase()
396
358
  if (!q) return { matched: false, score: 0 }
397
359
  const cmdObj = typeof command === 'string' ? { title: command } : command
398
- const candidates = [
399
- cmdObj.title,cmdObj.label,cmdObj.name,cmdObj.originalTitle,cmdObj.enTitle,cmdObj.id,cmdObj.description,cmdObj.category,...(Array.isArray(cmdObj.keywords) ? cmdObj.keywords : [])
360
+ const candidates = [cmdObj.title,cmdObj.label,cmdObj.name,cmdObj.originalTitle,cmdObj.enTitle,cmdObj.id,cmdObj.description,cmdObj.category,...(Array.isArray(cmdObj.keywords) ? cmdObj.keywords : [])
400
361
  ].filter((c) => typeof c === 'string' && c.trim())
401
362
  if (!candidates.length) return { matched: false, score: 0 }
402
363
  const queryVariants = [{ text: q, kind: 'direct' }]
@@ -417,25 +378,21 @@ for (const variant of queryVariants) {for (const cand of candidates) {const scor
417
378
  if (score > bestScore) {bestScore = score
418
379
  bestVariant = variant
419
380
  bestCandidate = cand
420
- }
421
- }
422
- }
381
+ }}}
423
382
  const threshold = options.threshold ?? 50
424
383
  return {matched: bestScore >= threshold,score: bestScore,matchedText: bestCandidate,variant: bestVariant ? bestVariant.text : q,kind: bestVariant ? bestVariant.kind : 'none'
425
- }
426
- }
384
+ }}
427
385
  const filterCommands = (query, commands = [], options = {}) => {if (!Array.isArray(commands)) return []
428
386
  if (!query || !String(query).trim()) return commands
429
387
  const results = []
430
388
  for (const cmd of commands) {const res = bilingualCommandMatch(query, cmd, options)
431
389
  if (res.matched) {results.push({ item: cmd, ...res })
432
- }
433
- }
390
+ }}
434
391
  return results.sort((a, b) => b.score - a.score).map((r) => r.item)
435
392
  }
436
- const RU = {"common":{"back":"Назад","brand.localBuild":"Локальная сборка DSH","cancel":"Отмена","close":"Закрыть","codeBlock.title":"Блок кода","codeBlock.unwrap":"Не переносить строки","codeBlock.wrap":"Переносить строки","collapse":"Свернуть","copied":"Скопировано","copy":"Копировать","copy.compactJson":"Копировать компактный JSON","copy.failed":"Не удалось скопировать","copy.json":"Копировать JSON","copy.optionsHint":"{action}; правый клик для вариантов копирования","copy.path":"Копировать путь к свойству","copy.prettyJson":"Копировать форматированный JSON","copy.value":"Копировать значение","delete":"Удалить","edit":"Изменить","expand":"Развернуть","json.collapseNode":"Свернуть узел JSON","json.expandNode":"Развернуть узел JSON","json.label":"JSON","load.failed":"Не удалось загрузить","loading":"Загрузка…","markdown.footnotes":"Сноски","markdown.truncatedCharacters":"… обрезано на {total} символах","more":"Ещё","next":"Дальше","none":"Нет","number.million":"{value}M","number.thousand":"{value}K","ok":"ОК","previous":"Назад","retry":"Повторить","save":"Сохранить","search":"Поиск","skip":"Пропустить","submit":"Отправить","submitting":"Отправка…","truncated":"Обрезано","unknown":"Неизвестно"},"dsh-cron":{"sidebar.label":"Задачи по расписанию"},"dsh-usage-stats":{"footer.todayLabel":"Сегодня"},"pluginMarket":{"trigger":"Магазин плагинов"}}
393
+ const RU = {"common":{"back":"Назад","brand.localBuild":"Локальная сборка DSH","cancel":"Отмена","close":"Закрыть","codeBlock.title":"Блок кода","codeBlock.unwrap":"Не переносить строки","codeBlock.wrap":"Переносить строки","collapse":"Свернуть","copied":"Скопировано","copy":"Копировать","copy.compactJson":"Копировать компактный JSON","copy.failed":"Не удалось скопировать","copy.json":"Копировать JSON","copy.optionsHint":"{action}; правый клик для вариантов копирования","copy.path":"Копировать путь к свойству","copy.prettyJson":"Копировать форматированный JSON","copy.value":"Копировать значение","delete":"Удалить","edit":"Изменить","expand":"Развернуть","json.label":"JSON","load.failed":"Не удалось загрузить","loading":"Загрузка…","markdown.footnotes":"Сноски","markdown.truncatedCharacters":"… обрезано на {total} символах","more":"Ещё","next":"Дальше","none":"Нет","number.million":"{value}M","number.thousand":"{value}K","ok":"ОК","previous":"Назад","retry":"Повторить","save":"Сохранить","search":"Поиск","skip":"Пропустить","submit":"Отправить","submitting":"Отправка…","truncated":"Обрезано","unknown":"Неизвестно"},"dsh-cron":{"sidebar.label":"Задачи по расписанию"},"dsh-usage-stats":{"footer.todayLabel":"Сегодня"},"pluginMarket":{"trigger":"Магазин плагинов"}}
437
394
  const ZH_RU = {}
438
- RU['russian-lang'] = {"cardTitle":"Русская локализация","cardSub":"Язык интерфейса, типографика, раскладка","badgeRu":"🟢 RU активен","badgeEn":"⚪ EN активен","badgeCoverage":"🟢 100% (7,902 ключа)","badgeSmartUx":"⚡ Smart UX активен","secLanguage":"🌐 Язык интерфейса","secLanguageDesc":"Нативное переключение языка интерфейса DSH на русский без перезагрузки страницы.","enabled":"Русский язык включён","enabledDesc":"Переключает язык интерфейса DeepSeek Harness на русский.","quickSwitchNote":"Быстрый переключатель RU ⇄ EN доступен в шапке сессии рядом с кнопками диалога.","secTypography":"✍️ Умная типографика и ввод (Smart UX)","secTypographyDesc":"Автоматическое улучшение текстов по нормам русской типографики во время диалога и набора.","typography":"Типографика вывода","typographyDesc":"Исправляет типографику ответов модели: кавычки-«ёлочки», тире («—») вместо дефисов, неразрывные пробелы после предлогов.","yo":"Буква «ё»","yoDesc":"Восстанавливать «ё» в частых словах (ещё, чёрный, идёт и др.), написанных через «е». Неоднозначные слова (все/всё) не трогаются.","liveInput":"Живая типографика инпута","liveInputDesc":"Автоматически заменять \"\" на «» и -- на — прямо во время набора промпта (код в бэктиках игнорируется).","slashAliases":"Русские алиасы команд","slashAliasesDesc":"Поддержка русских команд: /цель -> /goal, /сжать -> /compact, /план -> /plan, /справка -> /help, /память -> /memory.","altLHintText":"Мгновенная конвертация раскладки текущего поля (ghbdtn ⇄ привет, /vjltkm ⇄ /model). В углу поля ввода также отображается метка раскладки.","secAgentPrompt":"🤖 Системный промпт агента","secAgentPromptDesc":"Официальная секция расширения DSH systemPrompt для ведения диалога на русском языке.","agentPrompt":"Русский промпт агента","agentPromptDesc":"Добавляет в системный промпт инструкцию отвечать по-русски в выбранном стиле.","agentPromptPreset":"Стиль ответов агента","presetExpert":"Технический эксперт (строгая терминология, чистый код)","presetWriter":"Технический писатель (Markdown, таблицы, ГОСТ)","presetConcise":"Лаконичный режим (кратко, без лишней воды)","presetReviewer":"Код-ревьюер (поиск багов, безопасность, аудит)","presetArchitect":"Системный архитектор (контракты, масштабируемость)","presetTutor":"Наставник (понятные объяснения, пошаговый разбор)","secUpdater":"🔄 Обновление языкового пакета","secUpdaterDesc":"Проверка наличия новых релизов в реестре npm и обновление в один клик.","updaterCurrent":"Текущая версия: v{version}","updaterLatest":"Доступна новая версия: v{version}","updaterUpToDate":"Установлена актуальная версия","updaterChecking":"Проверка…","updaterCheckBtn":"Проверить обновления","updaterBtn":"Обновить до v{version} в 1 клик","updaterUpdating":"Установка обновления…","updaterSuccess":"✅ Плагин успешно обновлён! Перезапустите DSH для применения.","updaterFailed":"❌ Не удалось проверить/обновить плагин. Проверьте сеть или логи сервера.","badgeUpdateAvailable":"Доступно обновление","badgeUpToDate":"Актуальная версия","secTranslator":"🌐 Перевод сообщений ассистента","secTranslatorDesc":"Настройка перевода ответов модели в чате на русский язык (локально в Docker или онлайн).","translateEngine":"Движок перевода","engineOff":"Выключен (по умолчанию, без сетевых вызовов)","engineLocal":"Локальный LibreTranslate (приватно, ~600 МБ RAM)","engineGoogle":"Google Translate (онлайн)","googleWarn":"⚠️ Внимание: при онлайн-переводе текст сообщений ассистента передаётся на публичные серверы Google для перевода на лету. Не используйте для конфиденциальных данных.","localStatusRunning":"🟢 Контейнер LibreTranslate активен","localStatusStopped":"⚪ Контейнер LibreTranslate не запущен","localStartBtn":"Запустить LibreTranslate в Docker (~600 МБ RAM)","localStarting":"Запуск контейнера…","localInfo":"LibreTranslate запускается изолированно в Docker на сервере, потребляет ~500–700 МБ RAM и переводит на 100% локально.","secOverrides":"✍️ Пользовательские переопределения (Custom Overrides)","secOverridesDesc":"Возможность заменить любую формулировку интерфейса DSH на собственную. Переопределения сохраняются в настройках и имеют наивысший приоритет.","overrideKeyPlaceholder":"Ключ (например, common.back или settings.title)","overrideValuePlaceholder":"Ваш русский перевод","overrideAddBtn":"Добавить","overrideEmpty":"Переопределений пока нет.","overrideDelete":"Удалить","overrideTip":"💡 Совет: зажмите Alt и кликните на любой переведённый элемент интерфейса, чтобы открыть всплывающий инспектор перевода и сразу изменить формулировку.","secSupport":"📊 Покрытие экосистемы и поддержка","secSupportDesc":"Словари синхронизированы с DSH v0.1.6-alpha.1. 100.0% UI-покрытие ядра и всех установленных плагинов (7,902 ключа) без черновых машинных переводов.","statNamespaces":"Пространств имён","statCoreKeys":"Ключей ядра","statPluginKeys":"Ключей плагинов","overridesCount":"Своих переопределений","statusLoading":"Настройки загружаются…","statusUnavailable":"Настройки недоступны на этом хосте","translateTurn":"Перевести на русский","reportIssue":"Сообщить о неточности перевода","exportMdHint":"Экспорт диалога в Markdown доступен по кнопке [ 📥 MD ] в шапке сессии.","translateTurnHint":"Перевод ответов ассистента на русский доступен по кнопке [ RU ↗ ] на блоках сообщений."}
395
+ RU['russian-lang'] = {"cardTitle":"Русская локализация","cardSub":"Язык интерфейса, типографика, раскладка","badgeRu":"🟢 RU активен","badgeEn":"⚪ EN активен","badgeCoverage":"🟢 100% (7,902 ключа)","badgeSmartUx":"⚡ Smart UX активен","secLanguage":"🌐 Язык интерфейса","secLanguageDesc":"Нативное переключение языка интерфейса DSH на русский без перезагрузки страницы.","enabled":"Русский язык включён","enabledDesc":"Переключает язык интерфейса DeepSeek Harness на русский.","quickSwitchNote":"Быстрый переключатель RU ⇄ EN доступен в шапке сессии рядом с кнопками диалога.","secTypography":"✍️ Умная типографика и ввод (Smart UX)","secTypographyDesc":"Автоматическое улучшение текстов по нормам русской типографики во время диалога и набора.","typography":"Типографика вывода","typographyDesc":"Исправляет типографику ответов модели: кавычки-«ёлочки», тире («—») вместо дефисов, неразрывные пробелы после предлогов.","yo":"Буква «ё»","yoDesc":"Восстанавливать «ё» в частых словах (ещё, чёрный, идёт и др.), написанных через «е». Неоднозначные слова (все/всё) не трогаются.","liveInput":"Живая типографика инпута","liveInputDesc":"Форматировать текст сообщения перед отправкой: кавычки «», тире — и неразрывные пробелы. Внимание: изменяет отправляемый агенту текст (команды и флаги с латиницей защищены).","slashAliases":"Русские алиасы команд","slashAliasesDesc":"Поддержка русских команд: /цель -> /goal, /сжать -> /compact, /план -> /plan, /справка -> /help, /память -> /memory.","altLHintText":"Мгновенная конвертация раскладки текущего поля (ghbdtn ⇄ привет, /vjltkm ⇄ /model). В углу поля ввода также отображается метка раскладки.","secAgentPrompt":"🤖 Системный промпт агента","secAgentPromptDesc":"Официальная секция расширения DSH systemPrompt для ведения диалога на русском языке.","agentPrompt":"Русский промпт агента","agentPromptDesc":"Добавляет в системный промпт инструкцию отвечать по-русски в выбранном стиле.","agentPromptPreset":"Стиль ответов агента","presetExpert":"Технический эксперт (строгая терминология, чистый код)","presetWriter":"Технический писатель (Markdown, таблицы, ГОСТ)","presetConcise":"Лаконичный режим (кратко, без лишней воды)","presetReviewer":"Код-ревьюер (поиск багов, безопасность, аудит)","presetArchitect":"Системный архитектор (контракты, масштабируемость)","presetTutor":"Наставник (понятные объяснения, пошаговый разбор)","secUpdater":"🔄 Обновление языкового пакета","secUpdaterDesc":"Проверка наличия новых релизов в реестре npm и обновление в один клик.","updaterCurrent":"Текущая версия: v{version}","updaterLatest":"Доступна новая версия: v{version}","updaterUpToDate":"Установлена актуальная версия","updaterChecking":"Проверка…","updaterCheckBtn":"Проверить обновления","updaterBtn":"Обновить до v{version} в 1 клик","updaterUpdating":"Установка обновления…","updaterSuccess":"✅ Плагин успешно обновлён! Перезапустите DSH для применения.","updaterFailed":"❌ Не удалось проверить/обновить плагин. Проверьте сеть или логи сервера.","badgeUpdateAvailable":"Доступно обновление","badgeUpToDate":"Актуальная версия","secTranslator":"🌐 Перевод сообщений ассистента","secTranslatorDesc":"Настройка перевода ответов модели в чате на русский язык (локально в Docker или онлайн).","translateEngine":"Движок перевода","engineOff":"Выключен (по умолчанию, без сетевых вызовов)","engineLocal":"Локальный LibreTranslate (приватно, ~600 МБ RAM)","engineGoogle":"Google Translate (онлайн)","googleWarn":"⚠️ Внимание: при онлайн-переводе текст сообщений ассистента передаётся на публичные серверы Google для перевода на лету. Не используйте для конфиденциальных данных.","localStatusRunning":"🟢 Контейнер LibreTranslate активен","localStatusStopped":"⚪ Контейнер LibreTranslate не запущен","localStartBtn":"Запустить LibreTranslate в Docker (~600 МБ RAM)","localStarting":"Запуск контейнера…","localInfo":"LibreTranslate запускается изолированно в Docker на сервере, потребляет ~500–700 МБ RAM и переводит на 100% локально.","secOverrides":"✍️ Пользовательские переопределения (Custom Overrides)","secOverridesDesc":"Возможность заменить любую формулировку интерфейса DSH на собственную. Переопределения сохраняются в настройках и имеют наивысший приоритет.","overrideKeyPlaceholder":"Ключ (например, common.back или settings.title)","overrideValuePlaceholder":"Ваш русский перевод","overrideAddBtn":"Добавить","overrideEmpty":"Переопределений пока нет.","overrideDelete":"Удалить","overrideTip":"💡 Совет: зажмите Alt и кликните на любой переведённый элемент интерфейса, чтобы открыть всплывающий инспектор перевода и сразу изменить формулировку.","secSupport":"📊 Покрытие экосистемы и поддержка","secSupportDesc":"Словари синхронизированы с DSH v0.1.6-alpha.1. 100.0% UI-покрытие ядра и всех установленных плагинов (7,902 ключа) без черновых машинных переводов.","statNamespaces":"Пространств имён","statCoreKeys":"Ключей ядра","statPluginKeys":"Ключей плагинов","overridesCount":"Своих переопределений","statusLoading":"Настройки загружаются…","statusUnavailable":"Настройки недоступны на этом хосте","translateTurn":"Перевести на русский","reportIssue":"Сообщить о неточности перевода","exportMdHint":"Экспорт диалога в Markdown доступен по кнопке [ 📥 MD ] в шапке сессии.","translateTurnHint":"Перевод ответов ассистента на русский доступен по кнопке [ RU ↗ ] на блоках сообщений."}
439
396
  const SETTINGS_NS_NAME = 'russian-lang'
440
397
  const ROW_CONFIG_KEY = '@goodandready/dsh-russian-lang#russian-lang'
441
398
  function apply(ctx) {const runtime = ctx.locale
@@ -447,37 +404,26 @@ return null
447
404
  const rawScope = resolveScope(ctx)
448
405
  const scope = (rawScope && typeof rawScope.getSnapshot === 'function') ? rawScope : {getSnapshot: () => ({ status: 'ready', value: {} }),subscribe: () => () => {},set: () => Promise.resolve()
449
406
  }
450
- for (const ns of Object.keys(RU)) {ctx.effect(() => {try { return ctx.locale.register(ns, 'ru', RU[ns]) }
451
- catch (err) { return () => {} }
452
- }, 'dsh-russian-lang: ' + ns)
407
+ for (const ns of Object.keys(RU)) {ctx.effect(() => {try { return ctx.locale.register(ns, 'ru', RU[ns]) } catch (err) { return () => {} }}, 'dsh-russian-lang: ' + ns)
453
408
  }
454
409
  const registerDictMap = (dictMap) => {if (!dictMap || typeof dictMap !== 'object') return
455
410
  for (const ns of Object.keys(dictMap)) {if (ns === 'zhRu' || ns === 'plugins' || ns === 'core') continue
456
411
  const dict = dictMap[ns]
457
412
  if (typeof dict !== 'object' || !dict) continue
458
413
  if (!RU[ns]) {RU[ns] = dict
459
- ctx.effect(() => {try { return ctx.locale.register(ns, 'ru', dict) }
460
- catch (err) { return () => {} }
461
- }, 'dsh-russian-lang: ' + ns)
414
+ ctx.effect(() => {try { return ctx.locale.register(ns, 'ru', dict) } catch (err) { return () => {} }}, 'dsh-russian-lang: ' + ns)
462
415
  } else {Object.assign(RU[ns], dict)
463
- }
464
- }
465
- }
416
+ }}}
466
417
  const CACHE_NAME = 'dsh-ru-cache-v1'
467
418
  const loadLocalDict = (key) => {try {const raw = typeof localStorage !== 'undefined' ? localStorage.getItem('dsh_ru_' + key) : null
468
419
  return raw ? JSON.parse(raw) : null
469
- } catch (_) { return null }
470
- }
420
+ } catch (_) { return null }}
471
421
  const saveLocalDict = (key, etag, data) => {try {if (typeof localStorage !== 'undefined') {localStorage.setItem('dsh_ru_' + key, JSON.stringify({ etag, data }))
472
- }
473
- } catch (_e) { void _e }
474
- }
422
+ }} catch (_e) { void _e }}
475
423
  try {const cachedCore = loadLocalDict('core')
476
424
  if (cachedCore && cachedCore.data && cachedCore.data.core) {registerDictMap(cachedCore.data.core)
477
425
  if (cachedCore.data.zhRu && typeof ZH_RU === 'object') {Object.assign(ZH_RU, cachedCore.data.zhRu)
478
- }
479
- }
480
- } catch (_e) { void _e }
426
+ }}} catch (_e) { void _e }
481
427
  const fetchCachedResource = async (url, cacheKey, onData) => {let etag = null
482
428
  let delivered = false
483
429
  if (typeof caches !== 'undefined') {try {const cache = await caches.open(CACHE_NAME)
@@ -486,16 +432,12 @@ if (matched) {etag = matched.headers.get('etag')
486
432
  const parsed = await matched.json()
487
433
  if (parsed) {delivered = true
488
434
  onData(parsed)
489
- }
490
- }
491
- } catch (_e) { void _e }
492
- }
435
+ }}} catch (_e) { void _e }}
493
436
  if (!delivered && cacheKey) {const local = loadLocalDict(cacheKey)
494
437
  if (local && local.data) {etag = local.etag
495
438
  delivered = true
496
439
  onData(local.data)
497
- }
498
- }
440
+ }}
499
441
  if (typeof fetch !== 'function') return
500
442
  try {const headers = { 'Accept': 'application/json' }
501
443
  if (etag) headers['If-None-Match'] = etag
@@ -507,13 +449,9 @@ const data = await res.json()
507
449
  if (data) {onData(data)
508
450
  if (typeof caches !== 'undefined') {try {const cache = await caches.open(CACHE_NAME)
509
451
  await cache.put(url, cloned)
510
- } catch (_e) { void _e }
511
- }
452
+ } catch (_e) { void _e }}
512
453
  if (cacheKey) saveLocalDict(cacheKey, newEtag, data)
513
- }
514
- }
515
- } catch (_e) { void _e }
516
- }
454
+ }}} catch (_e) { void _e }}
517
455
  const loadedPluginNames = new Set()
518
456
  const pendingPluginLoads = new Set()
519
457
  let pluginBatchTimer = null
@@ -529,8 +467,7 @@ const url = '/api/dsh-russian-lang/dict/plugins' + qs
529
467
  const cacheKey = toLoad.length > 0 ? ('plugins_' + toLoad.sort().join('_')) : 'plugins_all'
530
468
  fetchCachedResource(url, cacheKey, (data) => {if (data && data.plugins) {registerDictMap(data.plugins)
531
469
  for (const n of toLoad) loadedPluginNames.add(n)
532
- }
533
- }).catch(() => {}).finally(() => {for (const n of toLoad) pendingPluginLoads.delete(n)
470
+ }}).catch(() => {}).finally(() => {for (const n of toLoad) pendingPluginLoads.delete(n)
534
471
  })
535
472
  }
536
473
  const schedulePluginLoad = (name) => {if (!name || loadedPluginNames.has(name) || pendingPluginLoads.has(name)) return
@@ -547,103 +484,76 @@ if (typeof updateZhRu === 'function') updateZhRu(data.zhRu)
547
484
  }
548
485
  if (typeof syncZhDom === 'function') syncZhDom()
549
486
  setTimeout(() => { loadPluginDictionaries([]) }, 100)
550
- }).catch(() => {if (typeof fetch === 'function') {fetch('/api/dsh-russian-lang/dict/all', { headers: { 'Accept': 'application/json' } })
551
- .then((res) => res.ok ? res.json() : null)
552
- .then((data) => {if (!data) return
487
+ }).catch(() => {if (typeof fetch === 'function') {fetch('/api/dsh-russian-lang/dict/all', { headers: { 'Accept': 'application/json' } }).then((res) => res.ok ? res.json() : null).then((data) => {if (!data) return
553
488
  const allDicts = Object.assign({}, data.core || {}, data.plugins || {}, data)
554
489
  if (data.zhRu && typeof ZH_RU === 'object') {Object.assign(ZH_RU, data.zhRu)
555
490
  if (typeof updateZhRu === 'function') updateZhRu(data.zhRu)
556
491
  }
557
492
  registerDictMap(allDicts)
558
493
  if (typeof syncZhDom === 'function') syncZhDom()
559
- })
560
- .catch(() => {})
561
- }
562
- })
563
- const origTranslate = runtime.translate.bind(runtime)
494
+ }).catch(() => {})
495
+ }})
564
496
  const getOverrides = () => {try {const value = scope.getSnapshot().value
565
- return value && value.overrides ? value.overrides : {}
566
- } catch (err) { return {} }
567
- }
497
+ return value && value.overrides ? value.overrides : {}} catch (err) { return {} }}
568
498
  const getPluginLocalizationStatus = makePluginLocalizationStatus(RU)
569
- try {runtime.formatNumber = formatNumber
570
- runtime.formatCompactNumber = formatCompactNumber
571
- runtime.formatDate = formatDate
572
- runtime.formatTokens = formatTokens
573
- runtime.formatRelativeTime = formatRelativeTime
574
- runtime.formatCurrency = formatCurrency
575
- runtime.inflect = inflect
576
- runtime.getPluginLocalizationStatus = getPluginLocalizationStatus
577
- runtime.stemRussian = stemRussian
578
- runtime.fuzzyMatchRu = fuzzyMatchRu
579
- runtime.humanizeError = humanizeError
580
- runtime.plural = plural
581
- runtime.pluralForm = pluralForm
582
- } catch (err) { }
583
499
  const lookupChain = () => {try {const chain = runtime.fallbackChain && runtime.fallbackChain(runtime.getLocale().active)
584
500
  if (Array.isArray(chain) && chain.length) return chain
585
501
  } catch (err) { }
586
502
  return [runtime.getLocale().active]
587
503
  }
588
504
  const translationRegistry = new Map()
589
- const lookup = (ns, key) => {const chain = lookupChain()
590
- return runtime.lookup.length >= 3 ? runtime.lookup(ns, key, chain) : runtime.lookup(ns, key)
591
- }
505
+ const lookup = (ns, key) => (runtime.lookup.length >= 3 ? runtime.lookup(ns, key, lookupChain()) : runtime.lookup(ns, key))
506
+ const syncLang = () => {try {if (typeof document !== 'undefined' && document.documentElement && runtime.getLocale().active === 'ru') document.documentElement.lang = 'ru-RU'
507
+ } catch (err) { }}
508
+ const RUNTIME_PROP_NAMES = ['plural','pluralForm','formatDate','formatCompactNumber','formatTokens','formatNumber','formatRelativeTime','formatCurrency','inflect','getPluginLocalizationStatus','stemRussian','fuzzyMatchRu','humanizeError']
509
+ ctx.effect(() => {const origTranslate = runtime.translate
510
+ const origProps = {}
511
+ for (const p of RUNTIME_PROP_NAMES) origProps[p] = runtime[p]
512
+ runtime.plural = plural
513
+ runtime.pluralForm = pluralForm
514
+ runtime.formatDate = formatDate
515
+ runtime.formatCompactNumber = formatCompactNumber
516
+ runtime.formatTokens = formatTokens
517
+ Object.assign(runtime, { formatNumber, formatRelativeTime, formatCurrency, inflect, getPluginLocalizationStatus, stemRussian, fuzzyMatchRu, humanizeError })
518
+ const boundOrig = typeof origTranslate === 'function' ? origTranslate.bind(runtime) : (() => '')
592
519
  runtime.translate = function (ns, key, params) {if (ns && !RU[ns] && !loadedPluginNames.has(ns)) schedulePluginLoad(ns)
593
- const overrides = getOverrides()
594
- if (overrides[key] !== undefined) {return params ? fill(overrides[key], params) : overrides[key]
595
- }
520
+ const ov = getOverrides()
521
+ if (ov[key] !== undefined) return params ? fill(ov[key], params) : ov[key]
596
522
  if (runtime.getLocale().active === 'ru' && params) {const n = params.n ?? params.count
597
523
  if (typeof n === 'number') {const form = pluralForm(n)
598
524
  const m = /^(.*)[.](one|other)$/.exec(key)
599
- if (m) {if (form === 'few' || form === 'many') {const pluralKey = m[1] + '.' + form
600
- const template = lookup(ns, pluralKey) ?? lookup('common', pluralKey)
601
- if (template !== undefined) {return fill(template, params)
602
- }
603
- }
604
- } else if (form !== 'other' && !/[.](one|other|few|many)$/.test(key)) {const pluralKey = key + '.' + form
605
- const template = lookup(ns, pluralKey) ?? lookup('common', pluralKey)
606
- if (template !== undefined) {return fill(template, params)
607
- }
608
- }
609
- }
610
- }
611
- const res = origTranslate(ns, key, params)
612
- if (typeof res === 'string' && res && res.length < 300) {translationRegistry.set(res.trim(), { ns, key, params, value: res })
613
- }
525
+ if (m) {if (form === 'few' || form === 'many') {const tmpl = lookup(ns, m[1] + '.' + form) ?? lookup('common', m[1] + '.' + form)
526
+ if (tmpl !== undefined) return fill(tmpl, params)
527
+ }} else if (form !== 'other' && !/[.](one|other|few|many)$/.test(key)) {const tmpl = lookup(ns, key + '.' + form) ?? lookup('common', key + '.' + form)
528
+ if (tmpl !== undefined) return fill(tmpl, params)
529
+ }}}
530
+ const res = boundOrig(ns, key, params)
531
+ if (typeof res === 'string' && res && res.length < 300) translationRegistry.set(res.trim(), { ns, key, params, value: res })
614
532
  return res
615
533
  }
616
- const syncLang = () => {try {if (typeof document !== 'undefined' && document.documentElement
617
- && runtime.getLocale().active === 'ru') {document.documentElement.lang = 'ru-RU'
618
- }
619
- } catch (err) { }
620
- }
621
- const native = runtime.getLocale().locales.some((l) => l.id === 'ru')
622
- if (!native) {runtime.addLanguage({ id: 'ru', label: 'Русский', fallback: 'en' })
534
+ let removeLang = null
535
+ if (!runtime.getLocale().locales.some((l) => l.id === 'ru') && typeof runtime.addLanguage === 'function') {removeLang = runtime.addLanguage({ id: 'ru', label: 'Русский', fallback: 'en' })
623
536
  syncLang()
624
537
  }
538
+ return () => {runtime.translate = origTranslate
539
+ for (const p of RUNTIME_PROP_NAMES) {if (origProps[p] !== undefined) runtime[p] = origProps[p]
540
+ else delete runtime[p]
541
+ }
542
+ if (typeof removeLang === 'function') { try { removeLang() } catch (_) { } }}}, 'dsh-russian-lang: runtime-lifecycle')
625
543
  const syncFlag = () => {try {const wantRu = runtime.getLocale().active === 'ru'
626
544
  const value = scope.getSnapshot().value || {}
627
545
  if (!!value.enabled !== wantRu) {const r = scope.set('enabled', wantRu)
628
546
  if (r && typeof r.catch === 'function') {r.catch((err) => {console.warn('dsh-russian-lang: scope.set enabled failed', err && err.message || err)
629
547
  })
630
- }
631
- }
632
- } catch (err) {}
633
- }
634
- ctx.effect(() => {try { return runtime.subscribe(syncFlag) }
635
- catch (err) { return undefined }
636
- }, 'dsh-russian-lang: sync-flag')
548
+ }}} catch (err) {}}
549
+ ctx.effect(() => {try { return runtime.subscribe(syncFlag) } catch (err) { return undefined }}, 'dsh-russian-lang: sync-flag')
637
550
  const activate = () => {try {if (runtime.getLocale().active === 'ru') return
638
551
  runtime.setLocale('ru')
639
- } catch (err) { console.warn('dsh-russian-lang: activate failed', err) }
640
- }
552
+ } catch (err) { console.warn('dsh-russian-lang: activate failed', err) }}
641
553
  let booted = false
642
554
  const tryBoot = () => {if (booted) return
643
555
  try {const value = scope.getSnapshot().value
644
- if (value && value.enabled === true) { booted = true; activate() }
645
- } catch (err) { }
646
- }
556
+ if (value && value.enabled === true) { booted = true; activate() }} catch (err) { }}
647
557
  ctx.effect(() => scope.subscribe(tryBoot), 'dsh-russian-lang: boot')
648
558
  tryBoot()
649
559
  const unsubscribeLang = runtime.subscribe(syncLang)
@@ -654,9 +564,7 @@ const SPELL_WAS = 'data-russian-lang-spell-was'
654
564
  const LANG_WAS = 'data-russian-lang-lang-was'
655
565
  const EDITABLE = 'textarea, input[type=text], input[type=search], [contenteditable=""], [contenteditable="true"]'
656
566
  const MONO_RE = /mono|consol|courier/i
657
- const isMonoField = (el) => {try { return MONO_RE.test(getComputedStyle(el).fontFamily || '') }
658
- catch (err) { return false }
659
- }
567
+ const isMonoField = (el) => {try { return MONO_RE.test(getComputedStyle(el).fontFamily || '') } catch (err) { return false }}
660
568
  const spellOn = (el) => {if (el.hasAttribute(SPELL_ON) || isMonoField(el)) return
661
569
  el.setAttribute(SPELL_ON, '1')
662
570
  el.setAttribute(SPELL_WAS, el.getAttribute('spellcheck') ?? '')
@@ -687,18 +595,13 @@ if (spellObserver) return
687
595
  spellObserver = new MutationObserver((records) => {for (const record of records) {for (const node of record.addedNodes) {if (node.nodeType !== 1) continue
688
596
  if (node.matches(EDITABLE)) spellOn(node)
689
597
  node.querySelectorAll ? node.querySelectorAll(EDITABLE).forEach(spellOn) : null
690
- }
691
- }
692
- })
598
+ }}})
693
599
  spellObserver.observe(document.body, { childList: true, subtree: true })
694
- } catch (err) { }
695
- }
600
+ } catch (err) { }}
696
601
  const unsubscribeSpell = runtime.subscribe(syncSpell)
697
602
  ctx.effect(() => {return () => {unsubscribeSpell()
698
603
  if (spellObserver) spellObserver.disconnect()
699
- try { document.querySelectorAll('[' + SPELL_ON + ']').forEach(spellOff) } catch (err) { }
700
- }
701
- }, 'dsh-russian-lang: spellcheck')
604
+ try { document.querySelectorAll('[' + SPELL_ON + ']').forEach(spellOff) } catch (err) { }}}, 'dsh-russian-lang: spellcheck')
702
605
  syncSpell()
703
606
  const ZH_CJK = /[\u3400-\u9fff\uf900-\ufaff]/
704
607
  const ZH_RE_ESC = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
@@ -719,8 +622,7 @@ if (/\{[a-zA-Z_]\w*\}/.test(zhText)) {const parts = zhText.split(/\{[a-zA-Z_]\w*
719
622
  if (parts.some((p) => p.length === 0)) continue
720
623
  ZH_PATTERNS.push({ re: new RegExp(parts.map(ZH_RE_ESC).join('([\\s\\S]*?)')), ruParts: ruText.split(/\{[a-zA-Z_]\w*\}/g) })
721
624
  } else {ZH_EXACT.set(zhText, ruText)
722
- }
723
- }
625
+ }}
724
626
  ZH_PATTERNS.sort((a, b) => b.re.source.length - a.re.source.length)
725
627
  rebuildZhSorted()
726
628
  }
@@ -739,14 +641,12 @@ for (const p of ZH_PATTERNS) {const m = p.re.exec(trimmed)
739
641
  if (m && m[0] === trimmed) {let out = p.ruParts[0]
740
642
  for (let i = 1; i < p.ruParts.length; i++) out += m[i] + p.ruParts[i]
741
643
  return text.replace(trimmed, out)
742
- }
743
- }
644
+ }}
744
645
  let replaced = text
745
646
  let changed = false
746
647
  for (const [zhPhrase, ruPhrase] of zhSortedExact) {if (zhPhrase.length >= 2 && replaced.includes(zhPhrase)) {replaced = replaced.split(zhPhrase).join(ruPhrase)
747
648
  changed = true
748
- }
749
- }
649
+ }}
750
650
  if (changed) return replaced
751
651
  return null
752
652
  }
@@ -765,9 +665,7 @@ continue
765
665
  if (trimmed.length >= 1 && ZH_CJK.test(val)) {hits.push(node)
766
666
  } else if (trimmed.length >= 1 && DOM_EN_TEXT[trimmed]) {const p = node.parentElement
767
667
  if (p && !p.closest('code, pre, script, style, textarea, input, select, kbd, samp, [contenteditable="true"], [data-composer-input], [role="textbox"]')) {node.nodeValue = val.replace(trimmed, DOM_EN_TEXT[trimmed])
768
- }
769
- }
770
- }
668
+ }}}
771
669
  for (const node of hits) {const next = zhTranslateText(node.nodeValue)
772
670
  if (next && next !== node.nodeValue) node.nodeValue = next
773
671
  }
@@ -777,19 +675,12 @@ if (DOM_EN_TEXT[tr]) {el.setAttribute(attr, v.replace(tr, DOM_EN_TEXT[tr]))
777
675
  } else if (DOM_EN_ATTRS[tr]) {el.setAttribute(attr, v.replace(tr, DOM_EN_ATTRS[tr]))
778
676
  } else if (ZH_CJK.test(v)) {const next = zhTranslateText(v)
779
677
  if (next) el.setAttribute(attr, next)
780
- }
781
- }
782
- }
783
- }
678
+ }}}}
784
679
  if (root.getAttribute) {for (const attr of ['title', 'placeholder', 'aria-label']) {const v = root.getAttribute(attr)
785
680
  if (v) {const tr = v.trim()
786
681
  if (DOM_EN_TEXT[tr]) root.setAttribute(attr, v.replace(tr, DOM_EN_TEXT[tr]))
787
682
  else if (DOM_EN_ATTRS[tr]) root.setAttribute(attr, v.replace(tr, DOM_EN_ATTRS[tr]))
788
- }
789
- }
790
- }
791
- } catch (err) { }
792
- }
683
+ }}}} catch (err) { }}
793
684
  let zhObserver = null
794
685
  let zhDebounceTimer = null
795
686
  let zhWalking = false
@@ -799,8 +690,7 @@ if (zhWalking) return
799
690
  zhWalking = true
800
691
  try {ZH_WALKER(target || document.body)
801
692
  } finally {zhWalking = false
802
- }
803
- }, 120)
693
+ }}, 120)
804
694
  }
805
695
  const syncZhDom = () => {try {if (typeof document === 'undefined') return
806
696
  const ru = runtime.getLocale().active === 'ru'
@@ -811,30 +701,21 @@ return
811
701
  if (!zhObserver) {zhObserver = new MutationObserver((records) => {for (const record of records) {if (record.type === 'childList') {for (const node of record.addedNodes) {if (node.nodeType === 1) {if (node.closest && node.closest('.chat-message, .markdown-body, pre, code, [data-stream]')) continue
812
702
  queueZhWalk(node)
813
703
  return
814
- }
815
- }
816
- }
817
- }
818
- })
704
+ }}}}})
819
705
  zhObserver.observe(document.body, { childList: true, subtree: true })
820
706
  }
821
707
  queueZhWalk(document.body)
822
- } catch (err) { }
823
- }
708
+ } catch (err) { }}
824
709
  const unsubscribeZh = runtime.subscribe(syncZhDom)
825
710
  ctx.effect(() => {return () => {unsubscribeZh()
826
711
  if (zhObserver) zhObserver.disconnect()
827
- if (zhDebounceTimer) { clearTimeout(zhDebounceTimer); zhDebounceTimer = null }
828
- }
829
- }, 'dsh-russian-lang: zh-dom')
712
+ if (zhDebounceTimer) { clearTimeout(zhDebounceTimer); zhDebounceTimer = null }}}, 'dsh-russian-lang: zh-dom')
830
713
  syncZhDom()
831
714
  const TYPO_YO_PAIRS = [["еще","ещё"],["ее","её"],["черный","чёрный"],["черная","чёрная"],["черные","чёрные"],["зеленый","зелёный"],["желтый","жёлтый"],["легкий","лёгкий"],["тяжелый","тяжёлый"],["надежный","надёжный"],["дешевый","дешёвый"],["идет","идёт"],["дает","даёт"],["ведет","ведёт"],["несет","несёт"],["живет","живёт"],["привел","привёл"],["шел","шёл"],["нее","неё"],["мое","моё"],["серьезно","серьёзно"],["придется","придётся"],["насчет","насчёт"],["твое","твоё"],["свое","своё"],["пойдем","пойдём"],["пришел","пришёл"],["нашел","нашёл"],["идем","идём"],["вперед","вперёд"],["пошел","пошёл"],["ребенка","ребёнка"],["ребенок","ребёнок"],["счет","счёт"],["своем","своём"],["придет","придёт"],["ушел","ушёл"],["ждет","ждёт"],["мертв","мёртв"],["твоем","твоём"],["вернется","вернётся"],["днем","днём"],["найдем","найдём"],["пойдет","пойдёт"],["начнем","начнём"],["вернемся","вернёмся"],["остается","остаётся"],["принес","принёс"],["идешь","идёшь"],["трех","трёх"],["ребенком","ребёнком"],["самолет","самолёт"],["определенно","определённо"],["пойдешь","пойдёшь"],["умрет","умрёт"],["прием","приём"],["прошел","прошёл"],["убьет","убьёт"],["тетя","тётя"],["провел","провёл"],["произойдет","произойдёт"],["семьей","семьёй"],["пойдемте","пойдёмте"],["пройдет","пройдёт"],["возьмем","возьмём"],["вернешься","вернёшься"],["найдешь","найдёшь"],["живешь","живёшь"],["отчет","отчёт"],["займет","займёт"],["ждем","ждём"],["сошел","сошёл"],["вдвоем","вдвоём"],["найдет","найдёт"],["вел","вёл"],["живем","живём"],["зашел","зашёл"],["начнется","начнётся"],["путем","путём"],["подойдет","подойдёт"],["режиссер","режиссёр"],["уйдет","уйдёт"],["придешь","придёшь"],["идемте","идёмте"],["ведешь","ведёшь"],["пес","пёс"],["умрешь","умрёшь"],["ждешь","ждёшь"],["найдете","найдёте"],["найдется","найдётся"],["клево","клёво"],["поймешь","поймёшь"],["разберемся","разберёмся"],["времен","времён"],["введен","введён"],["включен","включён"],["включенных","включённых"],["возьмется","возьмётся"],["завершен","завершён"],["завершенного","завершённого"],["задает","задаёт"],["задается","задаётся"],["заменен","заменён"],["звезд","звёзд"],["звезды","звёзды"],["изменен","изменён"],["истек","истёк"],["незавершенная","незавершённая"],["неподтвержденные","неподтверждённые"],["несохраненные","несохранённые"],["обновлен","обновлён"],["обновленные","обновлённые"],["определен","определён"],["отклонен","отклонён"],["отменен","отменён"],["очередность","очерёдность"],["поврежден","повреждён"],["повторен","повторён"],["подключен","подключён"],["подтвержденные","подтверждённые"],["приемки","приёмки"],["раздает","раздаёт"],["разрешенная","разрешённая"],["разрешенные","разрешённые"],["создаем","создаём"],["сойдется","сойдётся"],["сохранен","сохранён"],["сохраненный","сохранённый"],["счету","счёту"],["тяжелые","тяжёлые"],["темная","тёмная"],["уберет","уберёт"],["удален","удалён"],["учетные","учётные"],["учетных","учётных"],["емкости","ёмкости"],["емкость","ёмкость"]]
832
715
  const typoYo = makeTypoYo(TYPO_YO_PAIRS)
833
716
  const getTypoConf = () => {try {const t = scope.getSnapshot().value && scope.getSnapshot().value.typography
834
717
  if (!t || t.enabled === false) return null
835
- return { yo: t.yo === true }
836
- } catch (err) { return null }
837
- }
718
+ return { yo: t.yo === true }} catch (err) { return null }}
838
719
  const typoNode = (node, conf) => {const before = node.nodeValue
839
720
  if (!before || !before.match || (before.match(/[\u0400-\u04FF]/g) || []).length < 3) return
840
721
  if (node.parentElement && node.parentElement.closest('code, pre, a, script, style, textarea, input, select, button, kbd, samp, [contenteditable="true"], [data-composer-input], [role="textbox"]')) return
@@ -845,9 +726,7 @@ if (node.parentElement && node.parentElement.closest('[data-chat-flow-status="ru
845
726
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
846
727
  if (sel && !sel.isCollapsed && sel.rangeCount > 0) {try {const range = sel.getRangeAt(0)
847
728
  if (range.intersectsNode ? range.intersectsNode(node) : (sel.containsNode && sel.containsNode(node, true))) {return
848
- }
849
- } catch (e) { void e; }
850
- }
729
+ }} catch (e) { void e; }}
851
730
  let after = typoQuotes(before)
852
731
  after = typoDash(after)
853
732
  after = typoPunct(after)
@@ -867,8 +746,7 @@ for (const root of typoPending) {if (root.nodeType === 3) typoNode(root, conf)
867
746
  else typoWalk(root, conf)
868
747
  }
869
748
  typoPending.clear()
870
- } catch (err) { }
871
- }
749
+ } catch (err) { }}
872
750
  const typoPending = new Set()
873
751
  const queueTypo = (roots) => {for (const r of roots) typoPending.add(r)
874
752
  if (!typoQueued) typoQueued = requestAnimationFrame(flushTypo)
@@ -881,19 +759,15 @@ if (typoObserver) return
881
759
  typoWalk(document.body, getTypoConf())
882
760
  typoObserver = new MutationObserver((records) => {const roots = []
883
761
  for (const record of records) {if (record.type === 'childList') {for (const added of record.addedNodes) {if (added.nodeType === 3 || added.nodeType === 1) roots.push(added)
884
- }
885
- }
886
- }
762
+ }}}
887
763
  if (roots.length > 0) queueTypo(roots)
888
764
  })
889
765
  typoObserver.observe(document.body, { childList: true, subtree: true })
890
- } catch (err) { }
891
- }
766
+ } catch (err) { }}
892
767
  const unsubscribeTypo = runtime.subscribe(syncTypo)
893
768
  ctx.effect(() => {return () => {unsubscribeTypo()
894
769
  if (typoObserver) typoObserver.disconnect()
895
- }
896
- }, 'dsh-russian-lang: typography')
770
+ }}, 'dsh-russian-lang: typography')
897
771
  syncTypo()
898
772
  const layout = makeLayout(new Set(["не","что","ты","это","на","он","мы","как","вы","да","мне","нет","меня","так","но","его","все","она","тебя","если","за","бы","тебе","они","чтобы","же","есть","просто","из","для"]), new Set())
899
773
  const layoutFixCandidate = layout.candidate
@@ -910,16 +784,13 @@ if (el._valueTracker) {el._valueTracker.setValue(value === '' ? '__force__' : ''
910
784
  }
911
785
  try {const propsKey = Object.keys(el).find((k) => k.startsWith('__reactProps$') || k.startsWith('__reactEventHandlers$'))
912
786
  if (propsKey && el[propsKey] && typeof el[propsKey].onChange === 'function') {el[propsKey].onChange({ target: el, currentTarget: el })
913
- }
914
- } catch (e) { void e; }
787
+ }} catch (e) { void e; }
915
788
  try {el.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }))
916
789
  } catch (e) {el.dispatchEvent(new Event('input', { bubbles: true }))
917
790
  }
918
791
  el.dispatchEvent(new Event('change', { bubbles: true }))
919
792
  if (typeof cursorStart === 'number' && typeof el.setSelectionRange === 'function') {const end = typeof cursorEnd === 'number' ? cursorEnd : cursorStart
920
- try { el.setSelectionRange(cursorStart, end) } catch (e) { void e; }
921
- }
922
- }
793
+ try { el.setSelectionRange(cursorStart, end) } catch (e) { void e; }}}
923
794
  let layoutHintEl = null
924
795
  const layoutCurrentInput = (ev) => {if (ev && ev.target) {const t = ev.target
925
796
  if (t.tagName === 'TEXTAREA' || (t.tagName === 'INPUT' && (t.type === 'text' || !t.type))) {return t
@@ -947,8 +818,7 @@ preCaretRange.selectNodeContents(root)
947
818
  preCaretRange.setEnd(range.startContainer, range.startOffset)
948
819
  return preCaretRange.toString().length
949
820
  } catch (e) {return -1
950
- }
951
- }
821
+ }}
952
822
  function setCaretCharacterOffset(root, offset) {if (!root || offset < 0) return
953
823
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
954
824
  if (!sel) return
@@ -970,9 +840,7 @@ range.setStart(targetNode, targetOffset)
970
840
  range.collapse(true)
971
841
  sel.removeAllRanges()
972
842
  sel.addRange(range)
973
- }
974
- } catch (e) { void e; }
975
- }
843
+ }} catch (e) { void e; }}
976
844
  const isCaretInCode = (el, value, caretOffset) => {if (!el) return false
977
845
  if (caretOffset == null || caretOffset < 0) caretOffset = (value || '').length
978
846
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
@@ -1009,10 +877,8 @@ sel.addRange(range)
1009
877
  document.execCommand('insertText', false, value)
1010
878
  const targetOffset = typeof cursorStart === 'number' ? cursorStart : (initialOffset >= 0 ? Math.min(initialOffset, value.length) : value.length)
1011
879
  setCaretCharacterOffset(host, targetOffset)
1012
- } catch (e) { void e; }
1013
- }
1014
- const layoutDismiss = () => {if (layoutHintEl) { layoutHintEl.remove(); layoutHintEl = null }
1015
- }
880
+ } catch (e) { void e; }}
881
+ const layoutDismiss = () => {if (layoutHintEl) { layoutHintEl.remove(); layoutHintEl = null }}
1016
882
  const layoutShowHint = (inputEl, converted, direction) => {layoutDismiss()
1017
883
  layoutHintEl = document.createElement('div')
1018
884
  layoutHintEl.dataset.russianLangLayout = '1'
@@ -1049,8 +915,7 @@ const v = getComposerText(el)
1049
915
  const c = layoutFixCandidate(v, 'lat2cyr') || layoutFixCandidate(v, 'cyr2lat')
1050
916
  if (c) {setComposerText(el, c.converted)
1051
917
  learnWords(c.converted)
1052
- }
1053
- })
918
+ }})
1054
919
  document.body.appendChild(layoutBadgeEl)
1055
920
  }
1056
921
  layoutBadgeEl.textContent = label
@@ -1058,8 +923,7 @@ const r = el.getBoundingClientRect()
1058
923
  layoutBadgeEl.style.left = (r.right - 36) + 'px'
1059
924
  layoutBadgeEl.style.top = (r.top - 22) + 'px'
1060
925
  }
1061
- const layoutBadgeHide = () => {if (layoutBadgeEl) { layoutBadgeEl.remove(); layoutBadgeEl = null }
1062
- }
926
+ const layoutBadgeHide = () => {if (layoutBadgeEl) { layoutBadgeEl.remove(); layoutBadgeEl = null }}
1063
927
  let isFormatting = false
1064
928
  const layoutOnInput = (ev) => {if (isFormatting) return
1065
929
  try {const el = layoutCurrentInput(ev)
@@ -1070,14 +934,11 @@ if (value.trim().length < 4) { layoutDismiss(); return }
1070
934
  const latCount = (value.match(/[a-z]/g) || []).length
1071
935
  const cyrCount = (value.match(/[\u0430-\u044f\u0451]/g) || []).length
1072
936
  if (latCount > cyrCount && cyrCount === 0) {const c = layoutFixCandidate(value, 'lat2cyr')
1073
- if (c) { layoutShowHint(el, c.converted, 'ru'); return }
1074
- }
937
+ if (c) { layoutShowHint(el, c.converted, 'ru'); return }}
1075
938
  if (cyrCount > 0 && latCount === 0 && value.trim().startsWith('/')) {const c = layoutFixCandidate(value, 'cyr2lat')
1076
- if (c) { layoutShowHint(el, c.converted, 'cmd'); return }
1077
- }
939
+ if (c) { layoutShowHint(el, c.converted, 'cmd'); return }}
1078
940
  layoutDismiss()
1079
- } catch (err) { }
1080
- }
941
+ } catch (err) { }}
1081
942
  const unsubscribeLayout = runtime.subscribe(layoutOnInput)
1082
943
  const layoutOnKeydown = (ev) => {const el = layoutCurrentInput(ev)
1083
944
  if (!el) return
@@ -1098,8 +959,7 @@ learnWords(c.converted)
1098
959
  } finally {isFormatting = false
1099
960
  }
1100
961
  return
1101
- }
1102
- }
962
+ }}
1103
963
  const c = layoutFixCandidate(value, 'lat2cyr') || layoutFixCandidate(value, 'cyr2lat')
1104
964
  if (c) {ev.preventDefault()
1105
965
  isFormatting = true
@@ -1112,8 +972,7 @@ setComposerText(el, c.converted, offset, offset)
1112
972
  }
1113
973
  learnWords(c.converted)
1114
974
  } finally {isFormatting = false
1115
- }
1116
- }
975
+ }}
1117
976
  return
1118
977
  }
1119
978
  if (ev.key === ' ' || ev.key === 'Enter') {if (value && value.startsWith('/') && typeof expandSlashAlias === 'function') {const expanded = expandSlashAlias(value)
@@ -1127,20 +986,14 @@ return
1127
986
  } else if (ev.key === 'Enter') {isFormatting = true
1128
987
  try {setComposerText(el, expanded, expanded.length, expanded.length)
1129
988
  } finally {isFormatting = false
1130
- }
1131
- }
1132
- }
1133
- }
1134
- }
989
+ }}}}}
1135
990
  if (ev.key === 'Enter' && !ev.shiftKey && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {const snapVal = scope ? (scope.getSnapshot().value || {}) : {}
1136
- const typoLive = snapVal.typography ? snapVal.typography.liveInput !== false : true
991
+ const typoLive = snapVal.typography ? snapVal.typography.liveInput === true : false
1137
992
  if (typoLive && typeof formatInputLive === 'function' && (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT')) {const formatted = formatInputLive(value)
1138
993
  if (formatted && formatted !== value) {setNativeInputValue(el, formatted, formatted.length, formatted.length)
1139
- }
1140
- }
1141
- }
994
+ }}}
1142
995
  try {const snapVal = scope ? (scope.getSnapshot().value || {}) : {}
1143
- const typoLive = snapVal.typography ? snapVal.typography.liveInput !== false : true
996
+ const typoLive = snapVal.typography ? snapVal.typography.liveInput === true : false
1144
997
  if (typoLive && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {const isTextarea = el.tagName === 'TEXTAREA' || el.tagName === 'INPUT'
1145
998
  if (isTextarea) {const caretPos = el.selectionStart || 0
1146
999
  if (!isCaretInCode(el, value, caretPos)) {const textBefore = caretPos >= 0 ? value.slice(0, caretPos) : value
@@ -1162,12 +1015,7 @@ setNativeInputValue(el, nextVal, caretPos + 1, caretPos + 1)
1162
1015
  } finally {isFormatting = false
1163
1016
  }
1164
1017
  return
1165
- }
1166
- }
1167
- }
1168
- }
1169
- } catch (e) { void e; }
1170
- }
1018
+ }}}}} catch (e) { void e; }}
1171
1019
  const getHoverBox = () => {if (typeof document === 'undefined') return null
1172
1020
  if (!hoverBoxEl) {hoverBoxEl = document.createElement('div')
1173
1021
  hoverBoxEl.id = 'dsh-ru-inspector-hover'
@@ -1262,17 +1110,15 @@ const copyKeyBtn = modal.querySelector('.rl-copy-key-btn')
1262
1110
  if (copyKeyBtn) {copyKeyBtn.onclick = () => {try { navigator.clipboard.writeText(k || rawText) } catch (err) { void err; }
1263
1111
  copyKeyBtn.textContent = '✓'
1264
1112
  setTimeout(() => { copyKeyBtn.textContent = '📋' }, 1500)
1265
- }
1266
- }
1113
+ }}
1267
1114
  const copyReportBtn = modal.querySelector('.rl-copy-report-btn')
1268
1115
  if (copyReportBtn) {copyReportBtn.onclick = () => {const snippet = typeof generateBugReportSnippet === 'function'
1269
- ? generateBugReportSnippet({ key: k, ns, original: origText || rawText, current: rawText, override: textarea.value.trim(), pkgVersion: '0.3.8' })
1116
+ ? generateBugReportSnippet({ key: k, ns, original: origText || rawText, current: rawText, override: textarea.value.trim(), pkgVersion: '0.3.10' })
1270
1117
  : ('### Репорт: ' + (k || rawText) + '\n- Исходный: ' + (origText || rawText) + '\n- Перевод: ' + textarea.value.trim())
1271
1118
  try { navigator.clipboard.writeText(snippet) } catch (err) { void err; }
1272
1119
  copyReportBtn.textContent = '✓ Скопировано'
1273
1120
  setTimeout(() => { copyReportBtn.textContent = '📋 Скопировать репорт' }, 2000)
1274
- }
1275
- }
1121
+ }}
1276
1122
  const resetBtn = modal.querySelector('.rl-reset-override-btn')
1277
1123
  if (resetBtn) {resetBtn.onclick = () => {const targetKey = k || rawText
1278
1124
  const cur = getOverrides()
@@ -1281,8 +1127,7 @@ delete updated[targetKey]
1281
1127
  try { scope.set('overrides', updated) } catch (err) { void err; }
1282
1128
  modal.remove()
1283
1129
  if (typeof queueZhWalk === 'function') queueZhWalk()
1284
- }
1285
- }
1130
+ }}
1286
1131
  const saveBtn = modal.querySelector('.rl-save-btn')
1287
1132
  saveBtn.onclick = () => {const val = textarea.value.trim()
1288
1133
  const targetKey = k || rawText
@@ -1292,9 +1137,7 @@ const updated = Object.assign({}, cur, { [targetKey]: val })
1292
1137
  try { scope.set('overrides', updated) } catch (_e) { void _e }
1293
1138
  if (targetElement) {try {if (targetElement.childNodes && targetElement.childNodes.length === 1 && targetElement.childNodes[0].nodeType === 3) {targetElement.childNodes[0].nodeValue = val
1294
1139
  } else {targetElement.textContent = val
1295
- }
1296
- } catch (err) { void err; }
1297
- }
1140
+ }} catch (err) { void err; }}
1298
1141
  saveBtn.textContent = '✓ Применено'
1299
1142
  saveBtn.disabled = true
1300
1143
  if (typeof queueZhWalk === 'function') queueZhWalk()
@@ -1339,17 +1182,14 @@ else if (meta.source === 'untranslated_zh') title = '🔴 ZH'
1339
1182
  else if (meta.source === 'untranslated_en') title = '🔴 EN'
1340
1183
  }
1341
1184
  badge.textContent = title
1342
- }
1343
- }
1185
+ }}
1344
1186
  const inspectorOnKeyDown = (e) => {if (e.altKey && (e.key === 'i' || e.key === 'I' || e.code === 'KeyI')) {e.preventDefault()
1345
1187
  isInspectorActive = !isInspectorActive
1346
1188
  updateInspectorPill()
1347
1189
  if (!isInspectorActive && hoverBoxEl) hoverBoxEl.style.display = 'none'
1348
- }
1349
- }
1190
+ }}
1350
1191
  const inspectorOnKeyUp = (e) => {if (e.key === 'Alt' && !isInspectorActive) {if (hoverBoxEl) hoverBoxEl.style.display = 'none'
1351
- }
1352
- }
1192
+ }}
1353
1193
  ctx.effect(() => {document.addEventListener('click', inspectorOnClick, true)
1354
1194
  document.addEventListener('mousemove', inspectorOnMouseMove, true)
1355
1195
  document.addEventListener('keydown', inspectorOnKeyDown, true)
@@ -1361,9 +1201,7 @@ document.removeEventListener('keyup', inspectorOnKeyUp, true)
1361
1201
  const m = document.getElementById('dsh-ru-inspector-modal')
1362
1202
  if (m) m.remove()
1363
1203
  if (hoverBoxEl) { hoverBoxEl.remove(); hoverBoxEl = null }
1364
- if (pillEl) { pillEl.remove(); pillEl = null }
1365
- }
1366
- }, 'dsh-russian-lang: inspector')
1204
+ if (pillEl) { pillEl.remove(); pillEl = null }}}, 'dsh-russian-lang: inspector')
1367
1205
  runtime.findTranslationKey = (text) => (typeof findTranslationKey === 'function' ? findTranslationKey(text, RU, getOverrides(), ZH_RU) : null)
1368
1206
  runtime.openInspector = openInspectorModal
1369
1207
  runtime.translationRegistry = translationRegistry
@@ -1376,16 +1214,10 @@ if (node.matches && (node.matches('.dsw-toast, [role="alert"], .toast, .ant-mess
1376
1214
  if (/failed to fetch|network error|econnrefused|etimedout/i.test(text)) {const h = typeof humanizeError === 'function' ? humanizeError(text) : null
1377
1215
  if (h && h.title) {const targetEl = node.querySelector('.toast-body, .ant-message-custom-content') || node
1378
1216
  targetEl.textContent = h.title + ': ' + h.message
1379
- }
1380
- }
1381
- }
1382
- }
1383
- }
1384
- }) : null
1217
+ }}}}}}) : null
1385
1218
  ctx.effect(() => {if (toastObserver && typeof document !== 'undefined' && document.body) {toastObserver.observe(document.body, { childList: true, subtree: true })
1386
1219
  }
1387
- return () => { if (toastObserver) toastObserver.disconnect() }
1388
- }, 'dsh-russian-lang: toast-humanizer')
1220
+ return () => { if (toastObserver) toastObserver.disconnect() }}, 'dsh-russian-lang: toast-humanizer')
1389
1221
  ctx.effect(() => {document.addEventListener('input', layoutOnInput, true)
1390
1222
  document.addEventListener('keydown', layoutOnKeydown, true)
1391
1223
  return () => {unsubscribeLayout()
@@ -1393,13 +1225,11 @@ document.removeEventListener('input', layoutOnInput, true)
1393
1225
  document.removeEventListener('keydown', layoutOnKeydown, true)
1394
1226
  layoutDismiss()
1395
1227
  layoutBadgeHide()
1396
- }
1397
- }, 'dsh-russian-lang: layout')
1228
+ }}, 'dsh-russian-lang: layout')
1398
1229
  if (!ctx.slots || !React) return
1399
1230
  const toggleRu = (wantRu) => {try {if (runtime.getLocale().active === wantRu) return
1400
1231
  runtime.setLocale(wantRu ? 'ru' : 'en')
1401
- } catch (err) { console.warn('dsh-russian-lang: toggle failed', err) }
1402
- }
1232
+ } catch (err) { console.warn('dsh-russian-lang: toggle failed', err) }}
1403
1233
  try {ctx.slots.inject('plugins.item', () =>
1404
1234
  ctx.slots.register({name: 'plugins.item',id: SETTINGS_NS_NAME,order: 60,label: () => 'Russian language',locale: SETTINGS_NS_NAME,inject: () => ({ scope, runtime, toggleRu }),}, SettingsCard),)
1405
1235
  ctx.slots.inject('plugins.row.config', () =>
@@ -1415,15 +1245,13 @@ ctx.slots.register({name: 'conversation.chat.assistant-actions',id: 'dsh-russian
1415
1245
  } catch (err) { }
1416
1246
  try {ctx.slots.inject('conversation.session.header.utilities', () =>
1417
1247
  ctx.slots.register({name: 'conversation.session.header.utilities',id: 'dsh-russian-lang-md-export',order: 101,locale: SETTINGS_NS_NAME,inject: () => ({ runtime }),}, ExportMarkdownButton),)
1418
- } catch (err) { }
1419
- }
1248
+ } catch (err) { }}
1420
1249
  function QuickLangSwitch(props) {const inj = typeof props.inject === 'function' ? (props.inject() || {}) : (props.inject || {})
1421
1250
  const runtime = props.runtime || inj.runtime
1422
1251
  const toggleRu = props.toggleRu || inj.toggleRu
1423
1252
  const [locale, setLocaleState] = React.useState(runtime ? (runtime.getLocale().active || 'en') : 'ru')
1424
1253
  React.useEffect(() => {if (!runtime) return
1425
- return runtime.subscribe(() => {try { setLocaleState(runtime.getLocale().active || 'en') } catch (e) { }
1426
- })
1254
+ return runtime.subscribe(() => {try { setLocaleState(runtime.getLocale().active || 'en') } catch (e) { }})
1427
1255
  }, [runtime])
1428
1256
  const isRu = locale === 'ru'
1429
1257
  return React.createElement('button', {type: 'button',className: 'rl-lang-chip' + (isRu ? ' rl-lang-chip-active' : ''),title: isRu ? 'Интерфейс: Русский (нажмите для переключения на EN)' : 'Interface: English (click for RU)',onClick: () => { if (toggleRu) toggleRu(!isRu) },}, React.createElement('span', { className: 'rl-lang-text' }, isRu ? 'RU' : 'EN'))
@@ -1445,8 +1273,7 @@ if (text) messages.push({ role: 'user', content: text })
1445
1273
  clone.querySelectorAll('button, svg, [class*="actions"], [class*="Actions"], .rl-turn-translation').forEach((b) => b.remove())
1446
1274
  const text = clone.innerText.trim()
1447
1275
  if (text) messages.push({ role: 'assistant', content: text })
1448
- }
1449
- })
1276
+ }})
1450
1277
  }
1451
1278
  if (messages.length === 0) {const allElements = Array.from(document.querySelectorAll('[class*="userRow"], [class*="UserRow"], [class*="assistant-step"], [class*="AssistantMarkdown"], .dsw-turn-node, [data-role]'))
1452
1279
  const seen = new Set()
@@ -1473,24 +1300,18 @@ a.download = safeTitle + '-' + new Date().toISOString().slice(0, 10) + '.md'
1473
1300
  document.body.appendChild(a)
1474
1301
  a.click()
1475
1302
  setDone(true)
1476
- setTimeout(() => {try { document.body.removeChild(a); URL.revokeObjectURL(url) } catch (e) { void e; }
1477
- }, 30000)
1303
+ setTimeout(() => {try { document.body.removeChild(a); URL.revokeObjectURL(url) } catch (e) { void e; }}, 30000)
1478
1304
  setTimeout(() => setDone(false), 2000)
1479
1305
  } catch (err) {console.warn('dsh-russian-lang: export md failed', err)
1480
- }
1481
- }
1306
+ }}
1482
1307
  return React.createElement('button', {type: 'button',className: 'rl-lang-chip rl-export-md-btn',title: 'Экспорт диалога в Markdown (.md)',onClick: onExport,}, React.createElement('span', { className: 'rl-lang-text' }, done ? '✓ MD' : '📥 MD'))
1483
1308
  }
1484
1309
  async function translateTurnContent(text) {if (!text || typeof text !== 'string') return { error: 'Текст для перевода пуст.' }
1485
- try {const res = await fetch('/api/dsh-russian-lang/translate', {headers: { 'content-type': 'application/json', 'x-dsh-translator': '1' },method: 'POST',headers: { 'content-type': 'application/json' },body: JSON.stringify({ text })
1310
+ try {const res = await fetch('/api/dsh-russian-lang/translate', {method: 'POST',headers: { 'content-type': 'application/json', 'x-dsh-translator': '1' },body: JSON.stringify({ text })
1486
1311
  })
1487
1312
  const data = await res.json().catch(() => ({}))
1488
- if (!res.ok) {return { error: data.error || ('Ошибка перевода (HTTP ' + res.status + ')') }
1489
- }
1490
- return { translatedText: data.translatedText || text }
1491
- } catch (err) {return { error: 'Не удалось связаться с хостом DSH: ' + (err.message || err) }
1492
- }
1493
- }
1313
+ if (!res.ok) {return { error: data.error || ('Ошибка перевода (HTTP ' + res.status + ')') }}
1314
+ return { translatedText: data.translatedText || text }} catch (err) {return { error: 'Не удалось связаться с хостом DSH: ' + (err.message || err) }}}
1494
1315
  function TranslateTurnAction(props) {const t = typeof props.t === 'function' ? props.t : ((k) => k)
1495
1316
  const [loading, setLoading] = React.useState(false)
1496
1317
  const [open, setOpen] = React.useState(false)
@@ -1525,8 +1346,7 @@ rawText = targetSteps.map((s) => {const clone = s.cloneNode(true)
1525
1346
  clone.querySelectorAll('button, svg, [class*="actions"], [class*="Actions"]').forEach((b) => b.remove())
1526
1347
  return clone.innerText.trim()
1527
1348
  }).filter(Boolean).join('\n\n')
1528
- }
1529
- }
1349
+ }}
1530
1350
  if (!rawText) {const prose = tailFlowItem.querySelector('.dsw-prose, [data-block-kind="text"], .dsw-markdown-view, p')
1531
1351
  if (prose) rawText = prose.innerText.trim()
1532
1352
  }
@@ -1566,8 +1386,7 @@ if (parentContainer) {parentContainer.insertBefore(box, tailFlowItem)
1566
1386
  setOpen(true)
1567
1387
  } catch (err) {console.warn('dsh-russian-lang: translate turn failed', err)
1568
1388
  } finally {setLoading(false)
1569
- }
1570
- },}, React.createElement('span', null, loading ? '...' : (open ? 'RU ✓' : 'RU ↗')))
1389
+ }},}, React.createElement('span', null, loading ? '...' : (open ? 'RU ✓' : 'RU ↗')))
1571
1390
  }
1572
1391
  function SettingsCard(props) {const inj = typeof props.inject === 'function'
1573
1392
  ? (props.inject() || {})
@@ -1577,70 +1396,50 @@ const runtime = props.runtime || inj.runtime
1577
1396
  const toggleRu = props.toggleRu || inj.toggleRu
1578
1397
  const t = typeof props.t === 'function' ? props.t : ((k) => k)
1579
1398
  const [open, setOpen] = React.useState(false)
1580
- const [snap, setSnap] = React.useState(
1581
- () => (scope && scope.getSnapshot ? scope.getSnapshot() : { status: 'loading', value: {} }))
1582
- const [ruActive, setRuActive] = React.useState(
1583
- () => { try { return runtime.getLocale().active === 'ru' } catch (e) { return false } })
1399
+ const [snap, setSnap] = React.useState(() => (scope && scope.getSnapshot ? scope.getSnapshot() : { status: 'loading', value: {} }))
1400
+ const [ruActive, setRuActive] = React.useState(() => { try { return runtime.getLocale().active === 'ru' } catch (e) { return false } })
1584
1401
  const [typo, setTypoState] = React.useState(() =>
1585
1402
  (snap.value && snap.value.typography) || {})
1586
- const [upStatus, setUpStatus] = React.useState({currentVersion: '0.3.8',latestVersion: undefined,updateAvailable: false
1403
+ const [upStatus, setUpStatus] = React.useState({currentVersion: '0.3.10',latestVersion: undefined,updateAvailable: false
1587
1404
  })
1588
1405
  const [upLoading, setUpLoading] = React.useState(false)
1589
1406
  const [upMsg, setUpMsg] = React.useState(null)
1590
1407
  const checkUpdate = () => {setUpLoading(true)
1591
1408
  setUpMsg(null)
1592
- fetch('/api/dsh-russian-lang/update', {headers: { 'x-dsh-plugin-update': '1' }
1593
- })
1594
- .then((r) => r.json())
1595
- .then((data) => {setUpStatus(data)
1409
+ fetch('/api/dsh-russian-lang/update', {headers: { 'x-dsh-plugin-update': '1' }}).then((r) => r.json()).then((data) => {setUpStatus(data)
1596
1410
  setUpLoading(false)
1597
- })
1598
- .catch(() => {setUpLoading(false)
1411
+ }).catch(() => {setUpLoading(false)
1599
1412
  setUpMsg({ type: 'err', text: t('updaterFailed') })
1600
1413
  })
1601
1414
  }
1602
1415
  const triggerUpdate = () => {setUpLoading(true)
1603
1416
  setUpMsg(null)
1604
- fetch('/api/dsh-russian-lang/update', {method: 'POST',headers: { 'x-dsh-plugin-update': '1' }
1605
- })
1606
- .then((r) => r.json())
1607
- .then((data) => {setUpLoading(false)
1417
+ fetch('/api/dsh-russian-lang/update', {method: 'POST',headers: { 'x-dsh-plugin-update': '1' }}).then((r) => r.json()).then((data) => {setUpLoading(false)
1608
1418
  if (data.restartRequired || data.updatedVersion) {setUpStatus(data)
1609
1419
  setUpMsg({ type: 'ok', text: t('updaterSuccess') })
1610
1420
  } else if (data.error) {setUpMsg({ type: 'err', text: data.error })
1611
- }
1612
- })
1613
- .catch(() => {setUpLoading(false)
1421
+ }}).catch(() => {setUpLoading(false)
1614
1422
  setUpMsg({ type: 'err', text: t('updaterFailed') })
1615
1423
  })
1616
1424
  }
1617
1425
  const [transStatus, setTransStatus] = React.useState(null)
1618
1426
  const [transStarting, setTransStarting] = React.useState(false)
1619
1427
  const [transMsg, setTransMsg] = React.useState(null)
1620
- const fetchTransStatus = () => {fetch('/api/dsh-russian-lang/translator/status')
1621
- .then((r) => r.json())
1622
- .then((data) => setTransStatus(data))
1623
- .catch(() => setTransStatus(null))
1428
+ const fetchTransStatus = () => {fetch('/api/dsh-russian-lang/translator/status').then((r) => r.json()).then((data) => setTransStatus(data)).catch(() => setTransStatus(null))
1624
1429
  }
1625
1430
  const startLibreTranslate = () => {setTransStarting(true)
1626
1431
  setTransMsg(null)
1627
- fetch('/api/dsh-russian-lang/translator/setup', {method: 'POST',headers: { 'x-dsh-translator': '1' }
1628
- })
1629
- .then((r) => r.json())
1630
- .then((data) => {setTransStarting(false)
1432
+ fetch('/api/dsh-russian-lang/translator/setup', {method: 'POST',headers: { 'x-dsh-translator': '1' }}).then((r) => r.json()).then((data) => {setTransStarting(false)
1631
1433
  if (data.ok) {setTransMsg({ type: 'ok', text: data.message || 'Контейнер LibreTranslate запущен.' })
1632
1434
  fetchTransStatus()
1633
1435
  } else {setTransMsg({ type: 'err', text: data.error || 'Ошибка запуска контейнера.' })
1634
- }
1635
- })
1636
- .catch((err) => {setTransStarting(false)
1436
+ }}).catch((err) => {setTransStarting(false)
1637
1437
  setTransMsg({ type: 'err', text: 'Ошибка: ' + (err.message || err) })
1638
1438
  })
1639
1439
  }
1640
1440
  React.useEffect(() => {if (open || (props && props.view === 'page')) {checkUpdate()
1641
1441
  fetchTransStatus()
1642
- }
1643
- }, [open, props])
1442
+ }}, [open, props])
1644
1443
  React.useEffect(() => {if (!scope || !scope.subscribe) return undefined
1645
1444
  const un = scope.subscribe(() => {const s = scope.getSnapshot()
1646
1445
  setSnap(s)
@@ -1649,11 +1448,9 @@ if (s.value && s.value.typography) setTypoState(s.value.typography)
1649
1448
  setSnap(scope.getSnapshot())
1650
1449
  return un
1651
1450
  }, [scope])
1652
- React.useEffect(() => {try {const un = runtime.subscribe(() => {try { setRuActive(runtime.getLocale().active === 'ru') } catch (e) { }
1653
- })
1451
+ React.useEffect(() => {try {const un = runtime.subscribe(() => {try { setRuActive(runtime.getLocale().active === 'ru') } catch (e) { }})
1654
1452
  return un
1655
- } catch (e) { return undefined }
1656
- }, [runtime])
1453
+ } catch (e) { return undefined }}, [runtime])
1657
1454
  const status = snap.status || 'loading'
1658
1455
  const value = snap.value || {}
1659
1456
  const typography = typo
@@ -1667,8 +1464,7 @@ try {const r = scope.set('typography', next)
1667
1464
  if (r && typeof r.catch === 'function') r.catch((err) => {console.warn('dsh-russian-lang: scope.set typography failed', err && err.message || err)
1668
1465
  })
1669
1466
  } catch (err) {console.warn('dsh-russian-lang: scope.set typography sync threw', err && err.message || err)
1670
- }
1671
- }
1467
+ }}
1672
1468
  const onEnabled = (ev) => { if (toggleRu) toggleRu(ev.target.checked) }
1673
1469
  const checkbox = (checked, onChange, disabled) =>
1674
1470
  React.createElement('input', {type: 'checkbox', checked: !!checked, disabled: !!disabled,className: 'rl-check', onChange: (ev) => onChange(ev),})
@@ -1687,129 +1483,59 @@ const presetInfo = typeof SYSTEM_PROMPT_PRESETS !== 'undefined' ? SYSTEM_PROMPT_
1687
1483
  if (props && props.view === 'summary') {return React.createElement('div', { className: 'rl-sub' }, statusLine || t('cardSub'))
1688
1484
  }
1689
1485
  const page = !!(props && props.view === 'page')
1690
- return React.createElement('div', { className: page ? 'rl-page-seat' : 'rl-card' },React.createElement('button', {type: 'button',className: 'rl-head',style: page ? { display: 'none' } : undefined,'aria-expanded': page ? 'true' : String(open),onClick: () => setOpen(!open),},React.createElement('span', { className: 'rl-head-main' },React.createElement('div', { className: 'rl-title' },'🇷🇺 ' + t('cardTitle'),React.createElement('span', { className: 'rl-badge ' + (ruActive ? 'rl-badge-ok' : 'rl-badge-warn') },ruActive ? t('badgeRu') : t('badgeEn')),React.createElement('span', { className: 'rl-badge rl-badge-ok' }, t('badgeCoverage'))
1691
- ),React.createElement('div', { className: 'rl-sub' },statusLine || t('cardSub'))),React.createElement('span', {className: 'rl-chev' + (open ? ' rl-chev-open' : ''),}, React.createElement(Chevron, null))),(page || open) && React.createElement('div', { className: 'rl-body' },React.createElement('div', { className: 'rl-page' },React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secLanguage')),React.createElement('span', { className: 'rl-badge ' + (ruActive ? 'rl-badge-ok' : 'rl-badge-dim') },ruActive ? t('badgeRu') : t('badgeEn'))
1692
- ),React.createElement('div', { className: 'rl-section-desc' }, t('secLanguageDesc')),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { className: 'rl-item-head' },React.createElement('label', { className: 'rl-item-label' },checkbox(ruActive, onEnabled, disabled),t('enabled')
1693
- )
1694
- ),React.createElement('div', { className: 'rl-item-desc' }, t('enabledDesc'))
1695
- ),React.createElement('div', { className: 'rl-hint-text' }, t('quickSwitchNote')),overridesCount > 0 ? React.createElement('div', { className: 'rl-badge rl-badge-dim', style: { alignSelf: 'flex-start' } },t('overridesCount') + ': ' + overridesCount) : null
1696
- ),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secTypography')),React.createElement('span', { className: 'rl-badge rl-badge-ok' }, t('badgeSmartUx'))
1697
- ),React.createElement('div', { className: 'rl-section-desc' }, t('secTypographyDesc')),React.createElement('div', { className: 'rl-grid-2' },React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { className: 'rl-item-head' },React.createElement('label', { className: 'rl-item-label' },checkbox(typography.enabled !== false && ruActive,(ev) => setTypo({ enabled: ev.target.checked }), !ruActive || disabled),t('typography')
1698
- )
1699
- ),React.createElement('div', { className: 'rl-item-desc' }, t('typographyDesc'))
1700
- ),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { className: 'rl-item-head' },React.createElement('label', { className: 'rl-item-label' },checkbox(typography.liveInput !== false && ruActive,(ev) => setTypo({ liveInput: ev.target.checked }), !ruActive || disabled),t('liveInput')
1701
- )
1702
- ),React.createElement('div', { className: 'rl-item-desc' }, t('liveInputDesc'))
1703
- ),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { className: 'rl-item-head' },React.createElement('label', { className: 'rl-item-label' },checkbox(typography.yo === true && ruActive,(ev) => setTypo({ yo: ev.target.checked }), !ruActive || disabled),t('yo')
1704
- )
1705
- ),React.createElement('div', { className: 'rl-item-desc' }, t('yoDesc'))
1706
- ),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { className: 'rl-item-head' },React.createElement('label', { className: 'rl-item-label' },checkbox(value.slashAliases !== false && ruActive,(ev) => {try { scope.set('slashAliases', ev.target.checked) } catch (err) { void e; }
1707
- }, !ruActive || disabled),t('slashAliases')
1708
- )
1709
- ),React.createElement('div', { className: 'rl-item-desc' }, t('slashAliasesDesc'))
1710
- )
1711
- ),React.createElement('div', { className: 'rl-hotkey-box' },React.createElement('span', { className: 'rl-hotkey-tag' }, '⌨️ Alt+L'),React.createElement('span', null, t('altLHintText'))
1712
- )
1713
- ),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secAgentPrompt')),React.createElement('span', { className: 'rl-badge ' + (value.agentPrompt ? 'rl-badge-ok' : 'rl-badge-dim') },value.agentPrompt ? 'Активен' : 'Выключен')
1714
- ),React.createElement('div', { className: 'rl-section-desc' }, t('secAgentPromptDesc')),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { className: 'rl-item-head' },React.createElement('label', { className: 'rl-item-label' },checkbox(value.agentPrompt === true,(ev) => {try {const r = scope.set('agentPrompt', ev.target.checked)
1486
+ return React.createElement('div', { className: page ? 'rl-page-seat' : 'rl-card' },React.createElement('button', {type: 'button',className: 'rl-head',style: page ? { display: 'none' } : undefined,'aria-expanded': page ? 'true' : String(open),onClick: () => setOpen(!open),},React.createElement('span', { className: 'rl-head-main' },React.createElement('div', { className: 'rl-title' },'🇷🇺 ' + t('cardTitle'),React.createElement('span', { className: 'rl-badge ' + (ruActive ? 'rl-badge-ok' : 'rl-badge-warn') },ruActive ? t('badgeRu') : t('badgeEn')),React.createElement('span', { className: 'rl-badge rl-badge-ok' }, t('badgeCoverage'))),React.createElement('div', { className: 'rl-sub' },statusLine || t('cardSub'))),React.createElement('span', {className: 'rl-chev' + (open ? ' rl-chev-open' : ''),}, React.createElement(Chevron, null))),(page || open) && React.createElement('div', { className: 'rl-body' },React.createElement('div', { className: 'rl-page' },React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secLanguage')),React.createElement('span', { className: 'rl-badge ' + (ruActive ? 'rl-badge-ok' : 'rl-badge-dim') },ruActive ? t('badgeRu') : t('badgeEn'))),React.createElement('div', { className: 'rl-section-desc' }, t('secLanguageDesc')),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { className: 'rl-item-head' },React.createElement('label', { className: 'rl-item-label' },checkbox(ruActive, onEnabled, disabled),t('enabled'))),React.createElement('div', { className: 'rl-item-desc' }, t('enabledDesc'))),React.createElement('div', { className: 'rl-hint-text' }, t('quickSwitchNote')),overridesCount > 0 ? React.createElement('div', { className: 'rl-badge rl-badge-dim', style: { alignSelf: 'flex-start' } },t('overridesCount') + ': ' + overridesCount) : null
1487
+ ),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secTypography')),React.createElement('span', { className: 'rl-badge rl-badge-ok' }, t('badgeSmartUx'))),React.createElement('div', { className: 'rl-section-desc' }, t('secTypographyDesc')),React.createElement('div', { className: 'rl-grid-2' },React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { className: 'rl-item-head' },React.createElement('label', { className: 'rl-item-label' },checkbox(typography.enabled !== false && ruActive,(ev) => setTypo({ enabled: ev.target.checked }), !ruActive || disabled),t('typography'))),React.createElement('div', { className: 'rl-item-desc' }, t('typographyDesc'))),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { className: 'rl-item-head' },React.createElement('label', { className: 'rl-item-label' },checkbox(typography.liveInput !== false && ruActive,(ev) => setTypo({ liveInput: ev.target.checked }), !ruActive || disabled),t('liveInput'))),React.createElement('div', { className: 'rl-item-desc' }, t('liveInputDesc'))),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { className: 'rl-item-head' },React.createElement('label', { className: 'rl-item-label' },checkbox(typography.yo === true && ruActive,(ev) => setTypo({ yo: ev.target.checked }), !ruActive || disabled),t('yo'))),React.createElement('div', { className: 'rl-item-desc' }, t('yoDesc'))),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { className: 'rl-item-head' },React.createElement('label', { className: 'rl-item-label' },checkbox(value.slashAliases !== false && ruActive,(ev) => {try { scope.set('slashAliases', ev.target.checked) } catch (err) { void err; }}, !ruActive || disabled),t('slashAliases'))),React.createElement('div', { className: 'rl-item-desc' }, t('slashAliasesDesc')))),React.createElement('div', { className: 'rl-hotkey-box' },React.createElement('span', { className: 'rl-hotkey-tag' }, '⌨️ Alt+L'),React.createElement('span', null, t('altLHintText')))),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secAgentPrompt')),React.createElement('span', { className: 'rl-badge ' + (value.agentPrompt ? 'rl-badge-ok' : 'rl-badge-dim') },value.agentPrompt ? 'Активен' : 'Выключен')),React.createElement('div', { className: 'rl-section-desc' }, t('secAgentPromptDesc')),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { className: 'rl-item-head' },React.createElement('label', { className: 'rl-item-label' },checkbox(value.agentPrompt === true,(ev) => {try {const r = scope.set('agentPrompt', ev.target.checked)
1715
1488
  if (r && typeof r.catch === 'function') r.catch((err) => {console.warn('dsh-russian-lang: scope.set agentPrompt failed', err && err.message || err)
1716
1489
  })
1717
1490
  } catch (err) {console.warn('dsh-russian-lang: scope.set agentPrompt sync threw', err && err.message || err)
1718
- }
1719
- }, false),t('agentPrompt')
1720
- )
1721
- ),React.createElement('div', { className: 'rl-item-desc' }, t('agentPromptDesc'))
1722
- ),value.agentPrompt ? React.createElement(React.Fragment, null,React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },React.createElement('label', { className: 'rl-item-label', style: { fontWeight: 500 } }, t('agentPromptPreset')),React.createElement('select', {className: 'rl-select',value: value.agentPromptPreset || 'technical_expert',onChange: (ev) => {try {const val = ev.target.value
1491
+ }}, false),t('agentPrompt'))),React.createElement('div', { className: 'rl-item-desc' }, t('agentPromptDesc'))),value.agentPrompt ? React.createElement(React.Fragment, null,React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },React.createElement('label', { className: 'rl-item-label', style: { fontWeight: 500 } }, t('agentPromptPreset')),React.createElement('select', {className: 'rl-select',value: value.agentPromptPreset || 'technical_expert',onChange: (ev) => {try {const val = ev.target.value
1723
1492
  scope.set('agentPromptPreset', val)
1724
- } catch (err) { console.warn('dsh-russian-lang: set agentPromptPreset failed', err) }
1725
- }
1726
- },React.createElement('option', { value: 'technical_expert' }, t('presetExpert')),React.createElement('option', { value: 'tech_writer' }, t('presetWriter')),React.createElement('option', { value: 'concise' }, t('presetConcise')),React.createElement('option', { value: 'code_reviewer' }, t('presetReviewer')),React.createElement('option', { value: 'architect' }, t('presetArchitect')),React.createElement('option', { value: 'tutor' }, t('presetTutor'))
1727
- )
1728
- ),presetInfo ? React.createElement('div', { className: 'rl-preview-box' },React.createElement('div', { style: { fontWeight: 600, marginBottom: '4px', color: 'var(--dsw-alias-label-primary)' } }, '💬 ' + presetInfo.label + ':'),presetInfo.text
1493
+ } catch (err) { console.warn('dsh-russian-lang: set agentPromptPreset failed', err) }}},React.createElement('option', { value: 'technical_expert' }, t('presetExpert')),React.createElement('option', { value: 'tech_writer' }, t('presetWriter')),React.createElement('option', { value: 'concise' }, t('presetConcise')),React.createElement('option', { value: 'code_reviewer' }, t('presetReviewer')),React.createElement('option', { value: 'architect' }, t('presetArchitect')),React.createElement('option', { value: 'tutor' }, t('presetTutor')))),presetInfo ? React.createElement('div', { className: 'rl-preview-box' },React.createElement('div', { style: { fontWeight: 600, marginBottom: '4px', color: 'var(--dsw-alias-label-primary)' } }, '💬 ' + presetInfo.label + ':'),presetInfo.text
1729
1494
  ) : null
1730
1495
  ) : null
1731
- ),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secUpdater')),React.createElement('span', { className: 'rl-badge ' + (upStatus.updateAvailable ? 'rl-badge-warn' : 'rl-badge-ok') },upStatus.updateAvailable ? t('badgeUpdateAvailable') : t('badgeUpToDate'))
1732
- ),React.createElement('div', { className: 'rl-section-desc' }, t('secUpdaterDesc')),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },React.createElement('div', null,React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },t('updaterCurrent').replace('{version}', upStatus.currentVersion || '0.3.8')),upStatus.updateAvailable
1496
+ ),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secUpdater')),React.createElement('span', { className: 'rl-badge ' + (upStatus.updateAvailable ? 'rl-badge-warn' : 'rl-badge-ok') },upStatus.updateAvailable ? t('badgeUpdateAvailable') : t('badgeUpToDate'))),React.createElement('div', { className: 'rl-section-desc' }, t('secUpdaterDesc')),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },React.createElement('div', null,React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },t('updaterCurrent').replace('{version}', upStatus.currentVersion || '0.3.10')),upStatus.updateAvailable
1733
1497
  ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-warning-primary)', marginTop: '2px', fontWeight: 500 } },t('updaterLatest').replace('{version}', upStatus.latestVersion || ''))
1734
- : React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', marginTop: '2px' } },t('updaterUpToDate'))
1735
- ),upStatus.updateAvailable
1498
+ : React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', marginTop: '2px' } },t('updaterUpToDate'))),upStatus.updateAvailable
1736
1499
  ? React.createElement('button', {type: 'button',className: 'rl-btn rl-btn-primary',disabled: upLoading,onClick: triggerUpdate,}, upLoading ? t('updaterUpdating') : t('updaterBtn').replace('{version}', upStatus.latestVersion || ''))
1737
- : React.createElement('button', {type: 'button',className: 'rl-btn',disabled: upLoading,onClick: checkUpdate,}, upLoading ? t('updaterChecking') : t('updaterCheckBtn'))
1738
- ),upMsg ? React.createElement('div', {style: {marginTop: '10px', padding: '8px 12px', borderRadius: '8px',background: upMsg.type === 'ok' ? 'color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent)' : 'color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)',color: upMsg.type === 'ok' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontSize: '12px', fontWeight: 500
1739
- }
1740
- }, upMsg.text) : null
1741
- )
1742
- ),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secTranslator')),React.createElement('span', {className: 'rl-badge ' + (value.translateEngine === 'local' ? 'rl-badge-ok' : (value.translateEngine === 'google' ? 'rl-badge-warn' : 'rl-badge-dim'))
1743
- }, value.translateEngine === 'local' ? 'Локально' : (value.translateEngine === 'google' ? 'Google' : 'Выключен'))
1744
- ),React.createElement('div', { className: 'rl-section-desc' }, t('secTranslatorDesc')),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },React.createElement('label', { className: 'rl-item-label', style: { fontWeight: 500 } }, t('translateEngine')),React.createElement('select', {className: 'rl-select',value: value.translateEngine || 'off',onChange: (ev) => {try {const val = ev.target.value
1500
+ : React.createElement('button', {type: 'button',className: 'rl-btn',disabled: upLoading,onClick: checkUpdate,}, upLoading ? t('updaterChecking') : t('updaterCheckBtn'))),upMsg ? React.createElement('div', {style: {marginTop: '10px', padding: '8px 12px', borderRadius: '8px',background: upMsg.type === 'ok' ? 'color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent)' : 'color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)',color: upMsg.type === 'ok' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontSize: '12px', fontWeight: 500
1501
+ }}, upMsg.text) : null
1502
+ )),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secTranslator')),React.createElement('span', {className: 'rl-badge ' + (value.translateEngine === 'local' ? 'rl-badge-ok' : (value.translateEngine === 'google' ? 'rl-badge-warn' : 'rl-badge-dim'))
1503
+ }, value.translateEngine === 'local' ? 'Локально' : (value.translateEngine === 'google' ? 'Google' : 'Выключен'))),React.createElement('div', { className: 'rl-section-desc' }, t('secTranslatorDesc')),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },React.createElement('label', { className: 'rl-item-label', style: { fontWeight: 500 } }, t('translateEngine')),React.createElement('select', {className: 'rl-select',value: value.translateEngine || 'off',onChange: (ev) => {try {const val = ev.target.value
1745
1504
  scope.set('translateEngine', val)
1746
1505
  if (val === 'local') fetchTransStatus()
1747
- } catch (err) { console.warn('dsh-russian-lang: set translateEngine failed', err) }
1748
- }
1749
- },React.createElement('option', { value: 'off' }, t('engineOff')),React.createElement('option', { value: 'local' }, t('engineLocal')),React.createElement('option', { value: 'google' }, t('engineGoogle'))
1750
- )
1751
- ),value.translateEngine === 'google' ? React.createElement('div', {style: {marginTop: '10px', padding: '8px 12px', borderRadius: '8px',background: 'color-mix(in srgb, var(--dsw-alias-state-warning-primary) 12%, transparent)', border: '1px solid color-mix(in srgb, var(--dsw-alias-state-warning-primary) 30%, transparent)',color: 'var(--dsw-alias-state-warning-primary)', fontSize: '12px', fontWeight: 500, lineHeight: 1.4
1752
- }
1753
- }, t('googleWarn')) : null,value.translateEngine === 'local' ? React.createElement('div', {style: { marginTop: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }
1754
- },React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, t('localInfo')),React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },React.createElement('span', {className: 'rl-badge ' + (transStatus && transStatus.running ? 'rl-badge-ok' : 'rl-badge-dim')
1506
+ } catch (err) { console.warn('dsh-russian-lang: set translateEngine failed', err) }}},React.createElement('option', { value: 'off' }, t('engineOff')),React.createElement('option', { value: 'local' }, t('engineLocal')),React.createElement('option', { value: 'google' }, t('engineGoogle')))),value.translateEngine === 'google' ? React.createElement('div', {style: {marginTop: '10px', padding: '8px 12px', borderRadius: '8px',background: 'color-mix(in srgb, var(--dsw-alias-state-warning-primary) 12%, transparent)', border: '1px solid color-mix(in srgb, var(--dsw-alias-state-warning-primary) 30%, transparent)',color: 'var(--dsw-alias-state-warning-primary)', fontSize: '12px', fontWeight: 500, lineHeight: 1.4
1507
+ }}, t('googleWarn')) : null,value.translateEngine === 'local' ? React.createElement('div', {style: { marginTop: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }},React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, t('localInfo')),React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },React.createElement('span', {className: 'rl-badge ' + (transStatus && transStatus.running ? 'rl-badge-ok' : 'rl-badge-dim')
1755
1508
  }, transStatus && transStatus.running ? t('localStatusRunning') : t('localStatusStopped')),(!transStatus || !transStatus.running) ? React.createElement('button', {type: 'button',className: 'rl-btn rl-btn-primary',disabled: transStarting,onClick: startLibreTranslate
1756
1509
  }, transStarting ? t('localStarting') : t('localStartBtn')) : null
1757
1510
  ),transMsg ? React.createElement('div', {style: {marginTop: '6px', padding: '8px 12px', borderRadius: '8px',background: transMsg.type === 'ok' ? 'color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent)' : 'color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)',color: transMsg.type === 'ok' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontSize: '12px', fontWeight: 500
1758
- }
1759
- }, transMsg.text) : null
1511
+ }}, transMsg.text) : null
1760
1512
  ) : null
1761
- )
1762
- ),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secOverrides')),React.createElement('span', { className: 'rl-badge rl-badge-dim' },String(overridesCount) + ' ' + (typeof plural === 'function' ? plural(overridesCount, ['запись', 'записи', 'записей']) : 'записей'))
1763
- ),React.createElement('div', { className: 'rl-section-desc' }, t('secOverridesDesc')),React.createElement('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },React.createElement('input', {className: 'rl-select',style: { flex: '1 1 180px', maxWidth: 'none' },placeholder: t('overrideKeyPlaceholder'),value: newKey,onChange: (e) => setNewKey(e.target.value),}),React.createElement('input', {className: 'rl-select',style: { flex: '2 1 240px', maxWidth: 'none' },placeholder: t('overrideValuePlaceholder'),value: newVal,onChange: (e) => setNewVal(e.target.value),}),React.createElement('button', {type: 'button',className: 'rl-btn rl-btn-primary',disabled: !newKey.trim() || !newVal.trim(),onClick: () => {const k = newKey.trim()
1513
+ )),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secOverrides')),React.createElement('span', { className: 'rl-badge rl-badge-dim' },String(overridesCount) + ' ' + (typeof plural === 'function' ? plural(overridesCount, ['запись', 'записи', 'записей']) : 'записей'))),React.createElement('div', { className: 'rl-section-desc' }, t('secOverridesDesc')),React.createElement('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },React.createElement('input', {className: 'rl-select',style: { flex: '1 1 180px', maxWidth: 'none' },placeholder: t('overrideKeyPlaceholder'),value: newKey,onChange: (e) => setNewKey(e.target.value),}),React.createElement('input', {className: 'rl-select',style: { flex: '2 1 240px', maxWidth: 'none' },placeholder: t('overrideValuePlaceholder'),value: newVal,onChange: (e) => setNewVal(e.target.value),}),React.createElement('button', {type: 'button',className: 'rl-btn rl-btn-primary',disabled: !newKey.trim() || !newVal.trim(),onClick: () => {const k = newKey.trim()
1764
1514
  const v = newVal.trim()
1765
1515
  if (!k || !v) return
1766
1516
  const next = Object.assign({}, overrides, { [k]: v })
1767
- try { scope.set('overrides', updated) } catch (_e) { void _e }
1517
+ try { scope.set('overrides', next) } catch (_e) { void _e }
1768
1518
  setNewKey('')
1769
1519
  setNewVal('')
1770
- },}, t('overrideAddBtn'))
1771
- ),overridesCount === 0
1520
+ },}, t('overrideAddBtn'))),overridesCount === 0
1772
1521
  ? React.createElement('div', { className: 'rl-hint-text', style: { fontStyle: 'italic', padding: '4px 0' } }, t('overrideEmpty'))
1773
1522
  : React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '6px' } },Object.keys(overrides).map((k) =>
1774
- React.createElement('div', {key: k,style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 10px', background: 'var(--dsw-alias-bg-layer-3)', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', fontSize: '12px' },},React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', overflow: 'hidden' } },React.createElement('code', { style: { color: 'var(--dsw-alias-state-brand-primary)', fontWeight: 600 } }, k),React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, '→'),React.createElement('span', { style: { color: 'var(--dsw-alias-label-primary)' } }, overrides[k])
1775
- ),React.createElement('button', {type: 'button',className: 'rl-btn',style: { padding: '2px 8px', height: '24px', fontSize: '11px', color: 'var(--dsw-alias-state-error-primary)' },onClick: () => {const next = Object.assign({}, overrides)
1523
+ React.createElement('div', {key: k,style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 10px', background: 'var(--dsw-alias-bg-layer-3)', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', fontSize: '12px' },},React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', overflow: 'hidden' } },React.createElement('code', { style: { color: 'var(--dsw-alias-state-brand-primary)', fontWeight: 600 } }, k),React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, '→'),React.createElement('span', { style: { color: 'var(--dsw-alias-label-primary)' } }, overrides[k])),React.createElement('button', {type: 'button',className: 'rl-btn',style: { padding: '2px 8px', height: '24px', fontSize: '11px', color: 'var(--dsw-alias-state-error-primary)' },onClick: () => {const next = Object.assign({}, overrides)
1776
1524
  delete next[k]
1777
- try { scope.set('overrides', updated) } catch (_e) { void _e }
1778
- },}, t('overrideDelete'))
1779
- )
1780
- )
1781
- ),React.createElement('div', { className: 'rl-hint-text', style: { marginTop: '8px' } }, t('overrideTip')),React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', marginTop: '10px' } },React.createElement('button', {type: 'button',className: 'rl-btn' + (isInspectorActive ? ' rl-btn-primary' : ''),onClick: () => {isInspectorActive = !isInspectorActive
1525
+ try { scope.set('overrides', next) } catch (_e) { void _e }},}, t('overrideDelete'))))),React.createElement('div', { className: 'rl-hint-text', style: { marginTop: '8px' } }, t('overrideTip')),React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', marginTop: '10px' } },React.createElement('button', {type: 'button',className: 'rl-btn' + (isInspectorActive ? ' rl-btn-primary' : ''),onClick: () => {isInspectorActive = !isInspectorActive
1782
1526
  updateInspectorPill()
1783
1527
  },}, isInspectorActive ? '🔍 Инспектор перевода (включён)' : '🔍 Включить инспектор перевода (Alt+I)'),React.createElement('button', {type: 'button',className: 'rl-btn',style: { fontSize: '11px' },onClick: () => {try {navigator.clipboard.writeText(JSON.stringify(overrides, null, 2))
1784
1528
  alert('Оверрайды скопированы в буфер обмена как JSON!')
1785
- } catch (err) { void err; }
1786
- }
1787
- }, '📤 Экспорт JSON'),React.createElement('button', {type: 'button',className: 'rl-btn',style: { fontSize: '11px' },onClick: () => {const input = prompt('Вставьте JSON с оверрайдами:')
1529
+ } catch (err) { void err; }}}, '📤 Экспорт JSON'),React.createElement('button', {type: 'button',className: 'rl-btn',style: { fontSize: '11px' },onClick: () => {const input = prompt('Вставьте JSON с оверрайдами:')
1788
1530
  if (!input) return
1789
1531
  try {const parsed = JSON.parse(input)
1790
1532
  if (parsed && typeof parsed === 'object') {const merged = Object.assign({}, overrides, parsed)
1791
1533
  scope.set('overrides', merged)
1534
+ }} catch (err) { alert('Ошибка разбора JSON: ' + (err && err.message || err)) }}}, '📥 Импорт JSON'))),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secSupport')),React.createElement('span', { className: 'rl-badge rl-badge-ok' }, '🟢 100.0%')),React.createElement('div', { className: 'rl-section-desc' }, t('secSupportDesc')),React.createElement('div', { className: 'rl-grid-3' },React.createElement('div', { className: 'rl-stat-box' },React.createElement('div', { className: 'rl-stat-val' }, '113'),React.createElement('div', { className: 'rl-stat-label' }, t('statNamespaces'))),React.createElement('div', { className: 'rl-stat-box' },React.createElement('div', { className: 'rl-stat-val' }, '1 225'),React.createElement('div', { className: 'rl-stat-label' }, t('statCoreKeys'))),React.createElement('div', { className: 'rl-stat-box' },React.createElement('div', { className: 'rl-stat-val' }, '5 924'),React.createElement('div', { className: 'rl-stat-label' }, t('statPluginKeys')))),React.createElement('div', { className: 'rl-actions-row' },React.createElement('a', {href: makeIssueUrl({}, '0.3.10'),target: '_blank',rel: 'noopener noreferrer',className: 'rl-btn rl-btn-primary'
1535
+ }, '💬 ' + t('reportIssue')),React.createElement('div', { className: 'rl-hint-text', style: { flex: 1 } },t('exportMdHint')))))))
1792
1536
  }
1793
- } catch (err) { alert('Ошибка разбора JSON: ' + (err && err.message || err)) }
1794
- }
1795
- }, '📥 Импорт JSON')
1796
- )
1797
- ),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secSupport')),React.createElement('span', { className: 'rl-badge rl-badge-ok' }, '🟢 100.0%')
1798
- ),React.createElement('div', { className: 'rl-section-desc' }, t('secSupportDesc')),React.createElement('div', { className: 'rl-grid-3' },React.createElement('div', { className: 'rl-stat-box' },React.createElement('div', { className: 'rl-stat-val' }, '113'),React.createElement('div', { className: 'rl-stat-label' }, t('statNamespaces'))
1799
- ),React.createElement('div', { className: 'rl-stat-box' },React.createElement('div', { className: 'rl-stat-val' }, '1 225'),React.createElement('div', { className: 'rl-stat-label' }, t('statCoreKeys'))
1800
- ),React.createElement('div', { className: 'rl-stat-box' },React.createElement('div', { className: 'rl-stat-val' }, '5 924'),React.createElement('div', { className: 'rl-stat-label' }, t('statPluginKeys'))
1801
- )
1802
- ),React.createElement('div', { className: 'rl-actions-row' },React.createElement('a', {href: makeIssueUrl({}, '0.3.8'),target: '_blank',rel: 'noopener noreferrer',className: 'rl-btn rl-btn-primary'
1803
- }, '💬 ' + t('reportIssue')),React.createElement('div', { className: 'rl-hint-text', style: { flex: 1 } },t('exportMdHint')
1804
- )
1805
- )
1806
- )
1807
- )
1808
- )
1809
- )
1810
- }
1811
- const RL_CSS = [
1812
- '.rl-modal-mask{position:fixed;top:0;left:0;width:100vw;height:100vh;background:var(--dsw-alias-bg-mask);display:flex;align-items:center;justify-content:center;z-index:99999;backdrop-filter:blur(2px)}','.rl-modal-box{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;width:calc(100vw - 40px);max-width:500px;box-shadow:var(--dsw-alias-shadow-l3);display:flex;flex-direction:column;gap:12px;padding:18px}','.rl-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;overflow:hidden;transition:border-color .15s ease}','.rl-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 18px}','.rl-head:hover{background:var(--dsw-alias-bg-layer-2)}','.rl-head-main{flex:1;display:flex;flex-direction:column;gap:4px}','.rl-title{color:var(--dsw-alias-label-primary);font-size:16px;font-weight:600;line-height:1.4;display:flex;align-items:center;flex-wrap:wrap;gap:8px}','.rl-sub{color:var(--dsw-alias-label-secondary);font-size:13px}','.rl-chev{margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);display:inline-flex;transition:transform .16s}','.rl-chev-open{transform:rotate(180deg)}','.rl-body{border-top:1px solid var(--dsw-alias-border-l2);padding:18px}','.rl-page{display:flex;flex-direction:column;gap:16px;max-width:960px}','.rl-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:16px 18px;display:flex;flex-direction:column;gap:12px}','.rl-section-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px}','.rl-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}','.rl-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:10px}','.rl-grid-3{display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:10px}','.rl-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}','.rl-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}','.rl-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent)}','.rl-badge-dim{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3)}','.rl-item-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:4px}','.rl-item-head{display:flex;align-items:center;justify-content:space-between;gap:8px}','.rl-item-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:8px;cursor:pointer}','.rl-item-desc{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.4}','.rl-stat-box{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:2px}','.rl-stat-val{font-size:16px;font-weight:700;color:var(--dsw-alias-label-primary)}','.rl-stat-label{font-size:11px;color:var(--dsw-alias-label-secondary);letter-spacing:0.3px}','.rl-hotkey-box{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4;display:flex;align-items:flex-start;gap:10px}','.rl-hotkey-tag{font-size:11px;font-weight:700;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);white-space:nowrap}','.rl-preview-box{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}','.rl-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:12px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease;text-decoration:none}','.rl-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-secondary)}','.rl-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}','.rl-btn-primary:hover:not(:disabled){opacity:0.9}','.rl-select{height:32px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 8px;font-size:12px;outline:none;width:100%;max-width:380px}','.rl-select:focus{border-color:var(--dsw-alias-state-brand-primary)}','.rl-check{width:16px;height:16px;accent-color:var(--dsw-alias-label-primary);cursor:pointer;flex-shrink:0}','.rl-hint-text{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.4}','.rl-actions-row{display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding-top:4px}','.rl-lang-chip{appearance:none;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:700;padding:2px 7px;border-radius:6px;display:inline-flex;align-items:center;transition:all .15s;margin:0 4px}','.rl-lang-chip:hover{border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-label-primary)}','.rl-lang-chip-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}','.rl-action-btn{appearance:none;background:0 0;border:1px solid transparent;border-radius:4px;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:11px;padding:2px 5px;display:inline-flex;align-items:center;transition:all .15s}','.rl-action-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3)}','.rl-action-btn-active{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2)}','.rl-turn-translation{margin:8px 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;font-size:13px;line-height:1.5}','.rl-trans-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:4px}','.rl-trans-title{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:0.5px}','.rl-trans-tools{display:flex;gap:4px}','.rl-trans-btn{appearance:none;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);cursor:pointer;padding:2px 7px;border-radius:4px;font-size:11px}','.rl-trans-btn:hover{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}','.rl-trans-body{color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;user-select:text}','.rl-export-md-btn{appearance:none;border:1px solid var(--dsw-alias-border-l2);height:32px;color:var(--dsw-alias-label-primary);cursor:pointer;background:transparent;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:6px 12px;font-size:13px;font-weight:500;display:inline-flex;white-space:nowrap;margin-left:6px;transition:all .15s ease}','.rl-export-md-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}','.rl-inspector-hover{position:absolute;pointer-events:none;border:2px solid var(--dsw-alias-state-brand-primary);border-radius:4px;background:color-mix(in srgb,var(--dsw-alias-state-brand-primary) 12%,transparent);box-shadow:0 0 10px color-mix(in srgb,var(--dsw-alias-state-brand-primary) 30%,transparent);z-index:99998;transition:all .06s ease;display:none}','.rl-inspector-badge{position:absolute;bottom:calc(100% + 4px);left:0;background:var(--dsw-alias-state-brand-primary);color:var(--dsw-alias-label-inverse);font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;white-space:nowrap;box-shadow:var(--dsw-alias-shadow-l2);pointer-events:none}','.rl-inspector-pill{position:fixed;bottom:18px;right:18px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:20px;padding:6px 12px;box-shadow:var(--dsw-alias-shadow-l3);display:none;align-items:center;gap:8px;font-size:12px;z-index:99990;user-select:none}','.rl-inspector-pill-active{border-color:var(--dsw-alias-state-brand-primary);box-shadow:0 0 12px color-mix(in srgb,var(--dsw-alias-state-brand-primary) 35%,transparent)}',].join('')
1537
+ const RL_CSS = ['.rl-modal-mask{position:fixed;top:0;left:0;width:100vw;height:100vh;background:var(--dsw-alias-bg-mask);display:flex;align-items:center;justify-content:center;z-index:99999;backdrop-filter:blur(2px)}.rl-modal-box{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;width:calc(100vw - 40px);max-width:500px;box-shadow:var(--dsw-alias-shadow-l3);display:flex;flex-direction:column;gap:12px;padding:18px}.rl-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;overflow:hidden;transition:border-color .15s ease}.rl-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 18px}.rl-head:hover{background:var(--dsw-alias-bg-layer-2)}.rl-head-main{flex:1;display:flex;flex-direction:column;gap:4px}.rl-title{color:var(--dsw-alias-label-primary);font-size:16px;font-weight:600;line-height:1.4;display:flex;align-items:center;flex-wrap:wrap;gap:8px}.rl-sub{color:var(--dsw-alias-label-secondary);font-size:13px}.rl-chev{margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);display:inline-flex;transition:transform .16s}.rl-chev-open{transform:rotate(180deg)}.rl-body{border-top:1px solid var(--dsw-alias-border-l2);padding:18px}.rl-page{display:flex;flex-direction:column;gap:16px;max-width:960px}.rl-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:16px 18px;display:flex;flex-direction:column;gap:12px}.rl-section-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px}.rl-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}.rl-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:10px}.rl-grid-3{display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:10px}.rl-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}.rl-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}.rl-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent)}.rl-badge-dim{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3)}.rl-item-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:4px}.rl-item-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.rl-item-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:8px;cursor:pointer}.rl-item-desc{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.4}.rl-stat-box{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:2px}.rl-stat-val{font-size:16px;font-weight:700;color:var(--dsw-alias-label-primary)}.rl-stat-label{font-size:11px;color:var(--dsw-alias-label-secondary);letter-spacing:0.3px}.rl-hotkey-box{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4;display:flex;align-items:flex-start;gap:10px}.rl-hotkey-tag{font-size:11px;font-weight:700;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);white-space:nowrap}.rl-preview-box{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}.rl-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:12px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease;text-decoration:none}.rl-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-secondary)}.rl-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}.rl-btn-primary:hover:not(:disabled){opacity:0.9}.rl-select{height:32px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 8px;font-size:12px;outline:none;width:100%;max-width:380px}.rl-select:focus{border-color:var(--dsw-alias-state-brand-primary)}.rl-check{width:16px;height:16px;accent-color:var(--dsw-alias-label-primary);cursor:pointer;flex-shrink:0}.rl-hint-text{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.4}.rl-actions-row{display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding-top:4px}.rl-lang-chip{appearance:none;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:700;padding:2px 7px;border-radius:6px;display:inline-flex;align-items:center;transition:all .15s;margin:0 4px}.rl-lang-chip:hover{border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-label-primary)}.rl-lang-chip-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}.rl-action-btn{appearance:none;background:0 0;border:1px solid transparent;border-radius:4px;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:11px;padding:2px 5px;display:inline-flex;align-items:center;transition:all .15s}.rl-action-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3)}.rl-action-btn-active{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2)}.rl-turn-translation{margin:8px 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;font-size:13px;line-height:1.5}.rl-trans-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:4px}.rl-trans-title{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:0.5px}.rl-trans-tools{display:flex;gap:4px}.rl-trans-btn{appearance:none;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);cursor:pointer;padding:2px 7px;border-radius:4px;font-size:11px}.rl-trans-btn:hover{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.rl-trans-body{color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;user-select:text}.rl-export-md-btn{appearance:none;border:1px solid var(--dsw-alias-border-l2);height:32px;color:var(--dsw-alias-label-primary);cursor:pointer;background:transparent;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:6px 12px;font-size:13px;font-weight:500;display:inline-flex;white-space:nowrap;margin-left:6px;transition:all .15s ease}.rl-export-md-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.rl-inspector-hover{position:absolute;pointer-events:none;border:2px solid var(--dsw-alias-state-brand-primary);border-radius:4px;background:color-mix(in srgb,var(--dsw-alias-state-brand-primary) 12%,transparent);box-shadow:0 0 10px color-mix(in srgb,var(--dsw-alias-state-brand-primary) 30%,transparent);z-index:99998;transition:all .06s ease;display:none}.rl-inspector-badge{position:absolute;bottom:calc(100% + 4px);left:0;background:var(--dsw-alias-state-brand-primary);color:var(--dsw-alias-label-inverse);font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;white-space:nowrap;box-shadow:var(--dsw-alias-shadow-l2);pointer-events:none}.rl-inspector-pill{position:fixed;bottom:18px;right:18px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:20px;padding:6px 12px;box-shadow:var(--dsw-alias-shadow-l3);display:none;align-items:center;gap:8px;font-size:12px;z-index:99990;user-select:none}.rl-inspector-pill-active{border-color:var(--dsw-alias-state-brand-primary);box-shadow:0 0 12px color-mix(in srgb,var(--dsw-alias-state-brand-primary) 35%,transparent)}'
1538
+ ].join('')
1813
1539
  const STYLE_ID = 'dsh-russian-lang-styles'
1814
1540
  if (typeof document !== 'undefined' && (document.getElementById && !document.getElementById(STYLE_ID))) {const tag = document.createElement('style')
1815
1541
  tag.id = STYLE_ID