@goodandready/dsh-russian-lang 0.3.7 → 0.3.9

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
@@ -1,77 +1,55 @@
1
- window.__ModuleLoader__.load({
2
- id: '@goodandready/dsh-russian-lang',
3
- factory: (require) => {
4
- var module = { exports: {} }
1
+ window.__ModuleLoader__.load({id: '@goodandready/dsh-russian-lang',factory: (require) => {var module = { exports: {} }
5
2
  var React = null
6
3
  try { React = require('react') } catch (e) { }
4
+ let isInspectorActive = false
5
+ let hoverBoxEl = null
6
+ let pillEl = null
7
7
  const numberFormat = new Intl.NumberFormat('ru-RU')
8
8
  const compactNumberFormat = new Intl.NumberFormat('ru-RU', { notation: 'compact', compactDisplay: 'short' })
9
9
  const relativeTimeFormat = new Intl.RelativeTimeFormat('ru-RU', { numeric: 'auto' })
10
- const datePresetFormats = {
11
- short: new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' }),
12
- shortDateTime: new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }),
13
- medium: new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }),
14
- long: new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }),
15
- time: new Intl.DateTimeFormat('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
16
- }
17
- const formatDate = (val, style = 'short') => {
18
- if (val === null || val === undefined || val === '') return ''
10
+ const datePresetFormats = {short: new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' }),shortDateTime: new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }),medium: new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }),long: new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }),time: new Intl.DateTimeFormat('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),}
11
+ const formatDate = (val, style = 'short') => {if (val === null || val === undefined || val === '') return ''
19
12
  const d = val instanceof Date ? val : new Date(typeof val === 'number' && val < 1e12 ? val * 1000 : val)
20
13
  if (isNaN(d.getTime())) return String(val)
21
- if (typeof style === 'string' && datePresetFormats[style]) {
22
- return datePresetFormats[style].format(d)
14
+ if (typeof style === 'string' && datePresetFormats[style]) {return datePresetFormats[style].format(d)
23
15
  }
24
- if (typeof style === 'object' && style !== null) {
25
- try {
26
- return new Intl.DateTimeFormat('ru-RU', style).format(d)
27
- } catch (_) {
28
- return datePresetFormats.short.format(d)
16
+ if (typeof style === 'object' && style !== null) {try {return new Intl.DateTimeFormat('ru-RU', style).format(d)
17
+ } catch (_) {return datePresetFormats.short.format(d)
29
18
  }
30
19
  }
31
20
  return datePresetFormats.short.format(d)
32
21
  }
33
- const formatCompactNumber = (val) => {
34
- if (val === null || val === undefined || val === '') return ''
22
+ const formatCompactNumber = (val) => {if (val === null || val === undefined || val === '') return ''
35
23
  const n = typeof val === 'number' ? val : Number(val)
36
24
  return isNaN(n) ? String(val) : compactNumberFormat.format(n)
37
25
  }
38
- const formatTokens = (count, compact = false) => {
39
- if (count === null || count === undefined || count === '') return ''
26
+ const formatTokens = (count, compact = false) => {if (count === null || count === undefined || count === '') return ''
40
27
  const n = typeof count === 'number' ? count : Number(count)
41
28
  if (isNaN(n)) return String(count)
42
29
  const tokenWord = plural(n, ['токен', 'токена', 'токенов'])
43
- if (compact && n >= 1000) {
44
- return `${compactNumberFormat.format(n)} токенов`
30
+ if (compact && n >= 1000) {return `${compactNumberFormat.format(n)} токенов`
45
31
  }
46
32
  return `${numberFormat.format(n)} ${tokenWord}`
47
33
  }
48
34
  const currencyFormats = new Map()
49
- const getCurrencyFormat = (cur) => {
50
- const c = (cur || 'RUB').toUpperCase()
51
- if (!currencyFormats.has(c)) {
52
- try {
53
- currencyFormats.set(c, new Intl.NumberFormat('ru-RU', { style: 'currency', currency: c }))
54
- } catch (e) {
55
- currencyFormats.set(c, numberFormat)
35
+ const getCurrencyFormat = (cur) => {const c = (cur || 'RUB').toUpperCase()
36
+ if (!currencyFormats.has(c)) {try {currencyFormats.set(c, new Intl.NumberFormat('ru-RU', { style: 'currency', currency: c }))
37
+ } catch (e) {currencyFormats.set(c, numberFormat)
56
38
  }
57
39
  }
58
40
  return currencyFormats.get(c)
59
41
  }
60
- const formatNumber = (val) => {
61
- if (val === null || val === undefined || val === '') return ''
42
+ const formatNumber = (val) => {if (val === null || val === undefined || val === '') return ''
62
43
  const n = typeof val === 'number' ? val : Number(val)
63
44
  return isNaN(n) ? String(val) : numberFormat.format(n)
64
45
  }
65
- const formatCurrency = (val, cur) => {
66
- if (val === null || val === undefined || val === '') return ''
46
+ const formatCurrency = (val, cur) => {if (val === null || val === undefined || val === '') return ''
67
47
  const n = typeof val === 'number' ? val : Number(val)
68
48
  if (isNaN(n)) return String(val)
69
49
  return getCurrencyFormat(cur).format(n)
70
50
  }
71
- const formatRelativeTime = (val, unit) => {
72
- if (val === null || val === undefined || val === '') return ''
73
- if (typeof val === 'number' && typeof unit === 'string') {
74
- return relativeTimeFormat.format(val, unit)
51
+ const formatRelativeTime = (val, unit) => {if (val === null || val === undefined || val === '') return ''
52
+ if (typeof val === 'number' && typeof unit === 'string') {return relativeTimeFormat.format(val, unit)
75
53
  }
76
54
  const ts = val instanceof Date ? val.getTime() : (typeof val === 'number' ? (val < 1e12 ? val * 1000 : val) : Number(val))
77
55
  if (isNaN(ts)) return String(val)
@@ -84,69 +62,45 @@ if (absSec < 2592000) return relativeTimeFormat.format(Math.round(diffSec / 8640
84
62
  if (absSec < 31536000) return relativeTimeFormat.format(Math.round(diffSec / 2592000), 'month')
85
63
  return relativeTimeFormat.format(Math.round(diffSec / 31536000), 'year')
86
64
  }
87
- const INFLECT_CUSTOM = {
88
- 'пользователь': { gen: 'пользователя', dat: 'пользователю', acc: 'пользователя', ins: 'пользователем', pre: 'пользователе' },
89
- 'агент': { gen: 'агента', dat: 'агенту', acc: 'агента', ins: 'агентом', pre: 'агенте' },
90
- 'субагент': { gen: 'субагента', dat: 'субагенту', acc: 'субагента', ins: 'субагентом', pre: 'субагенте' },
91
- 'модель': { gen: 'модели', dat: 'модели', acc: 'модель', ins: 'моделью', pre: 'модели' },
92
- 'промпт': { gen: 'промпта', dat: 'промпту', acc: 'промпт', ins: 'промптом', pre: 'промпте' },
93
- 'инструмент': { gen: 'инструмента', dat: 'инструменту', acc: 'инструмент', ins: 'инструментом', pre: 'инструменте' },
94
- 'сессия': { gen: 'сессии', dat: 'сессии', acc: 'сессию', ins: 'сессией', pre: 'сессии' },
95
- 'ветка': { gen: 'ветки', dat: 'ветке', acc: 'ветку', ins: 'веткой', pre: 'ветке' },
96
- 'файл': { gen: 'файла', dat: 'файлу', acc: 'файл', ins: 'файлом', pre: 'файле' },
97
- 'папка': { gen: 'папки', dat: 'папке', acc: 'папку', ins: 'папкой', pre: 'папке' }
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: 'папке' }
98
66
  }
99
67
  const keepCase = (src, out) => (src[0] === src[0].toUpperCase() ? out[0].toUpperCase() + out.slice(1) : out)
100
- const inflectWord = (word, cName) => {
101
- if (!word || typeof word !== 'string') return word
68
+ const inflectWord = (word, cName) => {if (!word || typeof word !== 'string') return word
102
69
  const lower = word.toLowerCase()
103
- if (INFLECT_CUSTOM[lower] && INFLECT_CUSTOM[lower][cName]) {
104
- return keepCase(word, INFLECT_CUSTOM[lower][cName])
70
+ if (INFLECT_CUSTOM[lower] && INFLECT_CUSTOM[lower][cName]) {return keepCase(word, INFLECT_CUSTOM[lower][cName])
105
71
  }
106
72
  if (/[a-zA-Z0-9_-]/.test(word) || /^[А-ЯЁ]{2,}$/.test(word)) return word
107
73
  if (/[оеиую]$/i.test(word) && !/(ко|ло|но|то|во|ро|до|по|со|мо|го)$/i.test(word)) return word
108
74
  const w = lower
109
75
  const endings = [
110
- ['ия', 2, (s) => ({ gen: s + 'ии', dat: s + 'ии', acc: s + 'ию', ins: s + 'ией', pre: s + 'ии' })],
111
- ['а', 1, (s) => {
112
- const genEnd = /[гкхжшчщ]/.test(s.slice(-1)) ? 'и' : 'ы'
76
+ ['ия', 2, (s) => ({ gen: s + 'ии', dat: s + 'ии', acc: s + 'ию', ins: s + 'ией', pre: s + 'ии' })],['а', 1, (s) => {const genEnd = /[гкхжшчщ]/.test(s.slice(-1)) ? 'и' : 'ы'
113
77
  return { gen: s + genEnd, dat: s + 'е', acc: s + 'у', ins: s + 'ой', pre: s + 'е' }
114
- }],
115
- ['я', 1, (s) => ({ gen: s + 'и', dat: s + 'е', acc: s + 'ю', ins: s + 'ей', pre: s + 'е' })],
116
- ['ь', 1, (s) => ({ gen: s + 'и', dat: s + 'и', acc: s + 'ь', ins: s + 'ью', pre: s + 'и' })],
117
- ['й', 1, (s) => ({ gen: s + 'я', 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 + 'е' })]
118
79
  ]
119
- for (const [suffix, cut, build] of endings) {
120
- if (w.endsWith(suffix)) return keepCase(word, build(w.slice(0, -cut))[cName] || w)
80
+ for (const [suffix, cut, build] of endings) {if (w.endsWith(suffix)) return keepCase(word, build(w.slice(0, -cut))[cName] || w)
121
81
  }
122
- if (/[бвгджзклмнпрстфхцчшщ]$/.test(w)) {
123
- const map = { gen: w + 'а', dat: w + 'у', acc: w, ins: w + 'ом', pre: w + 'е' }
82
+ if (/[бвгджзклмнпрстфхцчшщ]$/.test(w)) {const map = { gen: w + 'а', dat: w + 'у', acc: w, ins: w + 'ом', pre: w + 'е' }
124
83
  return keepCase(word, map[cName] || w)
125
84
  }
126
85
  return word
127
86
  }
128
87
  const INFLECT_CASES = new Set(['gen', 'dat', 'acc', 'ins', 'pre'])
129
- const inflect = (phrase, cName) => {
130
- if (!phrase || typeof phrase !== 'string') return phrase
88
+ const inflect = (phrase, cName) => {if (!phrase || typeof phrase !== 'string') return phrase
131
89
  if (!INFLECT_CASES.has(cName)) return phrase
132
90
  return phrase.split(' ').map((w) => inflectWord(w, cName)).join(' ')
133
91
  }
134
92
  const pluralRules = new Intl.PluralRules('ru-RU')
135
93
  const pluralForm = (n) => pluralRules.select(n)
136
- function plural(count, forms, withCount = false) {
137
- const n = typeof count === 'number' ? count : (Number(count) || 0)
94
+ function plural(count, forms, withCount = false) {const n = typeof count === 'number' ? count : (Number(count) || 0)
138
95
  let one = '', few = '', many = ''
139
- if (Array.isArray(forms)) {
140
- one = forms[0] || ''
96
+ if (Array.isArray(forms)) {one = forms[0] || ''
141
97
  few = forms[1] || one
142
98
  many = forms[2] || few || one
143
- } else if (typeof forms === 'string') {
144
- one = forms
99
+ } else if (typeof forms === 'string') {one = forms
145
100
  few = arguments[2] || one
146
101
  many = arguments[3] || few || one
147
102
  withCount = arguments[4] || false
148
- } else {
149
- return ''
103
+ } else {return ''
150
104
  }
151
105
  const form = pluralRules.select(n)
152
106
  let word = many
@@ -155,10 +109,8 @@ else if (form === 'few') word = few
155
109
  else if (form === 'many') word = many
156
110
  return withCount ? `${formatNumber(n)} ${word}` : word
157
111
  }
158
- const fill = (template, params) => {
159
- if (!params || typeof params !== 'object') return String(template)
160
- return String(template).replace(/\{(\w+)(?::([^}]+))?\}/g, (match, name, spec) => {
161
- if (!(name in params)) return match
112
+ const fill = (template, params) => {if (!params || typeof params !== 'object') return String(template)
113
+ return String(template).replace(/\{(\w+)(?::([^}]+))?\}/g, (match, name, spec) => {if (!(name in params)) return match
162
114
  const val = params[name]
163
115
  if (!spec) return String(val)
164
116
  if (spec === 'number') return formatNumber(val)
@@ -169,16 +121,14 @@ if (spec === 'reltime') return formatRelativeTime(val)
169
121
  if (spec === 'currency') return formatCurrency(val, params.currency || 'RUB')
170
122
  if (spec === 'date') return formatDate(val)
171
123
  if (spec.startsWith('date:')) return formatDate(val, spec.slice(5))
172
- if (spec.startsWith('plural:')) {
173
- const parts = spec.slice(7).split(',')
124
+ if (spec.startsWith('plural:')) {const parts = spec.slice(7).split(',')
174
125
  return plural(val, parts)
175
126
  }
176
127
  if (INFLECT_CASES.has(spec)) return inflect(String(val), spec)
177
128
  return String(val)
178
129
  })
179
130
  }
180
- const stemRussian = (word) => {
181
- if (!word || typeof word !== 'string') return ''
131
+ const stemRussian = (word) => {if (!word || typeof word !== 'string') return ''
182
132
  let w = word.toLowerCase().trim()
183
133
  if (w.length < 4) return w
184
134
  w = w.replace(/(?:вшись|вши|ившись|ивши|ывшись|ывши|ив|ыв)$/, '')
@@ -188,14 +138,10 @@ w = w.replace(/(?:ила|ыла|ена|ейте|уйте|ите|или|ыли|е
188
138
  w = w.replace(/(?:ами|ями|иями|ией|иям|ием|ах|ях|иях|ев|ов|ие|ье|ей|ой|ий|ям|ем|ам|ом|а|е|и|о|у|ы|ь|ю|я)$/, '')
189
139
  return w.length >= 2 ? w : word.toLowerCase()
190
140
  }
191
- const EN_RU_KEYS = {
192
- 'q': 'й', 'w': 'ц', 'e': 'у', 'r': 'к', 't': 'е', 'y': 'н', 'u': 'г', 'i': 'ш', 'o': 'щ', 'p': 'з', '[': 'х', ']': 'ъ',
193
- 'a': 'ф', 's': 'ы', 'd': 'в', 'f': 'а', 'g': 'п', 'h': 'р', 'j': 'о', 'k': 'л', 'l': 'д', ';': 'ж', "'": 'э',
194
- 'z': 'я', 'x': 'ч', 'c': 'с', 'v': 'м', 'b': 'и', 'n': 'т', 'm': 'ь', ',': 'б', '.': 'ю'
141
+ const EN_RU_KEYS = {'q': 'й', 'w': 'ц', 'e': 'у', 'r': 'к', 't': 'е', 'y': 'н', 'u': 'г', 'i': 'ш', 'o': 'щ', 'p': 'з', '[': 'х', ']': 'ъ','a': 'ф', 's': 'ы', 'd': 'в', 'f': 'а', 'g': 'п', 'h': 'р', 'j': 'о', 'k': 'л', 'l': 'д', ';': 'ж', "'": 'э','z': 'я', 'x': 'ч', 'c': 'с', 'v': 'м', 'b': 'и', 'n': 'т', 'm': 'ь', ',': 'б', '.': 'ю'
195
142
  }
196
143
  const translitEnToRu = (str) => str.toLowerCase().split('').map((c) => EN_RU_KEYS[c] || c).join('')
197
- const fuzzyMatchRu = (query, target) => {
198
- if (!query || !target) return 0
144
+ const fuzzyMatchRu = (query, target) => {if (!query || !target) return 0
199
145
  const q = query.toLowerCase().trim()
200
146
  const t = target.toLowerCase().trim()
201
147
  if (t === q) return 100
@@ -204,44 +150,17 @@ if (t.includes(translitEnToRu(q))) return 85
204
150
  const qStems = q.split(/\s+/).map(stemRussian).filter(Boolean)
205
151
  const tStems = t.split(/\s+/).map(stemRussian).filter(Boolean)
206
152
  let matched = 0
207
- for (const qs of qStems) {
208
- if (tStems.some((ts) => ts.startsWith(qs) || qs.startsWith(ts))) matched++
153
+ for (const qs of qStems) {if (tStems.some((ts) => ts.startsWith(qs) || qs.startsWith(ts))) matched++
209
154
  }
210
155
  if (matched === qStems.length && qStems.length > 0) return 80
211
156
  if (matched > 0) return 50
212
157
  return 0
213
158
  }
214
- const ERROR_MAP = {
215
- ENOENT: { title: 'Файл не найден', message: 'Указанный файл или директория не существуют', hint: 'Проверьте правильность указанного пути к файлу.' },
216
- EACCES: { title: 'Отказано в доступе', message: 'Недостаточно прав для чтения или записи', hint: 'Проверьте права доступа к файлу или директории (chmod/chown).' },
217
- EPERM: { title: 'Операция запрещена', message: 'Недостаточно системных привилегий', hint: 'Запустите процесс с соответствующими правами.' },
218
- ECONNREFUSED: { title: 'Соединение отклонено', message: 'Целевой сервер или сервис не отвечает', hint: 'Убедитесь, что локальный или удаленный сервис запущен и слушает порт.' },
219
- ECONNRESET: { title: 'Сброс соединения', message: 'Соединение было принудительно разорвано удаленной стороной', hint: 'Проверьте стабильность сети и работу целевого сервера.' },
220
- ETIMEDOUT: { title: 'Таймаут соединения', message: 'Превышено время ожидания ответа', hint: 'Проверьте стабильность сети или увеличьте лимит ожидания.' },
221
- ENOTFOUND: { title: 'Хост не найден', message: 'Не удалось разрешить сетевой адрес', hint: 'Проверьте правильность URL или настройки DNS.' },
222
- EADDRINUSE: { title: 'Порт уже занят', message: 'Сетевой порт используется другим процессом', hint: 'Остановите конфликтующий процесс или выберите другой порт.' },
223
- ENOSPC: { title: 'Недостаточно места на диске', message: 'На устройстве закончилось свободное пространство', hint: 'Освободите место на диске и повторите операцию.' },
224
- 400: { title: 'Некорректный запрос (Bad Request)', message: 'Параметры запроса не соответствуют ожидаемому формату', hint: 'Проверьте синтаксис команды или переданные аргументы.' },
225
- 401: { title: 'Требуется авторизация', message: 'API-ключ или токен отсутствуют или недействительны', hint: 'Проверьте настройки учетных данных и актуальность токена.' },
226
- 403: { title: 'Доступ запрещен', message: 'Недостаточно прав для выполнения операции', hint: 'Проверьте область действия токена или права роли.' },
227
- 404: { title: 'Ресурс не найден', message: 'Запрошенный адрес или объект не существует', hint: 'Проверьте правильность пути или идентификатора ресурса.' },
228
- 429: { title: 'Превышен лимит запросов', message: 'Слишком много запросов (Rate Limit)', hint: 'Подождите несколько минут перед повторным запросом.' },
229
- 500: { title: 'Внутренняя ошибка сервера', message: 'На стороне сервера произошел сбой', hint: 'Попробуйте повторить запрос позже или проверьте серверные логи.' },
230
- 502: { title: 'Ошибочный шлюз (Bad Gateway)', message: 'Промежуточный прокси не получил корректный ответ', hint: 'Проверьте работу нижележащей службы или upstream-сервера.' },
231
- 503: { title: 'Служба временно недоступна', message: 'Сервер перегружен или находится на обслуживании', hint: 'Попробуйте повторить операцию через некоторое время.' },
232
- 504: { title: 'Шлюз не отвечает (Gateway Timeout)', message: 'Превышено время ожидания ответа от upstream-сервера', hint: 'Попробуйте повторить запрос позже.' },
233
- FETCH_FAILED: { title: 'Сетевой запрос не удался', message: 'Не удалось связаться с сервером (Failed to fetch)', hint: 'Проверьте сетевое подключение и доступность адреса.' },
234
- CORS_ERROR: { title: 'Ошибка CORS', message: 'Запрос заблокирован политикой безопасности браузера', hint: 'Проверьте заголовки разрешённых источников на сервере.' },
235
- WS_CLOSED: { title: 'Разрыв WebSocket', message: 'Связь с сервером по WebSocket прервана', hint: 'Проверьте работу сервера и сетевой статус.' },
236
- QUOTA_EXCEEDED: { title: 'Исчерпана квота API', message: 'Превышен лимит запросов или баланс провайдера', hint: 'Проверьте баланс API-ключа в кабинете провайдера.' },
237
- CONTEXT_OVERFLOW: { title: 'Превышен контекст', message: 'Объём диалога превышает размер контекстного окна', hint: 'Очистите или сожмите историю сообщений сессии.' },
238
- MODEL_NOT_FOUND: { title: 'Модель не найдена', message: 'Указанная модель отсутствует или недоступна', hint: 'Проверьте идентификатор модели в настройках.' }
239
- }
240
- const fromMap = (key, rawMsg) => ({
241
- code: String(key), title: ERROR_MAP[key].title, message: ERROR_MAP[key].message, hint: ERROR_MAP[key].hint, raw: rawMsg
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
+ }
161
+ const fromMap = (key, rawMsg) => ({code: String(key), title: ERROR_MAP[key].title, message: ERROR_MAP[key].message, hint: ERROR_MAP[key].hint, raw: rawMsg
242
162
  })
243
- const humanizeError = (err) => {
244
- if (!err) return null
163
+ const humanizeError = (err) => {if (!err) return null
245
164
  const rawMsg = typeof err === 'string' ? err : (err.message || String(err))
246
165
  const code = err.code || (rawMsg.match(/\b(E[A-Z]{2,20})\b/) || [])[1]
247
166
  const status = err.status || err.statusCode || (err.response && err.response.status) || (rawMsg.match(/\b([45]\d{2})\b/) || [])[1]
@@ -257,45 +176,26 @@ if (/context.*(?:exceeded|overflow|length)/i.test(rawMsg)) return fromMap('CONTE
257
176
  if (/model.*not.*found|model.*does not exist/i.test(rawMsg)) return fromMap('MODEL_NOT_FOUND', rawMsg)
258
177
  return { code: 'UNKNOWN', title: 'Ошибка операции', message: rawMsg, hint: 'Проверьте параметры операции и логи.', raw: rawMsg }
259
178
  }
260
- const formatErrorToast = (err) => {
261
- const h = humanizeError(err)
179
+ const formatErrorToast = (err) => {const h = humanizeError(err)
262
180
  if (!h) return null
263
- return {
264
- type: 'error',
265
- title: h.title,
266
- description: h.message + (h.hint ? ' ' + h.hint : '')
181
+ return {type: 'error',title: h.title,description: h.message + (h.hint ? ' ' + h.hint : '')
267
182
  }
268
183
  }
269
- const makeIssueUrl = (opts = {}, version = '') => {
270
- const repo = 'GooDAnDReaDY/dsh-russian-lang'
184
+ const makeIssueUrl = (opts = {}, version = '') => {const repo = 'GooDAnDReaDY/dsh-russian-lang'
271
185
  const title = opts.title || (opts.plugin
272
186
  ? `[Перевод] Запрос локализации для плагина ${opts.plugin}`
273
187
  : '[Ошибка перевода] Неточный перевод фразы')
274
188
  const bodyLines = [
275
- '### Описание проблемы',
276
- opts.description || (opts.plugin
189
+ '### Описание проблемы',opts.description || (opts.plugin
277
190
  ? `Просьба добавить русскую локализацию для плагина \`${opts.plugin}\`.`
278
- : 'Обнаружена неточность в переводе интерфейса.'),
279
- '',
280
- '### Технический контекст',
281
- opts.ns ? `- **Namespace**: \`${opts.ns}\`` : null,
282
- opts.key ? `- **Ключ**: \`${opts.key}\`` : null,
283
- opts.en ? `- **Оригинал (EN)**: ${opts.en}` : null,
284
- opts.ru ? `- **Текущий перевод (RU)**: ${opts.ru}` : null,
285
- opts.plugin ? `- **Плагин**: \`${opts.plugin}\`` : null,
286
- version ? `- **Версия dsh-russian-lang**: \`${version}\`` : null,
287
- typeof navigator !== 'undefined' ? `- **User Agent**: \`${navigator.userAgent}\`` : null,
288
- '',
289
- '### Предлагаемый вариант перевода',
290
- opts.proposal || '_Опишите ваш вариант перевода..._'
191
+ : 'Обнаружена неточность в переводе интерфейса.'),'','### Технический контекст',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 || '_Опишите ваш вариант перевода..._'
291
192
  ].filter(Boolean)
292
193
  const params = new URLSearchParams()
293
194
  params.set('title', title)
294
195
  params.set('body', bodyLines.join('\n'))
295
196
  return `https://github.com/${repo}/issues/new?` + params.toString()
296
197
  }
297
- const makePluginLocalizationStatus = (dicts) => (ns) => {
298
- const none = { status: 'none', count: 0, label: 'RU отсутствует' }
198
+ const makePluginLocalizationStatus = (dicts) => (ns) => {const none = { status: 'none', count: 0, label: 'RU отсутствует' }
299
199
  if (!ns) return none
300
200
  const count = Object.keys((dicts && dicts[ns]) || {}).length
301
201
  return count > 0 ? { status: 'full', count, label: `RU: ${count} строк` } : none
@@ -314,41 +214,32 @@ TYPO_SHORT.has(word) ? lead + word + ' ' : match
314
214
  ))
315
215
  const YO_EDGE_L = '(?<![а-яёА-ЯЁ])'
316
216
  const YO_EDGE_R = '(?![а-яёА-ЯЁ])'
317
- const yoCase = (src, repl) => {
318
- if (src === src.toUpperCase() && src !== src.toLowerCase()) return repl.toUpperCase()
217
+ const yoCase = (src, repl) => {if (src === src.toUpperCase() && src !== src.toLowerCase()) return repl.toUpperCase()
319
218
  if (src[0] === src[0].toUpperCase()) return repl[0].toUpperCase() + repl.slice(1)
320
219
  return repl
321
220
  }
322
- const makeTypoYo = (pairs) => {
323
- const list = typeof pairs === 'string'
221
+ const makeTypoYo = (pairs) => {const list = typeof pairs === 'string'
324
222
  ? pairs.split('|').filter(Boolean).map((s) => s.split(':'))
325
223
  : (pairs || [])
326
224
  const compiled = list.map((p) => [new RegExp(YO_EDGE_L + p[0] + YO_EDGE_R, 'gi'), p[1]])
327
- return (text) => {
328
- for (const [re, repl] of compiled) text = text.replace(re, (match) => yoCase(match, repl))
225
+ return (text) => {for (const [re, repl] of compiled) text = text.replace(re, (match) => yoCase(match, repl))
329
226
  return text
330
227
  }
331
228
  }
332
229
  const LAYOUT_LAT_TO_CYR = { ...EN_RU_KEYS, '/': '.', '`': 'ё' }
333
- const LAYOUT_CYR_TO_LAT = (() => {
334
- const out = {}
230
+ const LAYOUT_CYR_TO_LAT = (() => {const out = {}
335
231
  for (const k in LAYOUT_LAT_TO_CYR) out[LAYOUT_LAT_TO_CYR[k]] = k
336
232
  return out
337
233
  })()
338
- const translit = (word, map) => {
339
- let out = ''
234
+ const translit = (word, map) => {let out = ''
340
235
  for (const ch of word.toLowerCase()) out += map[ch] !== undefined ? map[ch] : ch
341
236
  return out
342
237
  }
343
- const makeLayout = (freq, localDict = new Set()) => {
344
- const ruWordFraction = (text) => {
345
- const words = text.toLowerCase().split(/[^а-яё]+/).filter(Boolean)
238
+ const makeLayout = (freq, localDict = new Set()) => {const ruWordFraction = (text) => {const words = text.toLowerCase().split(/[^а-яё]+/).filter(Boolean)
346
239
  if (!words.length) return 0
347
240
  return words.filter((w) => freq.has(w) || localDict.has(w)).length / words.length
348
241
  }
349
- const candidate = (value, direction) => {
350
- if (direction === 'lat2cyr') {
351
- const converted = translit(value, LAYOUT_LAT_TO_CYR)
242
+ const candidate = (value, direction) => {if (direction === 'lat2cyr') {const converted = translit(value, LAYOUT_LAT_TO_CYR)
352
243
  if (!/[а-яё]{2}/.test(converted)) return null
353
244
  if (ruWordFraction(converted) < 0.7) return null
354
245
  return { converted }
@@ -356,99 +247,66 @@ return { converted }
356
247
  const converted = translit(value, LAYOUT_CYR_TO_LAT)
357
248
  return converted.startsWith('/') ? { converted } : null
358
249
  }
359
- const learnWords = (text) => {
360
- for (const w of text.toLowerCase().split(/[^а-яё]+/).filter(Boolean)) {
361
- if (w.length >= 3) localDict.add(w)
250
+ const learnWords = (text) => {for (const w of text.toLowerCase().split(/[^а-яё]+/).filter(Boolean)) {if (w.length >= 3) localDict.add(w)
362
251
  }
363
252
  }
364
253
  return { ruWordFraction, candidate, learnWords, localDict }
365
254
  }
366
255
  const SHORT_WORDS_RE = /(^|[\s(«])([вВнНсСпПоОкКуУиИаА]|из|от|до|за|по|со|во|ко|об|на|под|над|при|про|без|для|не|ни|но)( )/g
367
- const formatInputLive = (text) => {
368
- if (!text || typeof text !== 'string') return text
256
+ const formatInputLive = (text) => {if (!text || typeof text !== 'string') return text
369
257
  const parts = text.split(/(```[\s\S]*?```|`[^`\n]*`)/g)
370
- for (let i = 0; i < parts.length; i += 2) {
371
- let s = parts[i]
258
+ for (let i = 0; i < parts.length; i += 2) {let s = parts[i]
372
259
  if (!s) continue
260
+ const lq = [], cf = []
261
+ s = s.replace(/"([^"\n]*[a-zA-Z][^"\n]*)"/g, (m) => `\x01L${lq.push(m) - 1}\x01`)
262
+ s = s.replace(/(^|\s)(--[a-zA-Z0-9_\u0400-\u04FF-]+)/g, (m, p1, p2) => `${p1}\x01C${cf.push(p2) - 1}\x01`)
263
+ s = s.replace(/(^|\s)(--)(?=\s|$)/g, (m, p1, p2) => /[a-zA-Z]/.test(text) ? `${p1}\x01C${cf.push(p2) - 1}\x01` : m)
373
264
  s = s.replace(/--/g, '—')
374
- s = s.replace(/(^|[\s([{-])"/g, '$1«')
375
- s = s.replace(/"/g, '»')
265
+ s = s.replace(/(^|[\s([{-])"/g, '$1«').replace(/"/g, '»')
376
266
  s = s.replace(SHORT_WORDS_RE, '$1$2\u00A0')
267
+ s = s.replace(/\x01C(\d+)\x01/g, (_, j) => cf[+j]).replace(/\x01L(\d+)\x01/g, (_, j) => lq[+j])
377
268
  parts[i] = s
378
269
  }
379
270
  return parts.join('')
380
271
  }
381
- const RUSSIAN_SLASH_ALIASES = {
382
- '/цель': '/goal',
383
- '/справка': '/help',
384
- '/задача': '/task',
385
- '/контекст': '/context',
386
- '/сжать': '/compact',
387
- '/план': '/plan',
388
- '/экспорт': '/export',
389
- '/отзыв': '/feedback',
390
- '/разрешение': '/permission',
391
- '/разрешения': '/permission',
392
- '/память': '/memory',
393
- '/помощь': '/help',
394
- '/очистить': '/clear',
395
- '/сессия': '/session',
396
- '/модель': '/model'
397
- }
398
- const expandSlashAlias = (input) => {
399
- if (!input || typeof input !== 'string' || !input.startsWith('/')) return input
272
+ const RUSSIAN_SLASH_ALIASES = {'/цель': '/goal','/справка': '/help','/задача': '/task','/контекст': '/context','/сжать': '/compact','/план': '/plan','/экспорт': '/export','/отзыв': '/feedback','/разрешение': '/permission','/разрешения': '/permission','/память': '/memory','/помощь': '/help','/очистить': '/clear','/сессия': '/session','/модель': '/model'
273
+ }
274
+ const expandSlashAlias = (input) => {if (!input || typeof input !== 'string' || !input.startsWith('/')) return input
400
275
  const match = input.match(/^(\s*\/[^\s]+)(.*)$/)
401
276
  if (!match) return input
402
277
  const [, cmd, rest] = match
403
278
  const lowerCmd = cmd.trim().toLowerCase()
404
- if (RUSSIAN_SLASH_ALIASES[lowerCmd]) {
405
- return RUSSIAN_SLASH_ALIASES[lowerCmd] + rest
279
+ if (RUSSIAN_SLASH_ALIASES[lowerCmd]) {return RUSSIAN_SLASH_ALIASES[lowerCmd] + rest
406
280
  }
407
281
  return input
408
282
  }
409
283
  const PHONETIC_LAT_TO_CYR = [
410
- ['shch', 'щ'], ['yo', 'ё'], ['zh', 'ж'], ['ch', 'ч'], ['sh', 'ш'],
411
- ['yu', 'ю'], ['ya', 'я'], ['ts', 'ц'],
412
- ['a', 'а'], ['b', 'б'], ['v', 'в'], ['g', 'г'], ['d', 'д'], ['e', 'е'],
413
- ['z', 'з'], ['i', 'и'], ['j', 'й'], ['k', 'к'], ['l', 'л'], ['m', 'м'],
414
- ['n', 'н'], ['o', 'о'], ['p', 'п'], ['r', 'р'], ['s', 'с'], ['t', 'т'],
415
- ['u', 'у'], ['f', 'ф'], ['h', 'х'], ['c', 'ц'], ['y', 'ы'], ['x', 'кс']
284
+ ['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', 'кс']
416
285
  ]
417
286
  const PHONETIC_CYR_TO_LAT = [
418
- ['щ', 'shch'], ['ё', 'yo'], ['ж', 'zh'], ['ч', 'ch'], ['ш', 'sh'],
419
- ['ю', 'yu'], ['я', 'ya'], ['ц', 'ts'],
420
- ['а', 'a'], ['б', 'b'], ['в', 'v'], ['г', 'g'], ['д', 'd'], ['е', 'e'],
421
- ['з', 'z'], ['и', 'i'], ['й', 'j'], ['к', 'k'], ['л', 'l'], ['м', 'm'],
422
- ['н', 'n'], ['о', 'o'], ['п', 'p'], ['р', 'r'], ['с', 's'], ['т', 't'],
423
- ['у', 'u'], ['ф', 'f'], ['х', 'h'], ['ы', 'y'], ['э', 'e'], ['ъ', ''], ['ь', '']
287
+ ['щ', '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'], ['ъ', ''], ['ь', '']
424
288
  ]
425
- const phoneticTranslit = (text, direction = 'lat2cyr') => {
426
- if (!text || typeof text !== 'string') return text
289
+ const phoneticTranslit = (text, direction = 'lat2cyr') => {if (!text || typeof text !== 'string') return text
427
290
  let res = text
428
291
  const pairs = direction === 'lat2cyr' ? PHONETIC_LAT_TO_CYR : PHONETIC_CYR_TO_LAT
429
- for (const [from, to] of pairs) {
430
- const fromUpper = from.toUpperCase()
292
+ for (const [from, to] of pairs) {const fromUpper = from.toUpperCase()
431
293
  const fromTitle = from[0].toUpperCase() + from.slice(1)
432
294
  const toUpper = to.toUpperCase()
433
295
  const toTitle = to[0] ? to[0].toUpperCase() + to.slice(1) : ''
434
- if (from.length > 1) {
435
- res = res.replaceAll(fromUpper, toUpper)
296
+ if (from.length > 1) {res = res.replaceAll(fromUpper, toUpper)
436
297
  res = res.replaceAll(fromTitle, toTitle)
437
298
  }
438
299
  res = res.replaceAll(from, to)
439
- if (from.length === 1) {
440
- res = res.replaceAll(fromUpper, toUpper)
300
+ if (from.length === 1) {res = res.replaceAll(fromUpper, toUpper)
441
301
  }
442
302
  }
443
303
  return res
444
304
  }
445
- const detectInputLayout = (text) => {
446
- if (!text || typeof text !== 'string') return null
305
+ const detectInputLayout = (text) => {if (!text || typeof text !== 'string') return null
447
306
  const clean = text.replace(/`[^`]*`/g, '').replace(/https?:\/\/\S+/g, '')
448
307
  let cyr = 0
449
308
  let lat = 0
450
- for (const ch of clean) {
451
- const code = ch.charCodeAt(0)
309
+ for (const ch of clean) {const code = ch.charCodeAt(0)
452
310
  if ((code >= 0x0400 && code <= 0x04FF) || code === 0x0500) cyr++
453
311
  else if ((code >= 0x41 && code <= 0x5A) || (code >= 0x61 && code <= 0x7A)) lat++
454
312
  }
@@ -456,51 +314,18 @@ if (cyr > lat && cyr >= 2) return 'RU'
456
314
  if (lat > cyr && lat >= 2) return 'EN'
457
315
  return null
458
316
  }
459
- const SYSTEM_PROMPT_PRESETS = {
460
- technical_expert: {
461
- id: 'technical_expert',
462
- label: 'Технический эксперт',
463
- desc: 'Точная терминология, чистый код и русские комментарии',
464
- text: 'Отвечай пользователю на русском языке. Используй точную инженерную терминологию, пиши чистый код и русские комментарии.'
465
- },
466
- tech_writer: {
467
- id: 'tech_writer',
468
- label: 'Технический писатель',
469
- desc: 'Структурированные тексты, Markdown и выверенная типографика',
470
- text: 'Отвечай пользователю на русском языке. Оформляй документацию в Markdown, строго соблюдай правила русской типографики («ёлочки», тире, «ё»).'
471
- },
472
- concise: {
473
- id: 'concise',
474
- label: 'Лаконичный режим',
475
- desc: 'Краткие ёмкие ответы без вводных слов и воды',
476
- text: 'Отвечай пользователю на русском языке максимально кратко и ёмко. Без вводных слов и воды, сразу код или решение.'
477
- },
478
- code_reviewer: {
479
- id: 'code_reviewer',
480
- label: 'Код-ревьюер',
481
- desc: 'Поиск багов, безопасность и архитектурный аудит',
482
- text: 'Анализируй код и отвечай на русском языке. Ищи уязвимости, граничные случаи и нарушения контрактов, предлагая исправления.'
483
- },
484
- architect: {
485
- id: 'architect',
486
- label: 'Системный архитектор',
487
- desc: 'Контракты API/CLI, модульность и надёжность',
488
- text: 'Отвечай на русском языке как архитектор. Проектируй модульные системы, чёткие контракты API/CLI и надёжные решения без лишней сложности.'
489
- },
490
- tutor: {
491
- id: 'tutor',
492
- label: 'Наставник (ментор)',
493
- desc: 'Понятные объяснения сложных концепций и пошаговый разбор',
494
- text: 'Отвечай на русском языке как наставник. Объясняй сложные темы простыми словами, наглядными аналогиями и пошаговыми примерами.'
495
- }
496
- }
497
- const exportSessionToMarkdown = (session, options = {}) => {
498
- if (!session) return ''
317
+ const SYSTEM_PROMPT_PRESETS = {technical_expert: {id: 'technical_expert',label: 'Технический эксперт',desc: 'Точная терминология, чистый код и русские комментарии',text: 'Отвечай пользователю на русском языке. Используй точную инженерную терминологию, пиши чистый код и русские комментарии.'
318
+ },tech_writer: {id: 'tech_writer',label: 'Технический писатель',desc: 'Структурированные тексты, Markdown и выверенная типографика',text: 'Отвечай пользователю на русском языке. Оформляй документацию в Markdown, строго соблюдай правила русской типографики («ёлочки», тире, «ё»).'
319
+ },concise: {id: 'concise',label: 'Лаконичный режим',desc: 'Краткие ёмкие ответы без вводных слов и воды',text: 'Отвечай пользователю на русском языке максимально кратко и ёмко. Без вводных слов и воды, сразу код или решение.'
320
+ },code_reviewer: {id: 'code_reviewer',label: 'Код-ревьюер',desc: 'Поиск багов, безопасность и архитектурный аудит',text: 'Анализируй код и отвечай на русском языке. Ищи уязвимости, граничные случаи и нарушения контрактов, предлагая исправления.'
321
+ },architect: {id: 'architect',label: 'Системный архитектор',desc: 'Контракты API/CLI, модульность и надёжность',text: 'Отвечай на русском языке как архитектор. Проектируй модульные системы, чёткие контракты API/CLI и надёжные решения без лишней сложности.'
322
+ },tutor: {id: 'tutor',label: 'Наставник (ментор)',desc: 'Понятные объяснения сложных концепций и пошаговый разбор',text: 'Отвечай на русском языке как наставник. Объясняй сложные темы простыми словами, наглядными аналогиями и пошаговыми примерами.'
323
+ }
324
+ }
325
+ const exportSessionToMarkdown = (session, options = {}) => {if (!session) return ''
499
326
  const title = session.title || session.name || 'Диалог DSH'
500
327
  const date = session.createdAt ? new Date(session.createdAt) : new Date()
501
- const dateStr = new Intl.DateTimeFormat('ru-RU', {
502
- dateStyle: 'full',
503
- timeStyle: 'medium'
328
+ const dateStr = new Intl.DateTimeFormat('ru-RU', {dateStyle: 'full',timeStyle: 'medium'
504
329
  }).format(date)
505
330
  const messages = Array.isArray(session.messages) ? session.messages : []
506
331
  let md = `# 💬 ${title}\n\n`
@@ -508,20 +333,16 @@ md += `> **Дата экспорта:** ${dateStr}\n`
508
333
  if (session.model) md += `> **Модель:** \`${session.model}\`\n`
509
334
  if (session.workspace) md += `> **Рабочая область:** \`${session.workspace}\`\n`
510
335
  md += `> **Всего сообщений:** ${messages.length}\n\n---\n\n`
511
- for (let idx = 0; idx < messages.length; idx++) {
512
- const msg = messages[idx]
336
+ for (let idx = 0; idx < messages.length; idx++) {const msg = messages[idx]
513
337
  const role = msg.role || 'unknown'
514
338
  const roleName = role === 'user' ? '👤 Пользователь' :
515
339
  role === 'assistant' ? '🤖 Ассистент' :
516
340
  role === 'system' ? '⚙️ Система' : `🔧 Инструмент (${role})`
517
341
  md += `### ${roleName}\n\n`
518
- if (msg.content) {
519
- md += `${msg.content}\n\n`
342
+ if (msg.content) {md += `${msg.content}\n\n`
520
343
  }
521
- if (Array.isArray(msg.toolCalls) && msg.toolCalls.length) {
522
- md += `*Вызовы инструментов:*\n`
523
- for (const tc of msg.toolCalls) {
524
- md += `- **${tc.name || 'tool'}**: \`${JSON.stringify(tc.arguments || {})}\`\n`
344
+ if (Array.isArray(msg.toolCalls) && msg.toolCalls.length) {md += `*Вызовы инструментов:*\n`
345
+ for (const tc of msg.toolCalls) {md += `- **${tc.name || 'tool'}**: \`${JSON.stringify(tc.arguments || {})}\`\n`
525
346
  }
526
347
  md += `\n`
527
348
  }
@@ -530,94 +351,56 @@ md += `---\n\n`
530
351
  md += `*Сгенерировано с помощью [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) и @goodandready/dsh-russian-lang*\n`
531
352
  return md
532
353
  }
533
- const findTranslationKey = (text, dicts = {}, overrides = {}, zhRu = {}, domEn = {}, options = {}) => {
534
- if (!text || typeof text !== 'string') return null
354
+ const findTranslationKey = (text, dicts = {}, overrides = {}, zhRu = {}, domEn = {}, options = {}) => {if (!text || typeof text !== 'string') return null
535
355
  const trimmed = text.trim()
536
356
  if (!trimmed) return null
537
- if (overrides && typeof overrides === 'object') {
538
- if (overrides[trimmed] !== undefined) {
539
- return { key: trimmed, value: overrides[trimmed], source: 'override' }
357
+ if (overrides && typeof overrides === 'object') {if (overrides[trimmed] !== undefined) {return { key: trimmed, value: overrides[trimmed], source: 'override' }
540
358
  }
541
- for (const [k, v] of Object.entries(overrides)) {
542
- if (v === text || (typeof v === 'string' && v.trim() === trimmed)) {
543
- return { key: k, value: v, source: 'override' }
359
+ for (const [k, v] of Object.entries(overrides)) {if (v === text || (typeof v === 'string' && v.trim() === trimmed)) {return { key: k, value: v, source: 'override' }
544
360
  }
545
361
  }
546
362
  }
547
- if (dicts && typeof dicts === 'object') {
548
- for (const [ns, map] of Object.entries(dicts)) {
549
- if (!map || typeof map !== 'object') continue
550
- if (map[trimmed] !== undefined) {
551
- return { ns, key: trimmed, value: map[trimmed], source: 'dictionary' }
363
+ if (dicts && typeof dicts === 'object') {for (const [ns, map] of Object.entries(dicts)) {if (!map || typeof map !== 'object') continue
364
+ if (map[trimmed] !== undefined) {return { ns, key: trimmed, value: map[trimmed], source: 'dictionary' }
552
365
  }
553
- for (const [k, v] of Object.entries(map)) {
554
- if (v === text || (typeof v === 'string' && v.trim() === trimmed)) {
555
- return { ns, key: k, value: v, source: 'dictionary' }
366
+ 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' }
556
367
  }
557
368
  }
558
369
  }
559
370
  }
560
- if (zhRu && typeof zhRu === 'object') {
561
- for (const [zh, ru] of Object.entries(zhRu)) {
562
- if (ru === text || (typeof ru === 'string' && ru.trim() === trimmed)) {
563
- return { zh, key: zh, value: ru, source: 'dom_zh' }
371
+ 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' }
564
372
  }
565
373
  }
566
- if (zhRu[trimmed] !== undefined) {
567
- return { zh: trimmed, key: trimmed, value: zhRu[trimmed], source: 'dom_zh_original' }
374
+ if (zhRu[trimmed] !== undefined) {return { zh: trimmed, key: trimmed, value: zhRu[trimmed], source: 'dom_zh_original' }
568
375
  }
569
376
  }
570
- if (domEn && typeof domEn === 'object') {
571
- for (const [en, ru] of Object.entries(domEn)) {
572
- if (ru === text || (typeof ru === 'string' && ru.trim() === trimmed)) {
573
- return { en, key: en, value: ru, source: 'dom_en' }
377
+ 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' }
574
378
  }
575
379
  }
576
- if (domEn[trimmed] !== undefined) {
577
- return { en: trimmed, key: trimmed, value: domEn[trimmed], source: 'dom_en_original' }
380
+ if (domEn[trimmed] !== undefined) {return { en: trimmed, key: trimmed, value: domEn[trimmed], source: 'dom_en_original' }
578
381
  }
579
382
  }
580
- if (options && options.detectUntranslated) {
581
- const hasRu = /[а-яё]/i.test(trimmed)
383
+ if (options && options.detectUntranslated) {const hasRu = /[а-яё]/i.test(trimmed)
582
384
  const hasZh = /[㐀-鿿豈-﫿]/.test(trimmed)
583
385
  const hasEn = /[a-z]/i.test(trimmed)
584
- if (!hasRu && hasZh) {
585
- return { key: trimmed, value: trimmed, source: 'untranslated_zh' }
386
+ if (!hasRu && hasZh) {return { key: trimmed, value: trimmed, source: 'untranslated_zh' }
586
387
  }
587
- if (!hasRu && hasEn) {
588
- return { key: trimmed, value: trimmed, source: 'untranslated_en' }
388
+ if (!hasRu && hasEn) {return { key: trimmed, value: trimmed, source: 'untranslated_en' }
589
389
  }
590
390
  }
591
391
  return null
592
392
  }
593
- const generateBugReportSnippet = (info = {}) => {
594
- const { key, ns, original, current, override, pkgVersion = '0.3.1' } = info
393
+ const generateBugReportSnippet = (info = {}) => {const { key, ns, original, current, override, pkgVersion = '0.3.1' } = info
595
394
  const lines = [
596
- '### 🌐 Неточность перевода / Пользовательское предложение',
597
- '- **Пакет:** `@goodandready/dsh-russian-lang@v' + pkgVersion + '`',
598
- ns ? '- **Пространство имён:** `' + ns + '`' : null,
599
- key ? '- **Ключ:** `' + key + '`' : null,
600
- original ? '- **Оригинальный текст:** `' + original + '`' : null,
601
- current ? '- **Текущий перевод:** `' + current + '`' : null,
602
- override ? '- **Предлагаемый перевод:** `' + override + '`' : null,
603
- ].filter(Boolean)
395
+ '### 🌐 Неточность перевода / Пользовательское предложение','- **Пакет:** `@goodandready/dsh-russian-lang@v' + pkgVersion + '`',ns ? '- **Пространство имён:** `' + ns + '`' : null,key ? '- **Ключ:** `' + key + '`' : null,original ? '- **Оригинальный текст:** `' + original + '`' : null,current ? '- **Текущий перевод:** `' + current + '`' : null,override ? '- **Предлагаемый перевод:** `' + override + '`' : null,].filter(Boolean)
604
396
  return lines.join('\n')
605
397
  }
606
- const bilingualCommandMatch = (query, command, options = {}) => {
607
- if (!query || !command) return { matched: false, score: 0 }
398
+ const bilingualCommandMatch = (query, command, options = {}) => {if (!query || !command) return { matched: false, score: 0 }
608
399
  const q = String(query).trim().toLowerCase()
609
400
  if (!q) return { matched: false, score: 0 }
610
401
  const cmdObj = typeof command === 'string' ? { title: command } : command
611
402
  const candidates = [
612
- cmdObj.title,
613
- cmdObj.label,
614
- cmdObj.name,
615
- cmdObj.originalTitle,
616
- cmdObj.enTitle,
617
- cmdObj.id,
618
- cmdObj.description,
619
- cmdObj.category,
620
- ...(Array.isArray(cmdObj.keywords) ? cmdObj.keywords : [])
403
+ cmdObj.title,cmdObj.label,cmdObj.name,cmdObj.originalTitle,cmdObj.enTitle,cmdObj.id,cmdObj.description,cmdObj.category,...(Array.isArray(cmdObj.keywords) ? cmdObj.keywords : [])
621
404
  ].filter((c) => typeof c === 'string' && c.trim())
622
405
  if (!candidates.length) return { matched: false, score: 0 }
623
406
  const queryVariants = [{ text: q, kind: 'direct' }]
@@ -626,155 +409,107 @@ if (lat2cyr !== q) queryVariants.push({ text: lat2cyr, kind: 'layout' })
626
409
  const cyr2lat = translit(q, LAYOUT_CYR_TO_LAT)
627
410
  if (cyr2lat !== q) queryVariants.push({ text: cyr2lat, kind: 'layout' })
628
411
  const phonLat2Cyr = phoneticTranslit(q, 'lat2cyr')
629
- if (phonLat2Cyr && phonLat2Cyr !== q && phonLat2Cyr !== lat2cyr) {
630
- queryVariants.push({ text: phonLat2Cyr, kind: 'phonetic' })
412
+ if (phonLat2Cyr && phonLat2Cyr !== q && phonLat2Cyr !== lat2cyr) {queryVariants.push({ text: phonLat2Cyr, kind: 'phonetic' })
631
413
  }
632
414
  const phonCyr2Lat = phoneticTranslit(q, 'cyr2lat')
633
- if (phonCyr2Lat && phonCyr2Lat !== q && phonCyr2Lat !== cyr2lat) {
634
- queryVariants.push({ text: phonCyr2Lat, kind: 'phonetic' })
415
+ if (phonCyr2Lat && phonCyr2Lat !== q && phonCyr2Lat !== cyr2lat) {queryVariants.push({ text: phonCyr2Lat, kind: 'phonetic' })
635
416
  }
636
417
  let bestScore = 0
637
418
  let bestVariant = null
638
419
  let bestCandidate = null
639
- for (const variant of queryVariants) {
640
- for (const cand of candidates) {
641
- const score = fuzzyMatchRu(variant.text, cand)
642
- if (score > bestScore) {
643
- bestScore = score
420
+ for (const variant of queryVariants) {for (const cand of candidates) {const score = fuzzyMatchRu(variant.text, cand)
421
+ if (score > bestScore) {bestScore = score
644
422
  bestVariant = variant
645
423
  bestCandidate = cand
646
424
  }
647
425
  }
648
426
  }
649
427
  const threshold = options.threshold ?? 50
650
- return {
651
- matched: bestScore >= threshold,
652
- score: bestScore,
653
- matchedText: bestCandidate,
654
- variant: bestVariant ? bestVariant.text : q,
655
- kind: bestVariant ? bestVariant.kind : 'none'
428
+ return {matched: bestScore >= threshold,score: bestScore,matchedText: bestCandidate,variant: bestVariant ? bestVariant.text : q,kind: bestVariant ? bestVariant.kind : 'none'
656
429
  }
657
430
  }
658
- const filterCommands = (query, commands = [], options = {}) => {
659
- if (!Array.isArray(commands)) return []
431
+ const filterCommands = (query, commands = [], options = {}) => {if (!Array.isArray(commands)) return []
660
432
  if (!query || !String(query).trim()) return commands
661
433
  const results = []
662
- for (const cmd of commands) {
663
- const res = bilingualCommandMatch(query, cmd, options)
664
- if (res.matched) {
665
- results.push({ item: cmd, ...res })
434
+ for (const cmd of commands) {const res = bilingualCommandMatch(query, cmd, options)
435
+ if (res.matched) {results.push({ item: cmd, ...res })
666
436
  }
667
437
  }
668
438
  return results.sort((a, b) => b.score - a.score).map((r) => r.item)
669
439
  }
670
-
671
- 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":"Магазин плагинов"}}
672
-
440
+ 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":"Магазин плагинов"}}
673
441
  const ZH_RU = {}
674
- 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 ↗ ] на блоках сообщений."}
442
+ 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 ↗ ] на блоках сообщений."}
675
443
  const SETTINGS_NS_NAME = 'russian-lang'
676
444
  const ROW_CONFIG_KEY = '@goodandready/dsh-russian-lang#russian-lang'
677
- function apply(ctx) {
678
- const runtime = ctx.locale
679
- const resolveScope = (c) => {
680
- try {
681
- if (c && c.configForms) return c.configForms.get(SETTINGS_NS_NAME)
445
+ function apply(ctx) {const runtime = ctx.locale
446
+ const resolveScope = (c) => {try {if (c && c.configForms) return c.configForms.get(SETTINGS_NS_NAME)
682
447
  if (c && c.settingsScope) return c.settingsScope.bind({ namespace: SETTINGS_NS_NAME })
683
448
  } catch (err) { void err }
684
449
  return null
685
450
  }
686
451
  const rawScope = resolveScope(ctx)
687
- const scope = (rawScope && typeof rawScope.getSnapshot === 'function') ? rawScope : {
688
- getSnapshot: () => ({ status: 'ready', value: {} }),
689
- subscribe: () => () => {},
690
- set: () => Promise.resolve()
691
- }
692
- for (const ns of Object.keys(RU)) {
693
- ctx.effect(() => {
694
- try { return ctx.locale.register(ns, 'ru', RU[ns]) }
452
+ const scope = (rawScope && typeof rawScope.getSnapshot === 'function') ? rawScope : {getSnapshot: () => ({ status: 'ready', value: {} }),subscribe: () => () => {},set: () => Promise.resolve()
453
+ }
454
+ for (const ns of Object.keys(RU)) {ctx.effect(() => {try { return ctx.locale.register(ns, 'ru', RU[ns]) }
695
455
  catch (err) { return () => {} }
696
456
  }, 'dsh-russian-lang: ' + ns)
697
457
  }
698
- const registerDictMap = (dictMap) => {
699
- if (!dictMap || typeof dictMap !== 'object') return
700
- for (const ns of Object.keys(dictMap)) {
701
- if (ns === 'zhRu' || ns === 'plugins' || ns === 'core') continue
458
+ const registerDictMap = (dictMap) => {if (!dictMap || typeof dictMap !== 'object') return
459
+ for (const ns of Object.keys(dictMap)) {if (ns === 'zhRu' || ns === 'plugins' || ns === 'core') continue
702
460
  const dict = dictMap[ns]
703
461
  if (typeof dict !== 'object' || !dict) continue
704
- if (!RU[ns]) {
705
- RU[ns] = dict
706
- ctx.effect(() => {
707
- try { return ctx.locale.register(ns, 'ru', dict) }
462
+ if (!RU[ns]) {RU[ns] = dict
463
+ ctx.effect(() => {try { return ctx.locale.register(ns, 'ru', dict) }
708
464
  catch (err) { return () => {} }
709
465
  }, 'dsh-russian-lang: ' + ns)
710
- } else {
711
- Object.assign(RU[ns], dict)
466
+ } else {Object.assign(RU[ns], dict)
712
467
  }
713
468
  }
714
469
  }
715
470
  const CACHE_NAME = 'dsh-ru-cache-v1'
716
- const loadLocalDict = (key) => {
717
- try {
718
- const raw = typeof localStorage !== 'undefined' ? localStorage.getItem('dsh_ru_' + key) : null
471
+ const loadLocalDict = (key) => {try {const raw = typeof localStorage !== 'undefined' ? localStorage.getItem('dsh_ru_' + key) : null
719
472
  return raw ? JSON.parse(raw) : null
720
473
  } catch (_) { return null }
721
474
  }
722
- const saveLocalDict = (key, etag, data) => {
723
- try {
724
- if (typeof localStorage !== 'undefined') {
725
- localStorage.setItem('dsh_ru_' + key, JSON.stringify({ etag, data }))
475
+ const saveLocalDict = (key, etag, data) => {try {if (typeof localStorage !== 'undefined') {localStorage.setItem('dsh_ru_' + key, JSON.stringify({ etag, data }))
726
476
  }
727
477
  } catch (_e) { void _e }
728
478
  }
729
- try {
730
- const cachedCore = loadLocalDict('core')
731
- if (cachedCore && cachedCore.data && cachedCore.data.core) {
732
- registerDictMap(cachedCore.data.core)
733
- if (cachedCore.data.zhRu && typeof ZH_RU === 'object') {
734
- Object.assign(ZH_RU, cachedCore.data.zhRu)
479
+ try {const cachedCore = loadLocalDict('core')
480
+ if (cachedCore && cachedCore.data && cachedCore.data.core) {registerDictMap(cachedCore.data.core)
481
+ if (cachedCore.data.zhRu && typeof ZH_RU === 'object') {Object.assign(ZH_RU, cachedCore.data.zhRu)
735
482
  }
736
483
  }
737
484
  } catch (_e) { void _e }
738
- const fetchCachedResource = async (url, cacheKey, onData) => {
739
- let etag = null
485
+ const fetchCachedResource = async (url, cacheKey, onData) => {let etag = null
740
486
  let delivered = false
741
- if (typeof caches !== 'undefined') {
742
- try {
743
- const cache = await caches.open(CACHE_NAME)
487
+ if (typeof caches !== 'undefined') {try {const cache = await caches.open(CACHE_NAME)
744
488
  const matched = await cache.match(url)
745
- if (matched) {
746
- etag = matched.headers.get('etag')
489
+ if (matched) {etag = matched.headers.get('etag')
747
490
  const parsed = await matched.json()
748
- if (parsed) {
749
- delivered = true
491
+ if (parsed) {delivered = true
750
492
  onData(parsed)
751
493
  }
752
494
  }
753
495
  } catch (_e) { void _e }
754
496
  }
755
- if (!delivered && cacheKey) {
756
- const local = loadLocalDict(cacheKey)
757
- if (local && local.data) {
758
- etag = local.etag
497
+ if (!delivered && cacheKey) {const local = loadLocalDict(cacheKey)
498
+ if (local && local.data) {etag = local.etag
759
499
  delivered = true
760
500
  onData(local.data)
761
501
  }
762
502
  }
763
503
  if (typeof fetch !== 'function') return
764
- try {
765
- const headers = { 'Accept': 'application/json' }
504
+ try {const headers = { 'Accept': 'application/json' }
766
505
  if (etag) headers['If-None-Match'] = etag
767
506
  const res = await fetch(url, { headers })
768
507
  if (res.status === 304) return
769
- if (res.ok) {
770
- const newEtag = res.headers.get('etag')
508
+ if (res.ok) {const newEtag = res.headers.get('etag')
771
509
  const cloned = res.clone()
772
510
  const data = await res.json()
773
- if (data) {
774
- onData(data)
775
- if (typeof caches !== 'undefined') {
776
- try {
777
- const cache = await caches.open(CACHE_NAME)
511
+ if (data) {onData(data)
512
+ if (typeof caches !== 'undefined') {try {const cache = await caches.open(CACHE_NAME)
778
513
  await cache.put(url, cloned)
779
514
  } catch (_e) { void _e }
780
515
  }
@@ -786,11 +521,9 @@ if (cacheKey) saveLocalDict(cacheKey, newEtag, data)
786
521
  const loadedPluginNames = new Set()
787
522
  const pendingPluginLoads = new Set()
788
523
  let pluginBatchTimer = null
789
- const loadPluginDictionaries = (names) => {
790
- if (typeof fetch !== 'function') return
524
+ const loadPluginDictionaries = (names) => {if (typeof fetch !== 'function') return
791
525
  const toLoad = []
792
- for (const raw of names) {
793
- const n = String(raw).trim()
526
+ for (const raw of names) {const n = String(raw).trim()
794
527
  if (!n || loadedPluginNames.has(n) || pendingPluginLoads.has(n)) continue
795
528
  toLoad.push(n)
796
529
  pendingPluginLoads.add(n)
@@ -798,42 +531,31 @@ pendingPluginLoads.add(n)
798
531
  const qs = toLoad.length > 0 ? ('?names=' + encodeURIComponent(toLoad.join(','))) : ''
799
532
  const url = '/api/dsh-russian-lang/dict/plugins' + qs
800
533
  const cacheKey = toLoad.length > 0 ? ('plugins_' + toLoad.sort().join('_')) : 'plugins_all'
801
- fetchCachedResource(url, cacheKey, (data) => {
802
- if (data && data.plugins) {
803
- registerDictMap(data.plugins)
534
+ fetchCachedResource(url, cacheKey, (data) => {if (data && data.plugins) {registerDictMap(data.plugins)
804
535
  for (const n of toLoad) loadedPluginNames.add(n)
805
536
  }
806
- }).catch(() => {}).finally(() => {
807
- for (const n of toLoad) pendingPluginLoads.delete(n)
537
+ }).catch(() => {}).finally(() => {for (const n of toLoad) pendingPluginLoads.delete(n)
808
538
  })
809
539
  }
810
- const schedulePluginLoad = (name) => {
811
- if (!name || loadedPluginNames.has(name) || pendingPluginLoads.has(name)) return
540
+ const schedulePluginLoad = (name) => {if (!name || loadedPluginNames.has(name) || pendingPluginLoads.has(name)) return
812
541
  pendingPluginLoads.add(name)
813
542
  if (pluginBatchTimer) clearTimeout(pluginBatchTimer)
814
- pluginBatchTimer = setTimeout(() => {
815
- const batch = Array.from(pendingPluginLoads).filter((n) => !loadedPluginNames.has(n))
543
+ pluginBatchTimer = setTimeout(() => {const batch = Array.from(pendingPluginLoads).filter((n) => !loadedPluginNames.has(n))
816
544
  if (batch.length > 0) loadPluginDictionaries(batch)
817
545
  }, 50)
818
546
  }
819
- fetchCachedResource('/api/dsh-russian-lang/dict/core', 'core', (data) => {
820
- if (!data) return
547
+ fetchCachedResource('/api/dsh-russian-lang/dict/core', 'core', (data) => {if (!data) return
821
548
  if (data.core) registerDictMap(data.core)
822
- if (data.zhRu && typeof ZH_RU === 'object') {
823
- Object.assign(ZH_RU, data.zhRu)
549
+ if (data.zhRu && typeof ZH_RU === 'object') {Object.assign(ZH_RU, data.zhRu)
824
550
  if (typeof updateZhRu === 'function') updateZhRu(data.zhRu)
825
551
  }
826
552
  if (typeof syncZhDom === 'function') syncZhDom()
827
553
  setTimeout(() => { loadPluginDictionaries([]) }, 100)
828
- }).catch(() => {
829
- if (typeof fetch === 'function') {
830
- fetch('/api/dsh-russian-lang/dict/all', { headers: { 'Accept': 'application/json' } })
554
+ }).catch(() => {if (typeof fetch === 'function') {fetch('/api/dsh-russian-lang/dict/all', { headers: { 'Accept': 'application/json' } })
831
555
  .then((res) => res.ok ? res.json() : null)
832
- .then((data) => {
833
- if (!data) return
556
+ .then((data) => {if (!data) return
834
557
  const allDicts = Object.assign({}, data.core || {}, data.plugins || {}, data)
835
- if (data.zhRu && typeof ZH_RU === 'object') {
836
- Object.assign(ZH_RU, data.zhRu)
558
+ if (data.zhRu && typeof ZH_RU === 'object') {Object.assign(ZH_RU, data.zhRu)
837
559
  if (typeof updateZhRu === 'function') updateZhRu(data.zhRu)
838
560
  }
839
561
  registerDictMap(allDicts)
@@ -843,15 +565,12 @@ if (typeof syncZhDom === 'function') syncZhDom()
843
565
  }
844
566
  })
845
567
  const origTranslate = runtime.translate.bind(runtime)
846
- const getOverrides = () => {
847
- try {
848
- const value = scope.getSnapshot().value
568
+ const getOverrides = () => {try {const value = scope.getSnapshot().value
849
569
  return value && value.overrides ? value.overrides : {}
850
570
  } catch (err) { return {} }
851
571
  }
852
572
  const getPluginLocalizationStatus = makePluginLocalizationStatus(RU)
853
- try {
854
- runtime.formatNumber = formatNumber
573
+ try {runtime.formatNumber = formatNumber
855
574
  runtime.formatCompactNumber = formatCompactNumber
856
575
  runtime.formatDate = formatDate
857
576
  runtime.formatTokens = formatTokens
@@ -865,96 +584,67 @@ runtime.humanizeError = humanizeError
865
584
  runtime.plural = plural
866
585
  runtime.pluralForm = pluralForm
867
586
  } catch (err) { }
868
- const lookupChain = () => {
869
- try {
870
- const chain = runtime.fallbackChain && runtime.fallbackChain(runtime.getLocale().active)
587
+ const lookupChain = () => {try {const chain = runtime.fallbackChain && runtime.fallbackChain(runtime.getLocale().active)
871
588
  if (Array.isArray(chain) && chain.length) return chain
872
589
  } catch (err) { }
873
590
  return [runtime.getLocale().active]
874
591
  }
875
592
  const translationRegistry = new Map()
876
- const lookup = (ns, key) => {
877
- const chain = lookupChain()
593
+ const lookup = (ns, key) => {const chain = lookupChain()
878
594
  return runtime.lookup.length >= 3 ? runtime.lookup(ns, key, chain) : runtime.lookup(ns, key)
879
595
  }
880
- runtime.translate = function (ns, key, params) {
881
- if (ns && !RU[ns] && !loadedPluginNames.has(ns)) schedulePluginLoad(ns)
596
+ runtime.translate = function (ns, key, params) {if (ns && !RU[ns] && !loadedPluginNames.has(ns)) schedulePluginLoad(ns)
882
597
  const overrides = getOverrides()
883
- if (overrides[key] !== undefined) {
884
- return params ? fill(overrides[key], params) : overrides[key]
598
+ if (overrides[key] !== undefined) {return params ? fill(overrides[key], params) : overrides[key]
885
599
  }
886
- if (runtime.getLocale().active === 'ru' && params) {
887
- const n = params.n ?? params.count
888
- if (typeof n === 'number') {
889
- const form = pluralForm(n)
600
+ if (runtime.getLocale().active === 'ru' && params) {const n = params.n ?? params.count
601
+ if (typeof n === 'number') {const form = pluralForm(n)
890
602
  const m = /^(.*)[.](one|other)$/.exec(key)
891
- if (m) {
892
- if (form === 'few' || form === 'many') {
893
- const pluralKey = m[1] + '.' + form
603
+ if (m) {if (form === 'few' || form === 'many') {const pluralKey = m[1] + '.' + form
894
604
  const template = lookup(ns, pluralKey) ?? lookup('common', pluralKey)
895
- if (template !== undefined) {
896
- return fill(template, params)
605
+ if (template !== undefined) {return fill(template, params)
897
606
  }
898
607
  }
899
- } else if (form !== 'other' && !/[.](one|other|few|many)$/.test(key)) {
900
- const pluralKey = key + '.' + form
608
+ } else if (form !== 'other' && !/[.](one|other|few|many)$/.test(key)) {const pluralKey = key + '.' + form
901
609
  const template = lookup(ns, pluralKey) ?? lookup('common', pluralKey)
902
- if (template !== undefined) {
903
- return fill(template, params)
610
+ if (template !== undefined) {return fill(template, params)
904
611
  }
905
612
  }
906
613
  }
907
614
  }
908
615
  const res = origTranslate(ns, key, params)
909
- if (typeof res === 'string' && res && res.length < 300) {
910
- translationRegistry.set(res.trim(), { ns, key, params, value: res })
616
+ if (typeof res === 'string' && res && res.length < 300) {translationRegistry.set(res.trim(), { ns, key, params, value: res })
911
617
  }
912
618
  return res
913
619
  }
914
- const syncLang = () => {
915
- try {
916
- if (typeof document !== 'undefined' && document.documentElement
917
- && runtime.getLocale().active === 'ru') {
918
- document.documentElement.lang = 'ru-RU'
620
+ const syncLang = () => {try {if (typeof document !== 'undefined' && document.documentElement
621
+ && runtime.getLocale().active === 'ru') {document.documentElement.lang = 'ru-RU'
919
622
  }
920
623
  } catch (err) { }
921
624
  }
922
625
  const native = runtime.getLocale().locales.some((l) => l.id === 'ru')
923
- if (!native) {
924
- runtime.addLanguage({ id: 'ru', label: 'Русский', fallback: 'en' })
626
+ if (!native) {runtime.addLanguage({ id: 'ru', label: 'Русский', fallback: 'en' })
925
627
  syncLang()
926
628
  }
927
- const syncFlag = () => {
928
- try {
929
- const wantRu = runtime.getLocale().active === 'ru'
629
+ const syncFlag = () => {try {const wantRu = runtime.getLocale().active === 'ru'
930
630
  const value = scope.getSnapshot().value || {}
931
- if (!!value.enabled !== wantRu) {
932
- const r = scope.set('enabled', wantRu)
933
- if (r && typeof r.catch === 'function') {
934
- r.catch((err) => {
935
- console.warn('dsh-russian-lang: scope.set enabled failed', err && err.message || err)
631
+ if (!!value.enabled !== wantRu) {const r = scope.set('enabled', wantRu)
632
+ if (r && typeof r.catch === 'function') {r.catch((err) => {console.warn('dsh-russian-lang: scope.set enabled failed', err && err.message || err)
936
633
  })
937
634
  }
938
635
  }
939
- } catch (err) {
940
-
941
- }
636
+ } catch (err) {}
942
637
  }
943
- ctx.effect(() => {
944
- try { return runtime.subscribe(syncFlag) }
638
+ ctx.effect(() => {try { return runtime.subscribe(syncFlag) }
945
639
  catch (err) { return undefined }
946
640
  }, 'dsh-russian-lang: sync-flag')
947
- const activate = () => {
948
- try {
949
- if (runtime.getLocale().active === 'ru') return
641
+ const activate = () => {try {if (runtime.getLocale().active === 'ru') return
950
642
  runtime.setLocale('ru')
951
643
  } catch (err) { console.warn('dsh-russian-lang: activate failed', err) }
952
644
  }
953
645
  let booted = false
954
- const tryBoot = () => {
955
- if (booted) return
956
- try {
957
- const value = scope.getSnapshot().value
646
+ const tryBoot = () => {if (booted) return
647
+ try {const value = scope.getSnapshot().value
958
648
  if (value && value.enabled === true) { booted = true; activate() }
959
649
  } catch (err) { }
960
650
  }
@@ -968,20 +658,17 @@ const SPELL_WAS = 'data-russian-lang-spell-was'
968
658
  const LANG_WAS = 'data-russian-lang-lang-was'
969
659
  const EDITABLE = 'textarea, input[type=text], input[type=search], [contenteditable=""], [contenteditable="true"]'
970
660
  const MONO_RE = /mono|consol|courier/i
971
- const isMonoField = (el) => {
972
- try { return MONO_RE.test(getComputedStyle(el).fontFamily || '') }
661
+ const isMonoField = (el) => {try { return MONO_RE.test(getComputedStyle(el).fontFamily || '') }
973
662
  catch (err) { return false }
974
663
  }
975
- const spellOn = (el) => {
976
- if (el.hasAttribute(SPELL_ON) || isMonoField(el)) return
664
+ const spellOn = (el) => {if (el.hasAttribute(SPELL_ON) || isMonoField(el)) return
977
665
  el.setAttribute(SPELL_ON, '1')
978
666
  el.setAttribute(SPELL_WAS, el.getAttribute('spellcheck') ?? '')
979
667
  el.setAttribute(LANG_WAS, el.getAttribute('lang') ?? '')
980
668
  el.setAttribute('spellcheck', 'true')
981
669
  el.setAttribute('lang', 'ru-RU')
982
670
  }
983
- const spellOff = (el) => {
984
- if (!el.hasAttribute(SPELL_ON)) return
671
+ const spellOff = (el) => {if (!el.hasAttribute(SPELL_ON)) return
985
672
  const was = el.getAttribute(SPELL_WAS)
986
673
  if (was === '') el.removeAttribute('spellcheck')
987
674
  else el.setAttribute('spellcheck', was)
@@ -993,21 +680,15 @@ el.removeAttribute(SPELL_WAS)
993
680
  el.removeAttribute(LANG_WAS)
994
681
  }
995
682
  let spellObserver = null
996
- const syncSpell = () => {
997
- try {
998
- if (typeof document === 'undefined') return
683
+ const syncSpell = () => {try {if (typeof document === 'undefined') return
999
684
  const ru = runtime.getLocale().active === 'ru'
1000
- if (!ru) {
1001
- if (spellObserver) { spellObserver.disconnect(); spellObserver = null }
685
+ if (!ru) {if (spellObserver) { spellObserver.disconnect(); spellObserver = null }
1002
686
  document.querySelectorAll('[' + SPELL_ON + ']').forEach(spellOff)
1003
687
  return
1004
688
  }
1005
689
  document.querySelectorAll(EDITABLE).forEach(spellOn)
1006
690
  if (spellObserver) return
1007
- spellObserver = new MutationObserver((records) => {
1008
- for (const record of records) {
1009
- for (const node of record.addedNodes) {
1010
- if (node.nodeType !== 1) continue
691
+ spellObserver = new MutationObserver((records) => {for (const record of records) {for (const node of record.addedNodes) {if (node.nodeType !== 1) continue
1011
692
  if (node.matches(EDITABLE)) spellOn(node)
1012
693
  node.querySelectorAll ? node.querySelectorAll(EDITABLE).forEach(spellOn) : null
1013
694
  }
@@ -1017,9 +698,7 @@ spellObserver.observe(document.body, { childList: true, subtree: true })
1017
698
  } catch (err) { }
1018
699
  }
1019
700
  const unsubscribeSpell = runtime.subscribe(syncSpell)
1020
- ctx.effect(() => {
1021
- return () => {
1022
- unsubscribeSpell()
701
+ ctx.effect(() => {return () => {unsubscribeSpell()
1023
702
  if (spellObserver) spellObserver.disconnect()
1024
703
  try { document.querySelectorAll('[' + SPELL_ON + ']').forEach(spellOff) } catch (err) { }
1025
704
  }
@@ -1029,326 +708,85 @@ const ZH_CJK = /[\u3400-\u9fff\uf900-\ufaff]/
1029
708
  const ZH_RE_ESC = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
1030
709
  const ZH_EXACT = new Map()
1031
710
  const ZH_PATTERNS = []
1032
- const CORE_ZH_PRESETS = {
1033
- '请求批准': 'Запрашивать подтверждение',
1034
- '完全放开': 'Полный доступ',
1035
- '只读': 'Только чтение',
1036
- '只能读,任何写入都需要审批。': 'Только чтение, любая запись требует подтверждения.',
1037
- '工作区内可写;工作区外的操作请求人工审批。': 'Запись в рабочей области разрешена; операции вне рабочей области требуют подтверждения.',
1038
- '全放行,不弹审批。': 'Полный доступ, запросы на подтверждение не выводятся.',
1039
- '确定性规则 + 两阶段分类器自动决定;危险或故障时 fail-closed。': 'Детерминированные правила + двухэтапный классификатор; при рисках — безопасная блокировка.',
1040
- '手动测试': 'Ручное тестирование',
1041
- '事件': 'Событие',
1042
- '模拟 (看匹配)': 'Симуляция (проверка)',
1043
- '执行 (真实触发)': 'Выполнение (реальный триггер)',
1044
- '通知渠道测试': 'Тест каналов уведомлений',
1045
- '渠道': 'Канал',
1046
- 'Slack 风格单行摘要': 'Сводка в стиле Slack',
1047
- '发送测试通知': 'Отправить тестовое уведомление',
1048
- '飞书通知': 'Уведомления Feishu',
1049
- '扫码连接飞书': 'Подключить Feishu по QR-коду',
1050
- '将创建名为 [DSH 通知机器人] 的飞书应用': 'Будет создано приложение Feishu [DSH 通知机器人]',
1051
- '卡片截断长度': 'Длина обрезки карточки',
1052
- '复制 YAML': 'Копировать YAML',
1053
- '去抖': 'Дебаунс',
1054
- '已断开': 'Отключено',
1055
- '已连接': 'Подключено',
1056
- '已保存': 'Сохранено',
1057
- '保存失败': 'Не удалось сохранить',
1058
- '编辑': 'Редактировать',
1059
- '取消编辑': 'Отмена',
1060
- '启用': 'Включить',
1061
- '停用': 'Отключить',
1062
- '重试': 'Повторить',
1063
- '刷新': 'Обновить',
1064
- '网络请求失败': 'Сетевой запрос не удался',
1065
- '如未立即生效请重启': 'Если изменения не применились, перезапустите DSH',
1066
- '通知机器人': 'Бот уведомлений',
1067
- '飞书扫码授权二维码': 'QR-код авторизации Feishu',
1068
- '在浏览器中打开飞书授权链接': 'Открыть ссылку авторизации Feishu в браузере',
1069
- '重新扫码会覆盖现有应用凭据与本': 'Повторное сканирование перезапишет учётные данные',
1070
- '扫码者本人接收通知卡片': 'Получатель карточки уведомлений — авторизованный пользователь',
1071
- 'tool (可选)': 'Инструмент (опц.)',
1072
- 'runningSubagents (可选)': 'Подагенты (опц.)',
1073
- 'durationMs (可选)': 'Длительность мс (опц.)',
1074
- 'usage 输入 (可选)': 'Входные токены (опц.)',
1075
- 'usage 输出 (可选)': 'Выходные токены (опц.)',
1076
- 'profile (写入哪个 profile 的 cordis.patch.yml)': 'Профиль (куда записать cordis.patch.yml)',
1077
- 'URL (留空用 DSH_HOOKS_WEBHOOK_URL)': 'URL (по умолчанию DSH_HOOKS_WEBHOOK_URL)',
1078
- '当前已配置的 Hook 规则列表': 'Список текущих настроенных правил хуков',
1079
- '测试通道': 'Тест канала'
711
+ const CORE_ZH_PRESETS = {'请求批准': 'Запрашивать подтверждение','完全放开': 'Полный доступ','只读': 'Только чтение','只能读,任何写入都需要审批。': 'Только чтение, любая запись требует подтверждения.','工作区内可写;工作区外的操作请求人工审批。': 'Запись в рабочей области разрешена; операции вне рабочей области требуют подтверждения.','全放行,不弹审批。': 'Полный доступ, запросы на подтверждение не выводятся.','确定性规则 + 两阶段分类器自动决定;危险或故障时 fail-closed。': 'Детерминированные правила + двухэтапный классификатор; при рисках — безопасная блокировка.','手动测试': 'Ручное тестирование','事件': 'Событие','模拟 (看匹配)': 'Симуляция (проверка)','执行 (真实触发)': 'Выполнение (реальный триггер)','通知渠道测试': 'Тест каналов уведомлений','渠道': 'Канал','Slack 风格单行摘要': 'Сводка в стиле Slack','发送测试通知': 'Отправить тестовое уведомление','飞书通知': 'Уведомления Feishu','扫码连接飞书': 'Подключить Feishu по QR-коду','将创建名为 [DSH 通知机器人] 的飞书应用': 'Будет создано приложение Feishu [DSH 通知机器人]','卡片截断长度': 'Длина обрезки карточки','复制 YAML': 'Копировать YAML','去抖': 'Дебаунс','已断开': 'Отключено','已连接': 'Подключено','已保存': 'Сохранено','保存失败': 'Не удалось сохранить','编辑': 'Редактировать','取消编辑': 'Отмена','启用': 'Включить','停用': 'Отключить','重试': 'Повторить','刷新': 'Обновить','网络请求失败': 'Сетевой запрос не удался','如未立即生效请重启': 'Если изменения не применились, перезапустите DSH','通知机器人': 'Бот уведомлений','飞书扫码授权二维码': 'QR-код авторизации Feishu','在浏览器中打开飞书授权链接': 'Открыть ссылку авторизации Feishu в браузере','重新扫码会覆盖现有应用凭据与本': 'Повторное сканирование перезапишет учётные данные','扫码者本人接收通知卡片': 'Получатель карточки уведомлений — авторизованный пользователь','tool (可选)': 'Инструмент (опц.)','runningSubagents (可选)': 'Подагенты (опц.)','durationMs (可选)': 'Длительность мс (опц.)','usage 输入 (可选)': 'Входные токены (опц.)','usage 输出 (可选)': 'Выходные токены (опц.)','profile (写入哪个 profile 的 cordis.patch.yml)': 'Профиль (куда записать cordis.patch.yml)','URL (留空用 DSH_HOOKS_WEBHOOK_URL)': 'URL (по умолчанию DSH_HOOKS_WEBHOOK_URL)','当前已配置的 Hook 规则列表': 'Список текущих настроенных правил хуков','测试通道': 'Тест канала'
1080
712
  }
1081
713
  for (const [k, v] of Object.entries(CORE_ZH_PRESETS)) ZH_EXACT.set(k, v)
1082
714
  const zhSortedExact = []
1083
- const rebuildZhSorted = () => {
1084
- zhSortedExact.length = 0
715
+ const rebuildZhSorted = () => {zhSortedExact.length = 0
1085
716
  for (const [k, v] of ZH_EXACT.entries()) zhSortedExact.push([k, v])
1086
717
  zhSortedExact.sort((a, b) => b[0].length - a[0].length)
1087
718
  }
1088
719
  rebuildZhSorted()
1089
- const updateZhRu = (entries) => {
1090
- if (!entries || typeof entries !== 'object') return
1091
- for (const [zhText, ruText] of Object.entries(entries)) {
1092
- if (typeof zhText !== 'string' || !ZH_CJK.test(zhText) || !ruText) continue
1093
- if (/\{[a-zA-Z_]\w*\}/.test(zhText)) {
1094
- const parts = zhText.split(/\{[a-zA-Z_]\w*\}/g)
720
+ const updateZhRu = (entries) => {if (!entries || typeof entries !== 'object') return
721
+ for (const [zhText, ruText] of Object.entries(entries)) {if (typeof zhText !== 'string' || !ZH_CJK.test(zhText) || !ruText) continue
722
+ if (/\{[a-zA-Z_]\w*\}/.test(zhText)) {const parts = zhText.split(/\{[a-zA-Z_]\w*\}/g)
1095
723
  if (parts.some((p) => p.length === 0)) continue
1096
724
  ZH_PATTERNS.push({ re: new RegExp(parts.map(ZH_RE_ESC).join('([\\s\\S]*?)')), ruParts: ruText.split(/\{[a-zA-Z_]\w*\}/g) })
1097
- } else {
1098
- ZH_EXACT.set(zhText, ruText)
725
+ } else {ZH_EXACT.set(zhText, ruText)
1099
726
  }
1100
727
  }
1101
728
  ZH_PATTERNS.sort((a, b) => b.re.source.length - a.re.source.length)
1102
729
  rebuildZhSorted()
1103
730
  }
1104
731
  updateZhRu(ZH_RU)
1105
- const zhTranslateText = (text) => {
1106
- const overrides = getOverrides()
732
+ const zhTranslateText = (text) => {const overrides = getOverrides()
1107
733
  if (overrides[text] !== undefined) return overrides[text]
1108
734
  const trimmed = text.trim()
1109
735
  if (overrides[trimmed] !== undefined) return text.replace(trimmed, overrides[trimmed])
1110
736
  if (!ZH_CJK.test(text)) return null
1111
737
  const exact = ZH_EXACT.get(text)
1112
738
  if (exact !== undefined) return exact
1113
- if (trimmed !== text) {
1114
- const exactTrimmed = ZH_EXACT.get(trimmed)
739
+ if (trimmed !== text) {const exactTrimmed = ZH_EXACT.get(trimmed)
1115
740
  if (exactTrimmed !== undefined) return text.replace(trimmed, exactTrimmed)
1116
741
  }
1117
- for (const p of ZH_PATTERNS) {
1118
- const m = p.re.exec(trimmed)
1119
- if (m && m[0] === trimmed) {
1120
- let out = p.ruParts[0]
742
+ for (const p of ZH_PATTERNS) {const m = p.re.exec(trimmed)
743
+ if (m && m[0] === trimmed) {let out = p.ruParts[0]
1121
744
  for (let i = 1; i < p.ruParts.length; i++) out += m[i] + p.ruParts[i]
1122
745
  return text.replace(trimmed, out)
1123
746
  }
1124
747
  }
1125
748
  let replaced = text
1126
749
  let changed = false
1127
- for (const [zhPhrase, ruPhrase] of zhSortedExact) {
1128
- if (zhPhrase.length >= 2 && replaced.includes(zhPhrase)) {
1129
- replaced = replaced.split(zhPhrase).join(ruPhrase)
750
+ for (const [zhPhrase, ruPhrase] of zhSortedExact) {if (zhPhrase.length >= 2 && replaced.includes(zhPhrase)) {replaced = replaced.split(zhPhrase).join(ruPhrase)
1130
751
  changed = true
1131
752
  }
1132
753
  }
1133
754
  if (changed) return replaced
1134
755
  return null
1135
756
  }
1136
- const DOM_EN_ATTRS = {
1137
- 'Streaming preview': 'Предпросмотр стриминга',
1138
- 'Visualization streaming preview': 'Предпросмотр визуализации',
1139
- 'Security Auditor Shield': 'Защитный щит аудитора',
1140
- 'Shield: Safe': 'Щит: Безопасно',
1141
- 'Shield: Alert': 'Щит: Тревога',
1142
- 'e.g. Implement report export and cover with unit tests': 'Например: Реализовать экспорт отчёта и покрыть юнит-тестами',
1143
- 'e.g. Implement report export and cover with unit tests...': 'Например: Реализовать экспорт отчёта и покрыть юнит-тестами…',
1144
- 'e.g. Implement user profile settings card with theme tokens': 'Например: Реализовать карточку настроек профиля с токенами темы',
1145
- 'Detailed functional specs, acceptance criteria, constraints...': 'Детальная функциональная спецификация, критерии приёмки, ограничения…',
1146
- 'Specific instructions, questions, or requirements for this specialist...': 'Конкретные инструкции, вопросы или требования для данного специалиста…',
1147
- }
1148
- const DOM_EN_TEXT = {
1149
- 'Scheduled tasks': 'Задачи по расписанию',
1150
- 'Today': 'Сегодня',
1151
- 'Plugin Market': 'Магазин плагинов',
1152
- 'GitHub ops': 'Операции с GitHub',
1153
- 'Auto mode': 'Автоматический режим',
1154
- 'Full access': 'Полный доступ',
1155
- 'Read only': 'Только чтение',
1156
- 'Side card': 'Боковая панель',
1157
- 'panelName': 'Палитра команд',
1158
- 'Hooks': 'Хуки',
1159
- 'Manage what the side card shows and how it behaves': 'Настройка содержимого и поведения боковой панели',
1160
- 'Inject the sidebar-open tool for the model': 'Предоставить модели инструмент sidebar-open',
1161
- 'Position compatibility mode': 'Режим совместимости расположения',
1162
- 'Auto-detect': 'Автоопределение',
1163
- 'Sidebar content': 'Содержимое боковой панели',
1164
- 'Changes': 'Изменения',
1165
- 'Tasks': 'Задачи',
1166
- 'Time Machine': 'Машина времени',
1167
- 'Live Canvas': 'Живой холст',
1168
- 'Side Chat (beta)': 'Боковой чат (бета)',
1169
- 'Terminal': 'Терминал',
1170
- 'Feature settings': 'Настройки функции',
1171
- 'Low': 'Низкий',
1172
- 'Medium': 'Средний',
1173
- 'High': 'Высокий',
1174
- 'Effort': 'Рассуждения',
1175
- 'Search engine (ModSearch)': 'Поисковая система (ModSearch)',
1176
- 'Search engine provider configuration.': 'Настройка провайдера поисковой системы.',
1177
- 'X search only': 'Только поиск в X',
1178
- 'Security Auditor Shield': 'Защитный щит аудитора',
1179
- 'Shield: Safe': 'Щит: Безопасно',
1180
- 'Shield: Alert': 'Щит: Тревога',
1181
- 'Lens': 'Линза',
1182
- 'Gallery': 'Галерея',
1183
- 'SSH: Local': 'SSH: Локально',
1184
- 'SSH: Remote': 'SSH: Удалённо',
1185
- '7/7 rot': '7/7 рот.',
1186
- 'Smoke chat': 'Тестовый чат',
1187
- 'Quick Launch Goal': 'Быстрый запуск цели',
1188
- 'Define the objective for the agent in autonomous mode:': 'Сформулируйте задачу для автономной работы агента:',
1189
- 'Fix Bug': 'Исправление бага',
1190
- 'Refactor (YAGNI)': 'Рефакторинг (YAGNI)',
1191
- 'Tests & Coverage': 'Тесты и покрытие',
1192
- 'Code Review': 'Ревью кода',
1193
- 'New Feature': 'Новая функция',
1194
- 'Security Audit': 'Аудит безопасности',
1195
- 'Docs & Contract': 'Документация и контракт',
1196
- 'Upgrade Deps': 'Обновление зависимостей',
1197
- 'Dead Code': 'Мёртвый код',
1198
- 'Performance': 'Производительность',
1199
- 'Start Goal': 'Запустить цель',
1200
- 'Launch Multi-Agent Orchestrator': 'Запуск мультиагентного оркестратора',
1201
- 'Full DAG Pipeline': 'Полный DAG-пайплайн',
1202
- 'Direct Specialist Subagent': 'Прямой субагент-специалист',
1203
- 'Objective Title': 'Название задачи',
1204
- 'Scope & Requirements': 'Объём и требования',
1205
- 'Topology Scenario': 'Топологический сценарий',
1206
- 'Auto (Infer based on prompt complexity)': 'Авто (определить по сложности задачи)',
1207
- 'Hotfix (1 Stage: Triage & Minimal Fix)': 'Хотфикс (1 этап: анализ и точечный фикс)',
1208
- 'Simple (2 Stages: Spec + Exec)': 'Простой (2 этапа: ТЗ + реализация)',
1209
- 'Medium (4 Stages: Spec -> Design -> Code -> QA)': 'Средний (4 этапа: ТЗ -> Дизайн -> Код -> Тестирование)',
1210
- 'Complex (6 Stages: Full Engineering Lifecycle)': 'Сложный (6 этапов: полный инженерный цикл)',
1211
- 'Enterprise (7 Stages: R&D Spike -> Fullstack -> Gate)': 'Enterprise (7 этапов: R&D исследование -> Фулстек -> Гейт приёмки)',
1212
- 'Target Specialist Role': 'Роль целевого специалиста',
1213
- 'UI/UX Interface Designer': 'UI/UX дизайнер интерфейсов',
1214
- 'System Architect (DESIGN.md / ADR)': 'Системный архитектор (DESIGN.md / ADR)',
1215
- 'Technical Spec Analyst': 'Аналитик технических спецификаций',
1216
- 'Senior Frontend Developer': 'Ведущий frontend-разработчик',
1217
- 'Senior Backend Developer': 'Ведущий backend-разработчик',
1218
- 'QA Automation Engineer': 'Инженер автоматизации тестирования',
1219
- 'Refactoring & Complexity Specialist': 'Специалист по рефакторингу и сложности',
1220
- 'Hotfix & Diagnostic Engineer': 'Инженер хотфиксов и диагностики',
1221
- 'Documentation Specialist': 'Технический писатель / Документация',
1222
- 'Spike & R&D Researcher': 'Исследователь R&D и прототипирования',
1223
- 'DevOps & Tooling Specialist': 'DevOps и инфраструктурный специалист',
1224
- 'Instructions for Subagent': 'Инструкции для субагента',
1225
- 'Start Pipeline': 'Запустить пайплайн',
1226
- 'Delegate Subagent': 'Делегировать субагенту',
1227
- 'Dispatching...': 'Отправка…',
1228
- 'Discounted rate active': 'Действует сниженный тариф',
1229
- 'off-peak': 'непиковый',
1230
- 'peak': 'пиковый',
1231
- 'Context cache saved:': 'Сэкономлено на кэше:',
1232
- '1M tokens, $': '1 млн токенов, $',
1233
- 'Input (cache hit)': 'Ввод (попадание в кэш)',
1234
- 'Input (cache miss)': 'Ввод (промах кэша)',
1235
- 'Output': 'Вывод',
1236
- 'SESSION TOKENS': 'ТОКЕНЫ СЕССИИ',
1237
- 'SPEND BY MODEL': 'РАСХОД ПО МОДЕЛЯМ',
1238
- 'Session total:': 'Всего за сессию:',
1239
- 'Copy Summary': 'Скопировать сводку',
1240
- 'Context Lens': 'Линза контекста',
1241
- 'Saved tokens': 'Сэкономлено токенов',
1242
- '% Saved': '% экономии',
1243
- 'ops': 'операций',
1244
- 'Budget': 'Бюджет',
1245
- 'Active focus': 'Активный фокус',
1246
- 'No focus paths set': 'Пути фокусировки не заданы',
1247
- 'Status': 'Статус',
1248
- 'Graph & CI': 'Граф и CI',
1249
- 'Events & PRs': 'События и PR',
1250
- 'Branch': 'Ветка',
1251
- 'Sync': 'Синхронизация',
1252
- 'Up to date with remote (@{upstream})': 'Синхронизировано с удалённым репозиторием (@{upstream})',
1253
- 'Up to date with remote': 'Синхронизировано с удалённым репозиторием',
1254
- 'Clean': 'Чисто',
1255
- 'No modified or untracked files in the working directory.': 'В рабочем каталоге нет изменённых или неотслеживаемых файлов.',
1256
- 'RECENT COMMITS': 'ПОСЛЕДНИЕ КОММИТЫ',
1257
- 'Active jobs': 'Активные задачи',
1258
- 'No active jobs': 'Нет активных задач',
1259
- 'Register in Models': 'Зарегистрировать в моделях',
1260
- 'Base URL': 'Базовый URL',
1261
- 'API key env / credential name': 'Имя переменной окружения / ключа API',
1262
- 'Default model': 'Модель по умолчанию',
1263
- 'Enabled': 'Включено',
1264
- 'Save': 'Сохранить',
1265
- 'Saved': 'Сохранено',
1266
- 'Refresh': 'Обновить',
1267
- 'Loading settings…': 'Загрузка настроек…',
1268
- 'Preferred engine': 'Предпочитаемый движок',
1269
- 'API key': 'API-ключ',
1270
- 'stored, leave empty to keep it': 'сохранён, оставьте пустым для сохранения',
1271
- 'Built-in official endpoint, leave blank to use it': 'Встроенная официальная конечная точка, оставьте пустым',
1272
- 'Discard': 'Сбросить',
1273
- 'Model synchronization': 'Синхронизация моделей',
1274
- 'All API-key providers': 'Все провайдеры API-ключей',
1275
- 'Preview only (dry-run)': 'Только предпросмотр (dry-run)',
1276
- 'Confirm stale removal': 'Подтверждать удаление устаревших',
1277
- 'Refresh status': 'Обновить статус',
1278
- 'Refresh all': 'Обновить всё',
1279
- 'Discover': 'Обнаружить',
1280
- 'Check availability': 'Проверить доступность',
1281
- 'Check credentials': 'Проверить учётные данные',
1282
- 'Choose models': 'Выбрать модели',
1283
- 'Manual model selection': 'Выбор моделей вручную',
1284
- 'Mode': 'Режим',
1285
- 'Hybrid (auto-rewrite + tools)': 'Гибридный (авто-переписывание + инструменты)',
1286
- 'Describe strategy': 'Стратегия описания',
1287
- 'Auto (use vision LLM)': 'Авто (использовать Vision LLM)',
1288
- 'Escalation': 'Эскалация',
1289
- 'Simple only (one pass)': 'Только простая (один проход)',
1290
- 'Routing': 'Маршрутизация',
1291
- 'Channel order': 'Порядок каналов',
1292
- 'Issue Reporter': 'Репортёр проблем',
1293
- 'GitHub sign-in is not configured for this installation.': 'Вход через GitHub не настроен для этой установки.',
1294
- '0 plugins': '0 плагинов',
1295
- 'Catalog': 'Каталог',
1296
- 'Report Editor': 'Редактор отчёта',
1297
- 'My Reports': 'Мои отчёты',
1298
- 'Authorization': 'Авторизация',
1299
- 'Search installed plugins…': 'Поиск установленных плагинов…',
1300
- 'Search installed plugins...': 'Поиск установленных плагинов...',
1301
- 'Refresh inventory': 'Обновить список',
1302
- 'Loading inventory & status…': 'Загрузка списка и статуса…',
1303
- 'Loading inventory & status...': 'Загрузка списка и статуса...'
1304
- }
1305
- const ZH_WALKER = (root) => {
1306
- try {
1307
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
757
+ const DOM_EN_ATTRS = {'Streaming preview': 'Предпросмотр стриминга','Visualization streaming preview': 'Предпросмотр визуализации','Security Auditor Shield': 'Защитный щит аудитора','Shield: Safe': 'Щит: Безопасно','Shield: Alert': 'Щит: Тревога','e.g. Implement report export and cover with unit tests': 'Например: Реализовать экспорт отчёта и покрыть юнит-тестами','e.g. Implement report export and cover with unit tests...': 'Например: Реализовать экспорт отчёта и покрыть юнит-тестами…','e.g. Implement user profile settings card with theme tokens': 'Например: Реализовать карточку настроек профиля с токенами темы','Detailed functional specs, acceptance criteria, constraints...': 'Детальная функциональная спецификация, критерии приёмки, ограничения…','Specific instructions, questions, or requirements for this specialist...': 'Конкретные инструкции, вопросы или требования для данного специалиста…',}
758
+ const DOM_EN_TEXT = {'Scheduled tasks': 'Задачи по расписанию','Today': 'Сегодня','Plugin Market': 'Магазин плагинов','GitHub ops': 'Операции с GitHub','Auto mode': 'Автоматический режим','Full access': 'Полный доступ','Read only': 'Только чтение','Side card': 'Боковая панель','panelName': 'Палитра команд','Hooks': 'Хуки','Manage what the side card shows and how it behaves': 'Настройка содержимого и поведения боковой панели','Inject the sidebar-open tool for the model': 'Предоставить модели инструмент sidebar-open','Position compatibility mode': 'Режим совместимости расположения','Auto-detect': 'Автоопределение','Sidebar content': 'Содержимое боковой панели','Changes': 'Изменения','Tasks': 'Задачи','Time Machine': 'Машина времени','Live Canvas': 'Живой холст','Side Chat (beta)': 'Боковой чат (бета)','Terminal': 'Терминал','Feature settings': 'Настройки функции','Low': 'Низкий','Medium': 'Средний','High': 'Высокий','Effort': 'Рассуждения','Search engine (ModSearch)': 'Поисковая система (ModSearch)','Search engine provider configuration.': 'Настройка провайдера поисковой системы.','X search only': 'Только поиск в X','Security Auditor Shield': 'Защитный щит аудитора','Shield: Safe': 'Щит: Безопасно','Shield: Alert': 'Щит: Тревога','Lens': 'Линза','Gallery': 'Галерея','SSH: Local': 'SSH: Локально','SSH: Remote': 'SSH: Удалённо','7/7 rot': '7/7 рот.','Smoke chat': 'Тестовый чат','Quick Launch Goal': 'Быстрый запуск цели','Define the objective for the agent in autonomous mode:': 'Сформулируйте задачу для автономной работы агента:','Fix Bug': 'Исправление бага','Refactor (YAGNI)': 'Рефакторинг (YAGNI)','Tests & Coverage': 'Тесты и покрытие','Code Review': 'Ревью кода','New Feature': 'Новая функция','Security Audit': 'Аудит безопасности','Docs & Contract': 'Документация и контракт','Upgrade Deps': 'Обновление зависимостей','Dead Code': 'Мёртвый код','Performance': 'Производительность','Start Goal': 'Запустить цель','Launch Multi-Agent Orchestrator': 'Запуск мультиагентного оркестратора','Full DAG Pipeline': 'Полный DAG-пайплайн','Direct Specialist Subagent': 'Прямой субагент-специалист','Objective Title': 'Название задачи','Scope & Requirements': 'Объём и требования','Topology Scenario': 'Топологический сценарий','Auto (Infer based on prompt complexity)': 'Авто (определить по сложности задачи)','Hotfix (1 Stage: Triage & Minimal Fix)': 'Хотфикс (1 этап: анализ и точечный фикс)','Simple (2 Stages: Spec + Exec)': 'Простой (2 этапа: ТЗ + реализация)','Medium (4 Stages: Spec -> Design -> Code -> QA)': 'Средний (4 этапа: ТЗ -> Дизайн -> Код -> Тестирование)','Complex (6 Stages: Full Engineering Lifecycle)': 'Сложный (6 этапов: полный инженерный цикл)','Enterprise (7 Stages: R&D Spike -> Fullstack -> Gate)': 'Enterprise (7 этапов: R&D исследование -> Фулстек -> Гейт приёмки)','Target Specialist Role': 'Роль целевого специалиста','UI/UX Interface Designer': 'UI/UX дизайнер интерфейсов','System Architect (DESIGN.md / ADR)': 'Системный архитектор (DESIGN.md / ADR)','Technical Spec Analyst': 'Аналитик технических спецификаций','Senior Frontend Developer': 'Ведущий frontend-разработчик','Senior Backend Developer': 'Ведущий backend-разработчик','QA Automation Engineer': 'Инженер автоматизации тестирования','Refactoring & Complexity Specialist': 'Специалист по рефакторингу и сложности','Hotfix & Diagnostic Engineer': 'Инженер хотфиксов и диагностики','Documentation Specialist': 'Технический писатель / Документация','Spike & R&D Researcher': 'Исследователь R&D и прототипирования','DevOps & Tooling Specialist': 'DevOps и инфраструктурный специалист','Instructions for Subagent': 'Инструкции для субагента','Start Pipeline': 'Запустить пайплайн','Delegate Subagent': 'Делегировать субагенту','Dispatching...': 'Отправка…','Discounted rate active': 'Действует сниженный тариф','off-peak': 'непиковый','peak': 'пиковый','Context cache saved:': 'Сэкономлено на кэше:','1M tokens, $': '1 млн токенов, $','Input (cache hit)': 'Ввод (попадание в кэш)','Input (cache miss)': 'Ввод (промах кэша)','Output': 'Вывод','SESSION TOKENS': 'ТОКЕНЫ СЕССИИ','SPEND BY MODEL': 'РАСХОД ПО МОДЕЛЯМ','Session total:': 'Всего за сессию:','Copy Summary': 'Скопировать сводку','Context Lens': 'Линза контекста','Saved tokens': 'Сэкономлено токенов','% Saved': '% экономии','ops': 'операций','Budget': 'Бюджет','Active focus': 'Активный фокус','No focus paths set': 'Пути фокусировки не заданы','Status': 'Статус','Graph & CI': 'Граф и CI','Events & PRs': 'События и PR','Branch': 'Ветка','Sync': 'Синхронизация','Up to date with remote (@{upstream})': 'Синхронизировано с удалённым репозиторием (@{upstream})','Up to date with remote': 'Синхронизировано с удалённым репозиторием','Clean': 'Чисто','No modified or untracked files in the working directory.': 'В рабочем каталоге нет изменённых или неотслеживаемых файлов.','RECENT COMMITS': 'ПОСЛЕДНИЕ КОММИТЫ','Active jobs': 'Активные задачи','No active jobs': 'Нет активных задач','Register in Models': 'Зарегистрировать в моделях','Base URL': 'Базовый URL','API key env / credential name': 'Имя переменной окружения / ключа API','Default model': 'Модель по умолчанию','Enabled': 'Включено','Save': 'Сохранить','Saved': 'Сохранено','Refresh': 'Обновить','Loading settings…': 'Загрузка настроек…','Preferred engine': 'Предпочитаемый движок','API key': 'API-ключ','stored, leave empty to keep it': 'сохранён, оставьте пустым для сохранения','Built-in official endpoint, leave blank to use it': 'Встроенная официальная конечная точка, оставьте пустым','Discard': 'Сбросить','Model synchronization': 'Синхронизация моделей','All API-key providers': 'Все провайдеры API-ключей','Preview only (dry-run)': 'Только предпросмотр (dry-run)','Confirm stale removal': 'Подтверждать удаление устаревших','Refresh status': 'Обновить статус','Refresh all': 'Обновить всё','Discover': 'Обнаружить','Check availability': 'Проверить доступность','Check credentials': 'Проверить учётные данные','Choose models': 'Выбрать модели','Manual model selection': 'Выбор моделей вручную','Mode': 'Режим','Hybrid (auto-rewrite + tools)': 'Гибридный (авто-переписывание + инструменты)','Describe strategy': 'Стратегия описания','Auto (use vision LLM)': 'Авто (использовать Vision LLM)','Escalation': 'Эскалация','Simple only (one pass)': 'Только простая (один проход)','Routing': 'Маршрутизация','Channel order': 'Порядок каналов','Issue Reporter': 'Репортёр проблем','GitHub sign-in is not configured for this installation.': 'Вход через GitHub не настроен для этой установки.','0 plugins': '0 плагинов','Catalog': 'Каталог','Report Editor': 'Редактор отчёта','My Reports': 'Мои отчёты','Authorization': 'Авторизация','Search installed plugins…': 'Поиск установленных плагинов…','Search installed plugins...': 'Поиск установленных плагинов...','Refresh inventory': 'Обновить список','Loading inventory & status…': 'Загрузка списка и статуса…','Loading inventory & status...': 'Загрузка списка и статуса...'
759
+ }
760
+ const ZH_WALKER = (root) => {try {const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
1308
761
  const hits = []
1309
762
  const curOverrides = getOverrides()
1310
- for (let node = walker.nextNode(); node; node = walker.nextNode()) {
1311
- const val = node.nodeValue
763
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {const val = node.nodeValue
1312
764
  if (!val) continue
1313
765
  const trimmed = val.trim()
1314
- if (curOverrides[trimmed] !== undefined) {
1315
- node.nodeValue = val.replace(trimmed, curOverrides[trimmed])
766
+ if (curOverrides[trimmed] !== undefined) {node.nodeValue = val.replace(trimmed, curOverrides[trimmed])
1316
767
  continue
1317
768
  }
1318
- if (trimmed.length >= 1 && ZH_CJK.test(val)) {
1319
- hits.push(node)
1320
- } else if (trimmed.length >= 1 && DOM_EN_TEXT[trimmed]) {
1321
- const p = node.parentElement
1322
- if (p && !p.closest('code, pre, script, style, textarea, input, select, kbd, samp, [contenteditable="true"], [data-composer-input], [role="textbox"]')) {
1323
- node.nodeValue = val.replace(trimmed, DOM_EN_TEXT[trimmed])
769
+ if (trimmed.length >= 1 && ZH_CJK.test(val)) {hits.push(node)
770
+ } else if (trimmed.length >= 1 && DOM_EN_TEXT[trimmed]) {const p = node.parentElement
771
+ 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])
1324
772
  }
1325
773
  }
1326
774
  }
1327
- for (const node of hits) {
1328
- const next = zhTranslateText(node.nodeValue)
775
+ for (const node of hits) {const next = zhTranslateText(node.nodeValue)
1329
776
  if (next && next !== node.nodeValue) node.nodeValue = next
1330
777
  }
1331
- for (const el of root.querySelectorAll ? root.querySelectorAll('[title],[placeholder],[aria-label]') : []) {
1332
- for (const attr of ['title', 'placeholder', 'aria-label']) {
1333
- const v = el.getAttribute && el.getAttribute(attr)
1334
- if (v) {
1335
- const tr = v.trim()
1336
- if (DOM_EN_TEXT[tr]) {
1337
- el.setAttribute(attr, v.replace(tr, DOM_EN_TEXT[tr]))
1338
- } else if (DOM_EN_ATTRS[tr]) {
1339
- el.setAttribute(attr, v.replace(tr, DOM_EN_ATTRS[tr]))
1340
- } else if (ZH_CJK.test(v)) {
1341
- const next = zhTranslateText(v)
778
+ for (const el of root.querySelectorAll ? root.querySelectorAll('[title],[placeholder],[aria-label]') : []) {for (const attr of ['title', 'placeholder', 'aria-label']) {const v = el.getAttribute && el.getAttribute(attr)
779
+ if (v) {const tr = v.trim()
780
+ if (DOM_EN_TEXT[tr]) {el.setAttribute(attr, v.replace(tr, DOM_EN_TEXT[tr]))
781
+ } else if (DOM_EN_ATTRS[tr]) {el.setAttribute(attr, v.replace(tr, DOM_EN_ATTRS[tr]))
782
+ } else if (ZH_CJK.test(v)) {const next = zhTranslateText(v)
1342
783
  if (next) el.setAttribute(attr, next)
1343
784
  }
1344
785
  }
1345
786
  }
1346
787
  }
1347
- if (root.getAttribute) {
1348
- for (const attr of ['title', 'placeholder', 'aria-label']) {
1349
- const v = root.getAttribute(attr)
1350
- if (v) {
1351
- const tr = v.trim()
788
+ if (root.getAttribute) {for (const attr of ['title', 'placeholder', 'aria-label']) {const v = root.getAttribute(attr)
789
+ if (v) {const tr = v.trim()
1352
790
  if (DOM_EN_TEXT[tr]) root.setAttribute(attr, v.replace(tr, DOM_EN_TEXT[tr]))
1353
791
  else if (DOM_EN_ATTRS[tr]) root.setAttribute(attr, v.replace(tr, DOM_EN_ATTRS[tr]))
1354
792
  }
@@ -1359,35 +797,22 @@ else if (DOM_EN_ATTRS[tr]) root.setAttribute(attr, v.replace(tr, DOM_EN_ATTRS[tr
1359
797
  let zhObserver = null
1360
798
  let zhDebounceTimer = null
1361
799
  let zhWalking = false
1362
- const queueZhWalk = (target) => {
1363
- if (zhDebounceTimer) return
1364
- zhDebounceTimer = setTimeout(() => {
1365
- zhDebounceTimer = null
800
+ const queueZhWalk = (target) => {if (zhDebounceTimer) return
801
+ zhDebounceTimer = setTimeout(() => {zhDebounceTimer = null
1366
802
  if (zhWalking) return
1367
803
  zhWalking = true
1368
- try {
1369
- ZH_WALKER(target || document.body)
1370
- } finally {
1371
- zhWalking = false
804
+ try {ZH_WALKER(target || document.body)
805
+ } finally {zhWalking = false
1372
806
  }
1373
807
  }, 120)
1374
808
  }
1375
- const syncZhDom = () => {
1376
- try {
1377
- if (typeof document === 'undefined') return
809
+ const syncZhDom = () => {try {if (typeof document === 'undefined') return
1378
810
  const ru = runtime.getLocale().active === 'ru'
1379
- if (!ru) {
1380
- if (zhObserver) { zhObserver.disconnect(); zhObserver = null }
811
+ if (!ru) {if (zhObserver) { zhObserver.disconnect(); zhObserver = null }
1381
812
  if (zhDebounceTimer) { clearTimeout(zhDebounceTimer); zhDebounceTimer = null }
1382
813
  return
1383
814
  }
1384
- if (!zhObserver) {
1385
- zhObserver = new MutationObserver((records) => {
1386
- for (const record of records) {
1387
- if (record.type === 'childList') {
1388
- for (const node of record.addedNodes) {
1389
- if (node.nodeType === 1) {
1390
- if (node.closest && node.closest('.chat-message, .markdown-body, pre, code, [data-stream]')) continue
815
+ 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
1391
816
  queueZhWalk(node)
1392
817
  return
1393
818
  }
@@ -1401,9 +826,7 @@ queueZhWalk(document.body)
1401
826
  } catch (err) { }
1402
827
  }
1403
828
  const unsubscribeZh = runtime.subscribe(syncZhDom)
1404
- ctx.effect(() => {
1405
- return () => {
1406
- unsubscribeZh()
829
+ ctx.effect(() => {return () => {unsubscribeZh()
1407
830
  if (zhObserver) zhObserver.disconnect()
1408
831
  if (zhDebounceTimer) { clearTimeout(zhDebounceTimer); zhDebounceTimer = null }
1409
832
  }
@@ -1411,28 +834,21 @@ if (zhDebounceTimer) { clearTimeout(zhDebounceTimer); zhDebounceTimer = null }
1411
834
  syncZhDom()
1412
835
  const TYPO_YO_PAIRS = [["еще","ещё"],["ее","её"],["черный","чёрный"],["черная","чёрная"],["черные","чёрные"],["зеленый","зелёный"],["желтый","жёлтый"],["легкий","лёгкий"],["тяжелый","тяжёлый"],["надежный","надёжный"],["дешевый","дешёвый"],["идет","идёт"],["дает","даёт"],["ведет","ведёт"],["несет","несёт"],["живет","живёт"],["привел","привёл"],["шел","шёл"],["нее","неё"],["мое","моё"],["серьезно","серьёзно"],["придется","придётся"],["насчет","насчёт"],["твое","твоё"],["свое","своё"],["пойдем","пойдём"],["пришел","пришёл"],["нашел","нашёл"],["идем","идём"],["вперед","вперёд"],["пошел","пошёл"],["ребенка","ребёнка"],["ребенок","ребёнок"],["счет","счёт"],["своем","своём"],["придет","придёт"],["ушел","ушёл"],["ждет","ждёт"],["мертв","мёртв"],["твоем","твоём"],["вернется","вернётся"],["днем","днём"],["найдем","найдём"],["пойдет","пойдёт"],["начнем","начнём"],["вернемся","вернёмся"],["остается","остаётся"],["принес","принёс"],["идешь","идёшь"],["трех","трёх"],["ребенком","ребёнком"],["самолет","самолёт"],["определенно","определённо"],["пойдешь","пойдёшь"],["умрет","умрёт"],["прием","приём"],["прошел","прошёл"],["убьет","убьёт"],["тетя","тётя"],["провел","провёл"],["произойдет","произойдёт"],["семьей","семьёй"],["пойдемте","пойдёмте"],["пройдет","пройдёт"],["возьмем","возьмём"],["вернешься","вернёшься"],["найдешь","найдёшь"],["живешь","живёшь"],["отчет","отчёт"],["займет","займёт"],["ждем","ждём"],["сошел","сошёл"],["вдвоем","вдвоём"],["найдет","найдёт"],["вел","вёл"],["живем","живём"],["зашел","зашёл"],["начнется","начнётся"],["путем","путём"],["подойдет","подойдёт"],["режиссер","режиссёр"],["уйдет","уйдёт"],["придешь","придёшь"],["идемте","идёмте"],["ведешь","ведёшь"],["пес","пёс"],["умрешь","умрёшь"],["ждешь","ждёшь"],["найдете","найдёте"],["найдется","найдётся"],["клево","клёво"],["поймешь","поймёшь"],["разберемся","разберёмся"],["времен","времён"],["введен","введён"],["включен","включён"],["включенных","включённых"],["возьмется","возьмётся"],["завершен","завершён"],["завершенного","завершённого"],["задает","задаёт"],["задается","задаётся"],["заменен","заменён"],["звезд","звёзд"],["звезды","звёзды"],["изменен","изменён"],["истек","истёк"],["незавершенная","незавершённая"],["неподтвержденные","неподтверждённые"],["несохраненные","несохранённые"],["обновлен","обновлён"],["обновленные","обновлённые"],["определен","определён"],["отклонен","отклонён"],["отменен","отменён"],["очередность","очерёдность"],["поврежден","повреждён"],["повторен","повторён"],["подключен","подключён"],["подтвержденные","подтверждённые"],["приемки","приёмки"],["раздает","раздаёт"],["разрешенная","разрешённая"],["разрешенные","разрешённые"],["создаем","создаём"],["сойдется","сойдётся"],["сохранен","сохранён"],["сохраненный","сохранённый"],["счету","счёту"],["тяжелые","тяжёлые"],["темная","тёмная"],["уберет","уберёт"],["удален","удалён"],["учетные","учётные"],["учетных","учётных"],["емкости","ёмкости"],["емкость","ёмкость"]]
1413
836
  const typoYo = makeTypoYo(TYPO_YO_PAIRS)
1414
- const getTypoConf = () => {
1415
- try {
1416
- const t = scope.getSnapshot().value && scope.getSnapshot().value.typography
837
+ const getTypoConf = () => {try {const t = scope.getSnapshot().value && scope.getSnapshot().value.typography
1417
838
  if (!t || t.enabled === false) return null
1418
839
  return { yo: t.yo === true }
1419
840
  } catch (err) { return null }
1420
841
  }
1421
- const typoNode = (node, conf) => {
1422
- const before = node.nodeValue
842
+ const typoNode = (node, conf) => {const before = node.nodeValue
1423
843
  if (!before || !before.match || (before.match(/[\u0400-\u04FF]/g) || []).length < 3) return
1424
844
  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
1425
845
  if (node.parentElement && node.parentElement.closest('.katex, [data-latex], math')) return
1426
846
  if (/\$[^$\n]+\$/.test(before)) return
1427
- if (node.parentElement && node.parentElement.closest('[data-chat-flow-status="running"], [data-turn-running], [data-turn-tail], [data-streaming="true"], .dsw-turn-running, [class*="streaming"], [class*="Streaming"]')) {
1428
- return
847
+ if (node.parentElement && node.parentElement.closest('[data-chat-flow-status="running"], [data-turn-running], [data-turn-tail], [data-streaming="true"], .dsw-turn-running, [class*="streaming"], [class*="Streaming"]')) {return
1429
848
  }
1430
849
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
1431
- if (sel && !sel.isCollapsed && sel.rangeCount > 0) {
1432
- try {
1433
- const range = sel.getRangeAt(0)
1434
- if (range.intersectsNode ? range.intersectsNode(node) : (sel.containsNode && sel.containsNode(node, true))) {
1435
- return
850
+ if (sel && !sel.isCollapsed && sel.rangeCount > 0) {try {const range = sel.getRangeAt(0)
851
+ if (range.intersectsNode ? range.intersectsNode(node) : (sel.containsNode && sel.containsNode(node, true))) {return
1436
852
  }
1437
853
  } catch (e) { void e; }
1438
854
  }
@@ -1443,44 +859,32 @@ after = typoNbsp(after)
1443
859
  if (conf.yo) after = typoYo(after)
1444
860
  if (after !== before) node.nodeValue = after
1445
861
  }
1446
- const typoWalk = (root, conf) => {
1447
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
862
+ const typoWalk = (root, conf) => {const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
1448
863
  for (let node = walker.nextNode(); node; node = walker.nextNode()) typoNode(node, conf)
1449
864
  }
1450
865
  let typoObserver = null
1451
866
  let typoQueued = null
1452
- const flushTypo = () => {
1453
- typoQueued = null
1454
- try {
1455
- const conf = getTypoConf()
867
+ const flushTypo = () => {typoQueued = null
868
+ try {const conf = getTypoConf()
1456
869
  if (!conf || !typoPending.size) return
1457
- for (const root of typoPending) {
1458
- if (root.nodeType === 3) typoNode(root, conf)
870
+ for (const root of typoPending) {if (root.nodeType === 3) typoNode(root, conf)
1459
871
  else typoWalk(root, conf)
1460
872
  }
1461
873
  typoPending.clear()
1462
874
  } catch (err) { }
1463
875
  }
1464
876
  const typoPending = new Set()
1465
- const queueTypo = (roots) => {
1466
- for (const r of roots) typoPending.add(r)
877
+ const queueTypo = (roots) => {for (const r of roots) typoPending.add(r)
1467
878
  if (!typoQueued) typoQueued = requestAnimationFrame(flushTypo)
1468
879
  }
1469
- const syncTypo = () => {
1470
- try {
1471
- if (typeof document === 'undefined') return
1472
- if (runtime.getLocale().active !== 'ru' || !getTypoConf()) {
1473
- if (typoObserver) { typoObserver.disconnect(); typoObserver = null }
880
+ const syncTypo = () => {try {if (typeof document === 'undefined') return
881
+ if (runtime.getLocale().active !== 'ru' || !getTypoConf()) {if (typoObserver) { typoObserver.disconnect(); typoObserver = null }
1474
882
  return
1475
883
  }
1476
884
  if (typoObserver) return
1477
885
  typoWalk(document.body, getTypoConf())
1478
- typoObserver = new MutationObserver((records) => {
1479
- const roots = []
1480
- for (const record of records) {
1481
- if (record.type === 'childList') {
1482
- for (const added of record.addedNodes) {
1483
- if (added.nodeType === 3 || added.nodeType === 1) roots.push(added)
886
+ typoObserver = new MutationObserver((records) => {const roots = []
887
+ 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)
1484
888
  }
1485
889
  }
1486
890
  }
@@ -1490,9 +894,7 @@ typoObserver.observe(document.body, { childList: true, subtree: true })
1490
894
  } catch (err) { }
1491
895
  }
1492
896
  const unsubscribeTypo = runtime.subscribe(syncTypo)
1493
- ctx.effect(() => {
1494
- return () => {
1495
- unsubscribeTypo()
897
+ ctx.effect(() => {return () => {unsubscribeTypo()
1496
898
  if (typoObserver) typoObserver.disconnect()
1497
899
  }
1498
900
  }, 'dsh-russian-lang: typography')
@@ -1500,51 +902,37 @@ syncTypo()
1500
902
  const layout = makeLayout(new Set(["не","что","ты","это","на","он","мы","как","вы","да","мне","нет","меня","так","но","его","все","она","тебя","если","за","бы","тебе","они","чтобы","же","есть","просто","из","для"]), new Set())
1501
903
  const layoutFixCandidate = layout.candidate
1502
904
  const learnWords = layout.learnWords
1503
- function setNativeInputValue(el, value, cursorStart, cursorEnd) {
1504
- if (!el) return
905
+ function setNativeInputValue(el, value, cursorStart, cursorEnd) {if (!el) return
1505
906
  const oldVal = el.value || ''
1506
907
  if (oldVal === value) return
1507
908
  const proto = el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype
1508
909
  const desc = Object.getOwnPropertyDescriptor(proto, 'value')
1509
- if (desc && desc.set) {
1510
- desc.set.call(el, value)
1511
- } else {
1512
- el.value = value
910
+ if (desc && desc.set) {desc.set.call(el, value)
911
+ } else {el.value = value
1513
912
  }
1514
- if (el._valueTracker) {
1515
- el._valueTracker.setValue(value === '' ? '__force__' : '')
913
+ if (el._valueTracker) {el._valueTracker.setValue(value === '' ? '__force__' : '')
1516
914
  }
1517
- try {
1518
- const propsKey = Object.keys(el).find((k) => k.startsWith('__reactProps$') || k.startsWith('__reactEventHandlers$'))
1519
- if (propsKey && el[propsKey] && typeof el[propsKey].onChange === 'function') {
1520
- el[propsKey].onChange({ target: el, currentTarget: el })
915
+ try {const propsKey = Object.keys(el).find((k) => k.startsWith('__reactProps$') || k.startsWith('__reactEventHandlers$'))
916
+ if (propsKey && el[propsKey] && typeof el[propsKey].onChange === 'function') {el[propsKey].onChange({ target: el, currentTarget: el })
1521
917
  }
1522
918
  } catch (e) { void e; }
1523
- try {
1524
- el.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }))
1525
- } catch (e) {
1526
- el.dispatchEvent(new Event('input', { bubbles: true }))
919
+ try {el.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }))
920
+ } catch (e) {el.dispatchEvent(new Event('input', { bubbles: true }))
1527
921
  }
1528
922
  el.dispatchEvent(new Event('change', { bubbles: true }))
1529
- if (typeof cursorStart === 'number' && typeof el.setSelectionRange === 'function') {
1530
- const end = typeof cursorEnd === 'number' ? cursorEnd : cursorStart
923
+ if (typeof cursorStart === 'number' && typeof el.setSelectionRange === 'function') {const end = typeof cursorEnd === 'number' ? cursorEnd : cursorStart
1531
924
  try { el.setSelectionRange(cursorStart, end) } catch (e) { void e; }
1532
925
  }
1533
926
  }
1534
927
  let layoutHintEl = null
1535
- const layoutCurrentInput = (ev) => {
1536
- if (ev && ev.target) {
1537
- const t = ev.target
1538
- if (t.tagName === 'TEXTAREA' || (t.tagName === 'INPUT' && (t.type === 'text' || !t.type))) {
1539
- return t
928
+ const layoutCurrentInput = (ev) => {if (ev && ev.target) {const t = ev.target
929
+ if (t.tagName === 'TEXTAREA' || (t.tagName === 'INPUT' && (t.type === 'text' || !t.type))) {return t
1540
930
  }
1541
931
  const c = t.closest ? t.closest('[data-composer-input], [contenteditable="true"], [role="textbox"]') : null
1542
932
  if (c) return c
1543
933
  }
1544
934
  const el = document.activeElement
1545
- if (el) {
1546
- if (el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && (el.type === 'text' || !el.type))) {
1547
- return el
935
+ if (el) {if (el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && (el.type === 'text' || !el.type))) {return el
1548
936
  }
1549
937
  const c = el.closest ? el.closest('[data-composer-input], [contenteditable="true"], [role="textbox"]') : null
1550
938
  if (c) return c
@@ -1553,43 +941,35 @@ const focused = document.querySelector('[data-composer-input], [contenteditable=
1553
941
  if (focused) return focused
1554
942
  return null
1555
943
  }
1556
- function getCaretCharacterOffset(root) {
1557
- if (!root) return -1
944
+ function getCaretCharacterOffset(root) {if (!root) return -1
1558
945
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
1559
946
  if (!sel || !sel.rangeCount) return -1
1560
- try {
1561
- const range = sel.getRangeAt(0)
947
+ try {const range = sel.getRangeAt(0)
1562
948
  if (!root.contains(range.startContainer)) return -1
1563
949
  const preCaretRange = range.cloneRange()
1564
950
  preCaretRange.selectNodeContents(root)
1565
951
  preCaretRange.setEnd(range.startContainer, range.startOffset)
1566
952
  return preCaretRange.toString().length
1567
- } catch (e) {
1568
- return -1
953
+ } catch (e) {return -1
1569
954
  }
1570
955
  }
1571
- function setCaretCharacterOffset(root, offset) {
1572
- if (!root || offset < 0) return
956
+ function setCaretCharacterOffset(root, offset) {if (!root || offset < 0) return
1573
957
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
1574
958
  if (!sel) return
1575
- try {
1576
- let current = 0
959
+ try {let current = 0
1577
960
  let targetNode = null
1578
961
  let targetOffset = 0
1579
962
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null)
1580
963
  let node = walker.nextNode()
1581
- while (node) {
1582
- const len = node.nodeValue.length
1583
- if (current + len >= offset) {
1584
- targetNode = node
964
+ while (node) {const len = node.nodeValue.length
965
+ if (current + len >= offset) {targetNode = node
1585
966
  targetOffset = Math.max(0, offset - current)
1586
967
  break
1587
968
  }
1588
969
  current += len
1589
970
  node = walker.nextNode()
1590
971
  }
1591
- if (targetNode) {
1592
- const range = document.createRange()
972
+ if (targetNode) {const range = document.createRange()
1593
973
  range.setStart(targetNode, targetOffset)
1594
974
  range.collapse(true)
1595
975
  sel.removeAllRanges()
@@ -1597,12 +977,10 @@ sel.addRange(range)
1597
977
  }
1598
978
  } catch (e) { void e; }
1599
979
  }
1600
- const isCaretInCode = (el, value, caretOffset) => {
1601
- if (!el) return false
980
+ const isCaretInCode = (el, value, caretOffset) => {if (!el) return false
1602
981
  if (caretOffset == null || caretOffset < 0) caretOffset = (value || '').length
1603
982
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
1604
- if (sel && sel.anchorNode && sel.anchorNode.parentElement) {
1605
- if (sel.anchorNode.parentElement.closest('code, pre, .katex, [data-latex], math')) return true
983
+ if (sel && sel.anchorNode && sel.anchorNode.parentElement) {if (sel.anchorNode.parentElement.closest('code, pre, .katex, [data-latex], math')) return true
1606
984
  }
1607
985
  const textBefore = (value || '').slice(0, caretOffset)
1608
986
  const triple = textBefore.match(/```/g)
@@ -1612,28 +990,22 @@ const single = lastLine.match(/`/g)
1612
990
  if (single && single.length % 2 === 1) return true
1613
991
  return false
1614
992
  }
1615
- const getComposerText = (el) => {
1616
- if (!el) return ''
1617
- if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
1618
- return el.value || ''
993
+ const getComposerText = (el) => {if (!el) return ''
994
+ if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {return el.value || ''
1619
995
  }
1620
996
  const host = (el.closest && el.closest('[data-composer-input], [contenteditable="true"]')) || el
1621
997
  const raw = host.innerText !== undefined ? host.innerText : (host.textContent || '')
1622
998
  return raw.replace(/\r/g, '').replace(/[\u200B\uFEFF]/g, '').replace(/\n+$/, '')
1623
999
  }
1624
- const setComposerText = (el, value, cursorStart, cursorEnd) => {
1625
- if (!el) return
1626
- if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
1627
- setNativeInputValue(el, value, cursorStart, cursorEnd)
1000
+ const setComposerText = (el, value, cursorStart, cursorEnd) => {if (!el) return
1001
+ if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {setNativeInputValue(el, value, cursorStart, cursorEnd)
1628
1002
  return
1629
1003
  }
1630
1004
  const host = (el.closest && el.closest('[data-composer-input], [contenteditable="true"]')) || el
1631
1005
  const initialOffset = typeof cursorStart === 'number' ? cursorStart : getCaretCharacterOffset(host)
1632
- try {
1633
- host.focus()
1006
+ try {host.focus()
1634
1007
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
1635
- if (sel) {
1636
- const range = document.createRange()
1008
+ if (sel) {const range = document.createRange()
1637
1009
  range.selectNodeContents(host)
1638
1010
  sel.removeAllRanges()
1639
1011
  sel.addRange(range)
@@ -1643,25 +1015,18 @@ const targetOffset = typeof cursorStart === 'number' ? cursorStart : (initialOff
1643
1015
  setCaretCharacterOffset(host, targetOffset)
1644
1016
  } catch (e) { void e; }
1645
1017
  }
1646
- const layoutDismiss = () => {
1647
- if (layoutHintEl) { layoutHintEl.remove(); layoutHintEl = null }
1018
+ const layoutDismiss = () => {if (layoutHintEl) { layoutHintEl.remove(); layoutHintEl = null }
1648
1019
  }
1649
- const layoutShowHint = (inputEl, converted, direction) => {
1650
- layoutDismiss()
1020
+ const layoutShowHint = (inputEl, converted, direction) => {layoutDismiss()
1651
1021
  layoutHintEl = document.createElement('div')
1652
1022
  layoutHintEl.dataset.russianLangLayout = '1'
1653
- Object.assign(layoutHintEl.style, {
1654
- position: 'fixed', zIndex: '99999', background: 'var(--dsw-alias-bg-layer-3)',
1655
- color: 'var(--dsw-alias-label-primary)',
1656
- border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', padding: '6px 10px',
1657
- fontSize: '13px', boxShadow: 'var(--dsw-alias-shadow-l2)', cursor: 'pointer'
1023
+ Object.assign(layoutHintEl.style, {position: 'fixed', zIndex: '99999', background: 'var(--dsw-alias-bg-layer-3)',color: 'var(--dsw-alias-label-primary)',border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', padding: '6px 10px',fontSize: '13px', boxShadow: 'var(--dsw-alias-shadow-l2)', cursor: 'pointer'
1658
1024
  })
1659
1025
  const label = direction === 'cyr2lat' ? 'Команда, не та раскладка' : 'Не та раскладка'
1660
1026
  layoutHintEl.textContent = label + ': ' + converted
1661
- layoutHintEl.addEventListener('mousedown', (ev) => {
1662
- ev.preventDefault()
1027
+ layoutHintEl.addEventListener('mousedown', (ev) => {ev.preventDefault()
1663
1028
  setComposerText(inputEl, converted)
1664
- learnWords(converted) // #67: запомнить принятые слова
1029
+ learnWords(converted)
1665
1030
  layoutDismiss()
1666
1031
  })
1667
1032
  document.body.appendChild(layoutHintEl)
@@ -1670,30 +1035,23 @@ layoutHintEl.style.left = (r.left + 8) + 'px'
1670
1035
  layoutHintEl.style.bottom = (window.innerHeight - r.top + 6) + 'px'
1671
1036
  }
1672
1037
  let layoutBadgeEl = null
1673
- const layoutBadge = (el) => {
1674
- const value = getComposerText(el)
1038
+ const layoutBadge = (el) => {const value = getComposerText(el)
1039
+ const detected = typeof detectInputLayout === 'function' ? detectInputLayout(value) : null
1675
1040
  const last = value.trim().slice(-1)
1676
1041
  const isCyr = /[\u0430-\u044f\u0451]/.test(last)
1677
1042
  const isLat = /[a-z]/i.test(last)
1678
- const label = isCyr ? 'RU' : (isLat ? 'EN' : '')
1043
+ const label = detected || (isCyr ? 'RU' : (isLat ? 'EN' : ''))
1679
1044
  if (!label) { layoutBadgeHide(); return }
1680
- if (!layoutBadgeEl) {
1681
- layoutBadgeEl = document.createElement('button')
1045
+ if (!layoutBadgeEl) {layoutBadgeEl = document.createElement('button')
1682
1046
  layoutBadgeEl.type = 'button'
1683
1047
  layoutBadgeEl.dataset.russianLangLayoutBadge = '1'
1684
- Object.assign(layoutBadgeEl.style, {
1685
- position: 'fixed', zIndex: '99998', background: 'var(--dsw-alias-bg-layer-3)',
1686
- color: 'var(--dsw-alias-label-secondary)', border: '1px solid var(--dsw-alias-border-l2)',
1687
- borderRadius: '6px', padding: '1px 6px', fontSize: '11px', cursor: 'pointer',
1688
- fontFamily: 'monospace', lineHeight: '1.4', fontWeight: '600'
1048
+ Object.assign(layoutBadgeEl.style, {position: 'fixed', zIndex: '99998', background: 'var(--dsw-alias-bg-layer-3)',color: 'var(--dsw-alias-label-secondary)', border: '1px solid var(--dsw-alias-border-l2)',borderRadius: '6px', padding: '1px 6px', fontSize: '11px', cursor: 'pointer',fontFamily: 'monospace', lineHeight: '1.4', fontWeight: '600'
1689
1049
  })
1690
1050
  layoutBadgeEl.title = 'Раскладка — клик: конвертировать (Alt+L)'
1691
- layoutBadgeEl.addEventListener('mousedown', (ev) => {
1692
- ev.preventDefault()
1051
+ layoutBadgeEl.addEventListener('mousedown', (ev) => {ev.preventDefault()
1693
1052
  const v = getComposerText(el)
1694
1053
  const c = layoutFixCandidate(v, 'lat2cyr') || layoutFixCandidate(v, 'cyr2lat')
1695
- if (c) {
1696
- setComposerText(el, c.converted)
1054
+ if (c) {setComposerText(el, c.converted)
1697
1055
  learnWords(c.converted)
1698
1056
  }
1699
1057
  })
@@ -1704,138 +1062,108 @@ const r = el.getBoundingClientRect()
1704
1062
  layoutBadgeEl.style.left = (r.right - 36) + 'px'
1705
1063
  layoutBadgeEl.style.top = (r.top - 22) + 'px'
1706
1064
  }
1707
- const layoutBadgeHide = () => {
1708
- if (layoutBadgeEl) { layoutBadgeEl.remove(); layoutBadgeEl = null }
1065
+ const layoutBadgeHide = () => {if (layoutBadgeEl) { layoutBadgeEl.remove(); layoutBadgeEl = null }
1709
1066
  }
1710
1067
  let isFormatting = false
1711
- const layoutOnInput = (ev) => {
1712
- if (isFormatting) return
1713
- try {
1714
- const el = layoutCurrentInput(ev)
1068
+ const layoutOnInput = (ev) => {if (isFormatting) return
1069
+ try {const el = layoutCurrentInput(ev)
1715
1070
  if (!el) { layoutDismiss(); layoutBadgeHide(); return }
1716
- layoutBadge(el) // #66: метка раскладки
1071
+ layoutBadge(el)
1717
1072
  const value = getComposerText(el)
1718
1073
  if (value.trim().length < 4) { layoutDismiss(); return }
1719
1074
  const latCount = (value.match(/[a-z]/g) || []).length
1720
1075
  const cyrCount = (value.match(/[\u0430-\u044f\u0451]/g) || []).length
1721
- if (latCount > cyrCount && cyrCount === 0) {
1722
- const c = layoutFixCandidate(value, 'lat2cyr')
1076
+ if (latCount > cyrCount && cyrCount === 0) {const c = layoutFixCandidate(value, 'lat2cyr')
1723
1077
  if (c) { layoutShowHint(el, c.converted, 'ru'); return }
1724
1078
  }
1725
- if (cyrCount > 0 && latCount === 0 && value.trim().startsWith('/')) {
1726
- const c = layoutFixCandidate(value, 'cyr2lat')
1079
+ if (cyrCount > 0 && latCount === 0 && value.trim().startsWith('/')) {const c = layoutFixCandidate(value, 'cyr2lat')
1727
1080
  if (c) { layoutShowHint(el, c.converted, 'cmd'); return }
1728
1081
  }
1729
1082
  layoutDismiss()
1730
1083
  } catch (err) { }
1731
1084
  }
1732
1085
  const unsubscribeLayout = runtime.subscribe(layoutOnInput)
1733
- const layoutOnKeydown = (ev) => {
1734
- const el = layoutCurrentInput(ev)
1086
+ const layoutOnKeydown = (ev) => {const el = layoutCurrentInput(ev)
1735
1087
  if (!el) return
1736
1088
  const value = getComposerText(el)
1737
1089
  const isL = ev.code === 'KeyL' || ev.key.toLowerCase() === 'l' || ev.key.toLowerCase() === 'д'
1738
- if (ev.altKey && !ev.ctrlKey && !ev.metaKey && isL) {
1739
- const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
1090
+ if (ev.altKey && !ev.ctrlKey && !ev.metaKey && isL) {const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
1740
1091
  const selectedText = (sel && !sel.isCollapsed) ? sel.toString() : ''
1741
- if (selectedText) {
1742
- const c = layoutFixCandidate(selectedText, 'lat2cyr') || layoutFixCandidate(selectedText, 'cyr2lat')
1743
- if (c) {
1744
- ev.preventDefault()
1092
+ if (selectedText) {const c = layoutFixCandidate(selectedText, 'lat2cyr') || layoutFixCandidate(selectedText, 'cyr2lat')
1093
+ if (c) {ev.preventDefault()
1745
1094
  isFormatting = true
1746
- try {
1747
- if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
1748
- const sStart = el.selectionStart || 0
1095
+ try {if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {const sStart = el.selectionStart || 0
1749
1096
  const sEnd = el.selectionEnd || sStart
1750
1097
  const nextVal = value.slice(0, sStart) + c.converted + value.slice(sEnd)
1751
1098
  setNativeInputValue(el, nextVal, sStart, sStart + c.converted.length)
1752
- } else {
1753
- document.execCommand('insertText', false, c.converted)
1099
+ } else {document.execCommand('insertText', false, c.converted)
1754
1100
  }
1755
1101
  learnWords(c.converted)
1756
- } finally {
1757
- isFormatting = false
1102
+ } finally {isFormatting = false
1758
1103
  }
1759
1104
  return
1760
1105
  }
1761
1106
  }
1762
1107
  const c = layoutFixCandidate(value, 'lat2cyr') || layoutFixCandidate(value, 'cyr2lat')
1763
- if (c) {
1764
- ev.preventDefault()
1108
+ if (c) {ev.preventDefault()
1765
1109
  isFormatting = true
1766
- try {
1767
- if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
1768
- const sStart = el.selectionStart || 0
1110
+ try {if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {const sStart = el.selectionStart || 0
1769
1111
  const sEnd = el.selectionEnd || sStart
1770
1112
  setNativeInputValue(el, c.converted, sStart, sEnd)
1771
- } else {
1772
- const host = (el.closest && el.closest('[data-composer-input], [contenteditable="true"]')) || el
1113
+ } else {const host = (el.closest && el.closest('[data-composer-input], [contenteditable="true"]')) || el
1773
1114
  const offset = getCaretCharacterOffset(host)
1774
1115
  setComposerText(el, c.converted, offset, offset)
1775
1116
  }
1776
- learnWords(c.converted) // #67
1777
- } finally {
1778
- isFormatting = false
1117
+ learnWords(c.converted)
1118
+ } finally {isFormatting = false
1779
1119
  }
1780
1120
  }
1781
1121
  return
1782
1122
  }
1783
- if (ev.key === ' ' || ev.key === 'Enter') {
1784
- if (value && value.startsWith('/') && typeof expandSlashAlias === 'function') {
1785
- const expanded = expandSlashAlias(value)
1786
- if (expanded !== value) {
1787
- if (ev.key === ' ') {
1788
- ev.preventDefault()
1123
+ if (ev.key === ' ' || ev.key === 'Enter') {if (value && value.startsWith('/') && typeof expandSlashAlias === 'function') {const expanded = expandSlashAlias(value)
1124
+ if (expanded !== value) {if (ev.key === ' ') {ev.preventDefault()
1789
1125
  isFormatting = true
1790
- try {
1791
- const nextVal = expanded + ' '
1126
+ try {const nextVal = expanded + ' '
1792
1127
  setComposerText(el, nextVal, nextVal.length, nextVal.length)
1793
- } finally {
1794
- isFormatting = false
1128
+ } finally {isFormatting = false
1795
1129
  }
1796
1130
  return
1797
- } else if (ev.key === 'Enter') {
1798
- isFormatting = true
1799
- try {
1800
- setComposerText(el, expanded, expanded.length, expanded.length)
1801
- } finally {
1802
- isFormatting = false
1131
+ } else if (ev.key === 'Enter') {isFormatting = true
1132
+ try {setComposerText(el, expanded, expanded.length, expanded.length)
1133
+ } finally {isFormatting = false
1134
+ }
1803
1135
  }
1804
1136
  }
1805
1137
  }
1806
1138
  }
1139
+ if (ev.key === 'Enter' && !ev.shiftKey && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {const snapVal = scope ? (scope.getSnapshot().value || {}) : {}
1140
+ const typoLive = snapVal.typography ? snapVal.typography.liveInput === true : false
1141
+ if (typoLive && typeof formatInputLive === 'function' && (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT')) {const formatted = formatInputLive(value)
1142
+ if (formatted && formatted !== value) {setNativeInputValue(el, formatted, formatted.length, formatted.length)
1807
1143
  }
1808
- try {
1809
- const snapVal = scope ? (scope.getSnapshot().value || {}) : {}
1810
- const typoLive = snapVal.typography ? snapVal.typography.liveInput !== false : true
1811
- if (typoLive && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {
1812
- const isTextarea = el.tagName === 'TEXTAREA' || el.tagName === 'INPUT'
1813
- if (isTextarea) {
1814
- const caretPos = el.selectionStart || 0
1815
- if (!isCaretInCode(el, value, caretPos)) {
1816
- const textBefore = caretPos >= 0 ? value.slice(0, caretPos) : value
1817
- if (ev.key === '-' && textBefore.endsWith('-')) {
1818
- ev.preventDefault()
1144
+ }
1145
+ }
1146
+ try {const snapVal = scope ? (scope.getSnapshot().value || {}) : {}
1147
+ const typoLive = snapVal.typography ? snapVal.typography.liveInput === true : false
1148
+ if (typoLive && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {const isTextarea = el.tagName === 'TEXTAREA' || el.tagName === 'INPUT'
1149
+ if (isTextarea) {const caretPos = el.selectionStart || 0
1150
+ if (!isCaretInCode(el, value, caretPos)) {const textBefore = caretPos >= 0 ? value.slice(0, caretPos) : value
1151
+ if (ev.key === '-' && textBefore.endsWith('-')) {ev.preventDefault()
1819
1152
  isFormatting = true
1820
- try {
1821
- const nextVal = value.slice(0, caretPos - 1) + '—' + value.slice(el.selectionEnd || caretPos)
1153
+ try {const nextVal = value.slice(0, caretPos - 1) + '—' + value.slice(el.selectionEnd || caretPos)
1822
1154
  setNativeInputValue(el, nextVal, caretPos, caretPos)
1823
- } finally {
1824
- isFormatting = false
1155
+ } finally {isFormatting = false
1825
1156
  }
1826
1157
  return
1827
1158
  }
1828
- if (ev.key === '"') {
1829
- ev.preventDefault()
1159
+ if (ev.key === '"') {ev.preventDefault()
1830
1160
  isFormatting = true
1831
- try {
1832
- const prevChar = textBefore.slice(-1)
1161
+ try {const prevChar = textBefore.slice(-1)
1833
1162
  const isOpening = !textBefore || /[\s([{-]/.test(prevChar)
1834
1163
  const quoteChar = isOpening ? '«' : '»'
1835
1164
  const nextVal = value.slice(0, caretPos) + quoteChar + value.slice(el.selectionEnd || caretPos)
1836
1165
  setNativeInputValue(el, nextVal, caretPos + 1, caretPos + 1)
1837
- } finally {
1838
- isFormatting = false
1166
+ } finally {isFormatting = false
1839
1167
  }
1840
1168
  return
1841
1169
  }
@@ -1844,13 +1172,8 @@ return
1844
1172
  }
1845
1173
  } catch (e) { void e; }
1846
1174
  }
1847
- let isInspectorActive = false
1848
- let hoverBoxEl = null
1849
- let pillEl = null
1850
- const getHoverBox = () => {
1851
- if (typeof document === 'undefined') return null
1852
- if (!hoverBoxEl) {
1853
- hoverBoxEl = document.createElement('div')
1175
+ const getHoverBox = () => {if (typeof document === 'undefined') return null
1176
+ if (!hoverBoxEl) {hoverBoxEl = document.createElement('div')
1854
1177
  hoverBoxEl.id = 'dsh-ru-inspector-hover'
1855
1178
  hoverBoxEl.className = 'rl-inspector-hover'
1856
1179
  hoverBoxEl.innerHTML = '<span class="rl-inspector-badge"></span>'
@@ -1858,16 +1181,13 @@ document.body.appendChild(hoverBoxEl)
1858
1181
  }
1859
1182
  return hoverBoxEl
1860
1183
  }
1861
- const updateInspectorPill = () => {
1862
- if (typeof document === 'undefined') return
1863
- if (!pillEl) {
1864
- pillEl = document.createElement('div')
1184
+ const updateInspectorPill = () => {if (typeof document === 'undefined') return
1185
+ if (!pillEl) {pillEl = document.createElement('div')
1865
1186
  pillEl.id = 'dsh-ru-inspector-pill'
1866
1187
  pillEl.className = 'rl-inspector-pill'
1867
1188
  pillEl.innerHTML = '<span>🔍 Инспектор</span>' +
1868
1189
  '<button type="button" class="rl-btn rl-pill-exit" style="padding:2px 8px;font-size:11px;height:22px;">✕ Выйти (Alt+I)</button>'
1869
- pillEl.querySelector('.rl-pill-exit').onclick = () => {
1870
- isInspectorActive = false
1190
+ pillEl.querySelector('.rl-pill-exit').onclick = () => {isInspectorActive = false
1871
1191
  updateInspectorPill()
1872
1192
  if (hoverBoxEl) hoverBoxEl.style.display = 'none'
1873
1193
  }
@@ -1876,8 +1196,7 @@ document.body.appendChild(pillEl)
1876
1196
  pillEl.style.display = isInspectorActive ? 'flex' : 'none'
1877
1197
  if (isInspectorActive) pillEl.classList.add('rl-inspector-pill-active')
1878
1198
  }
1879
- const openInspectorModal = (meta, rawText, targetElement) => {
1880
- if (typeof document === 'undefined') return
1199
+ const openInspectorModal = (meta, rawText, targetElement) => {if (typeof document === 'undefined') return
1881
1200
  if (hoverBoxEl) hoverBoxEl.style.display = 'none'
1882
1201
  const old = document.getElementById('dsh-ru-inspector-modal')
1883
1202
  if (old) old.remove()
@@ -1888,8 +1207,7 @@ const k = meta ? (meta.key || (meta.ns ? meta.ns + '.' + meta.key : '')) : ''
1888
1207
  const ns = meta && meta.ns ? meta.ns : ''
1889
1208
  const origText = meta ? (meta.zh || meta.en || '') : ''
1890
1209
  let srcLabel = 'Текст'
1891
- if (meta) {
1892
- if (meta.source === 'override') srcLabel = '✍️ Оверрайд'
1210
+ if (meta) {if (meta.source === 'override') srcLabel = '✍️ Оверрайд'
1893
1211
  else if (meta.source === 'dictionary') srcLabel = '🟢 Словарь' + (ns ? ' ' + ns : '')
1894
1212
  else if (meta.source === 'dom_zh') srcLabel = '🟡 DOM ZH'
1895
1213
  else if (meta.source === 'dom_zh_original') srcLabel = '🟡 DOM ZH (Оригинал)'
@@ -1903,7 +1221,7 @@ modal.innerHTML = '<div class="rl-modal-box">' +
1903
1221
  '<div style="display:flex;justify-content:space-between;align-items:center;">' +
1904
1222
  '<div style="font-weight:600;font-size:14px;display:flex;align-items:center;gap:6px;">' +
1905
1223
  '<span>🔍 Инспектор перевода</span>' +
1906
- '<span class="rl-badge rl-badge-dim" style="font-size:11px;">' + srcLabel + '</span>' +
1224
+ '<span class="rl-badge rl-badge-dim" style="font-size:11px;"></span>' +
1907
1225
  '</div>' +
1908
1226
  '<button type="button" class="rl-btn-close" style="background:none;border:none;color:var(--dsw-alias-label-secondary);font-size:18px;cursor:pointer;">&times;</button>' +
1909
1227
  '</div>' +
@@ -1911,11 +1229,11 @@ modal.innerHTML = '<div class="rl-modal-box">' +
1911
1229
  '<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px;">' +
1912
1230
  '<span style="color:var(--dsw-alias-label-secondary);">Ключ:</span>' +
1913
1231
  '<div style="display:flex;gap:6px;align-items:center;">' +
1914
- '<code class="rl-modal-key" style="color:var(--dsw-alias-state-brand-primary);font-weight:600;font-size:12px;">' + (k || '(текстовый узел)') + '</code>' +
1232
+ '<code class="rl-modal-key" style="color:var(--dsw-alias-state-brand-primary);font-weight:600;font-size:12px;"></code>' +
1915
1233
  '<button type="button" class="rl-btn rl-copy-key-btn" style="padding:1px 6px;height:20px;font-size:10px;" title="Скопировать ключ">📋</button>' +
1916
1234
  '</div>' +
1917
1235
  '</div>' +
1918
- (origText ? '<div style="color:var(--dsw-alias-label-secondary);word-break:break-word;">Оригинал: <b style="color:var(--dsw-alias-label-primary);">' + origText + '</b></div>' : '') +
1236
+ '<div class="rl-modal-orig-row" style="color:var(--dsw-alias-label-secondary);word-break:break-word;display:none;">Оригинал: <b class="rl-orig-text" style="color:var(--dsw-alias-label-primary);"></b></div>' +
1919
1237
  '<div style="color:var(--dsw-alias-label-secondary);word-break:break-word;">Текущий текст: <b class="rl-raw-text" style="color:var(--dsw-alias-label-primary)"></b></div>' +
1920
1238
  '</div>' +
1921
1239
  '<div style="display:flex;flex-direction:column;gap:6px;">' +
@@ -1931,6 +1249,13 @@ modal.innerHTML = '<div class="rl-modal-box">' +
1931
1249
  '</div>' +
1932
1250
  '</div>' +
1933
1251
  '</div>'
1252
+ modal.querySelector('.rl-badge-dim').textContent = srcLabel
1253
+ modal.querySelector('.rl-modal-key').textContent = k || '(текстовый узел)'
1254
+ if (origText) {const row = modal.querySelector('.rl-modal-orig-row')
1255
+ if (row) row.style.display = 'block'
1256
+ const el = modal.querySelector('.rl-orig-text')
1257
+ if (el) el.textContent = origText
1258
+ }
1934
1259
  modal.querySelector('.rl-raw-text').textContent = rawText
1935
1260
  const textarea = modal.querySelector('.rl-edit-val')
1936
1261
  textarea.value = currentTranslation
@@ -1938,18 +1263,14 @@ modal.querySelector('.rl-btn-close').onclick = () => modal.remove()
1938
1263
  modal.querySelector('.rl-cancel-btn').onclick = () => modal.remove()
1939
1264
  modal.onclick = (e) => { if (e.target === modal) modal.remove() }
1940
1265
  const copyKeyBtn = modal.querySelector('.rl-copy-key-btn')
1941
- if (copyKeyBtn) {
1942
- copyKeyBtn.onclick = () => {
1943
- try { navigator.clipboard.writeText(k || rawText) } catch (err) { void err; }
1266
+ if (copyKeyBtn) {copyKeyBtn.onclick = () => {try { navigator.clipboard.writeText(k || rawText) } catch (err) { void err; }
1944
1267
  copyKeyBtn.textContent = '✓'
1945
1268
  setTimeout(() => { copyKeyBtn.textContent = '📋' }, 1500)
1946
1269
  }
1947
1270
  }
1948
1271
  const copyReportBtn = modal.querySelector('.rl-copy-report-btn')
1949
- if (copyReportBtn) {
1950
- copyReportBtn.onclick = () => {
1951
- const snippet = typeof generateBugReportSnippet === 'function'
1952
- ? generateBugReportSnippet({ key: k, ns, original: origText || rawText, current: rawText, override: textarea.value.trim(), pkgVersion: '0.3.7' })
1272
+ if (copyReportBtn) {copyReportBtn.onclick = () => {const snippet = typeof generateBugReportSnippet === 'function'
1273
+ ? generateBugReportSnippet({ key: k, ns, original: origText || rawText, current: rawText, override: textarea.value.trim(), pkgVersion: '0.3.9' })
1953
1274
  : ('### Репорт: ' + (k || rawText) + '\n- Исходный: ' + (origText || rawText) + '\n- Перевод: ' + textarea.value.trim())
1954
1275
  try { navigator.clipboard.writeText(snippet) } catch (err) { void err; }
1955
1276
  copyReportBtn.textContent = '✓ Скопировано'
@@ -1957,9 +1278,7 @@ setTimeout(() => { copyReportBtn.textContent = '📋 Скопировать ре
1957
1278
  }
1958
1279
  }
1959
1280
  const resetBtn = modal.querySelector('.rl-reset-override-btn')
1960
- if (resetBtn) {
1961
- resetBtn.onclick = () => {
1962
- const targetKey = k || rawText
1281
+ if (resetBtn) {resetBtn.onclick = () => {const targetKey = k || rawText
1963
1282
  const cur = getOverrides()
1964
1283
  const updated = Object.assign({}, cur)
1965
1284
  delete updated[targetKey]
@@ -1969,19 +1288,14 @@ if (typeof queueZhWalk === 'function') queueZhWalk()
1969
1288
  }
1970
1289
  }
1971
1290
  const saveBtn = modal.querySelector('.rl-save-btn')
1972
- saveBtn.onclick = () => {
1973
- const val = textarea.value.trim()
1291
+ saveBtn.onclick = () => {const val = textarea.value.trim()
1974
1292
  const targetKey = k || rawText
1975
1293
  if (!targetKey || !val) return
1976
1294
  const cur = getOverrides()
1977
1295
  const updated = Object.assign({}, cur, { [targetKey]: val })
1978
1296
  try { scope.set('overrides', updated) } catch (_e) { void _e }
1979
- if (targetElement) {
1980
- try {
1981
- if (targetElement.childNodes && targetElement.childNodes.length === 1 && targetElement.childNodes[0].nodeType === 3) {
1982
- targetElement.childNodes[0].nodeValue = val
1983
- } else {
1984
- targetElement.textContent = val
1297
+ if (targetElement) {try {if (targetElement.childNodes && targetElement.childNodes.length === 1 && targetElement.childNodes[0].nodeType === 3) {targetElement.childNodes[0].nodeValue = val
1298
+ } else {targetElement.textContent = val
1985
1299
  }
1986
1300
  } catch (err) { void err; }
1987
1301
  }
@@ -1993,8 +1307,7 @@ setTimeout(() => modal.remove(), 500)
1993
1307
  document.body.appendChild(modal)
1994
1308
  setTimeout(() => textarea.focus(), 50)
1995
1309
  }
1996
- const inspectorOnClick = (e) => {
1997
- if (!e.altKey && !isInspectorActive) return
1310
+ const inspectorOnClick = (e) => {if (!e.altKey && !isInspectorActive) return
1998
1311
  if (e.target && e.target.closest && (e.target.closest('#dsh-ru-inspector-modal') || e.target.closest('#dsh-ru-inspector-pill'))) return
1999
1312
  const raw = (e.target && (e.target.innerText || e.target.textContent) || '').trim()
2000
1313
  if (!raw) return
@@ -2003,15 +1316,12 @@ e.stopPropagation()
2003
1316
  const meta = translationRegistry.get(raw) || (typeof findTranslationKey === 'function' ? findTranslationKey(raw, RU, getOverrides(), ZH_RU, DOM_EN_TEXT, { detectUntranslated: true }) : null)
2004
1317
  openInspectorModal(meta, raw, e.target)
2005
1318
  }
2006
- const inspectorOnMouseMove = (e) => {
2007
- if (!isInspectorActive && !e.altKey) {
2008
- if (hoverBoxEl && hoverBoxEl.style.display !== 'none') hoverBoxEl.style.display = 'none'
1319
+ const inspectorOnMouseMove = (e) => {if (!isInspectorActive && !e.altKey) {if (hoverBoxEl && hoverBoxEl.style.display !== 'none') hoverBoxEl.style.display = 'none'
2009
1320
  return
2010
1321
  }
2011
1322
  if (!e.target || (e.target.closest && (e.target.closest('#dsh-ru-inspector-modal') || e.target.closest('#dsh-ru-inspector-pill') || e.target.closest('#dsh-ru-inspector-hover')))) return
2012
1323
  const raw = (e.target.innerText || e.target.textContent || '').trim()
2013
- if (!raw || raw.length > 200) {
2014
- if (hoverBoxEl) hoverBoxEl.style.display = 'none'
1324
+ if (!raw || raw.length > 200) {if (hoverBoxEl) hoverBoxEl.style.display = 'none'
2015
1325
  return
2016
1326
  }
2017
1327
  const box = getHoverBox()
@@ -2023,11 +1333,9 @@ box.style.width = rect.width + 'px'
2023
1333
  box.style.height = rect.height + 'px'
2024
1334
  box.style.display = 'block'
2025
1335
  const badge = box.querySelector('.rl-inspector-badge')
2026
- if (badge) {
2027
- const meta = translationRegistry.get(raw) || (typeof findTranslationKey === 'function' ? findTranslationKey(raw, RU, getOverrides(), ZH_RU, DOM_EN_TEXT, { detectUntranslated: true }) : null)
1336
+ if (badge) {const meta = translationRegistry.get(raw) || (typeof findTranslationKey === 'function' ? findTranslationKey(raw, RU, getOverrides(), ZH_RU, DOM_EN_TEXT, { detectUntranslated: true }) : null)
2028
1337
  let title = '🔍 Клик: инспекция'
2029
- if (meta) {
2030
- if (meta.source === 'override') title = '✍️ ' + meta.key
1338
+ if (meta) {if (meta.source === 'override') title = '✍️ ' + meta.key
2031
1339
  else if (meta.source === 'dictionary') title = '🟢 ' + (meta.ns ? meta.ns + '.' : '') + meta.key
2032
1340
  else if (meta.source === 'dom_zh') title = '🟡 ' + meta.zh
2033
1341
  else if (meta.source === 'dom_en') title = '🟡 ' + meta.en
@@ -2037,26 +1345,20 @@ else if (meta.source === 'untranslated_en') title = '🔴 EN'
2037
1345
  badge.textContent = title
2038
1346
  }
2039
1347
  }
2040
- const inspectorOnKeyDown = (e) => {
2041
- if (e.altKey && (e.key === 'i' || e.key === 'I' || e.code === 'KeyI')) {
2042
- e.preventDefault()
1348
+ const inspectorOnKeyDown = (e) => {if (e.altKey && (e.key === 'i' || e.key === 'I' || e.code === 'KeyI')) {e.preventDefault()
2043
1349
  isInspectorActive = !isInspectorActive
2044
1350
  updateInspectorPill()
2045
1351
  if (!isInspectorActive && hoverBoxEl) hoverBoxEl.style.display = 'none'
2046
1352
  }
2047
1353
  }
2048
- const inspectorOnKeyUp = (e) => {
2049
- if (e.key === 'Alt' && !isInspectorActive) {
2050
- if (hoverBoxEl) hoverBoxEl.style.display = 'none'
1354
+ const inspectorOnKeyUp = (e) => {if (e.key === 'Alt' && !isInspectorActive) {if (hoverBoxEl) hoverBoxEl.style.display = 'none'
2051
1355
  }
2052
1356
  }
2053
- ctx.effect(() => {
2054
- document.addEventListener('click', inspectorOnClick, true)
1357
+ ctx.effect(() => {document.addEventListener('click', inspectorOnClick, true)
2055
1358
  document.addEventListener('mousemove', inspectorOnMouseMove, true)
2056
1359
  document.addEventListener('keydown', inspectorOnKeyDown, true)
2057
1360
  document.addEventListener('keyup', inspectorOnKeyUp, true)
2058
- return () => {
2059
- document.removeEventListener('click', inspectorOnClick, true)
1361
+ return () => {document.removeEventListener('click', inspectorOnClick, true)
2060
1362
  document.removeEventListener('mousemove', inspectorOnMouseMove, true)
2061
1363
  document.removeEventListener('keydown', inspectorOnKeyDown, true)
2062
1364
  document.removeEventListener('keyup', inspectorOnKeyUp, true)
@@ -2073,16 +1375,10 @@ runtime.bilingualCommandMatch = (query, cmd, opts) => (typeof bilingualCommandMa
2073
1375
  runtime.filterCommands = (query, cmds, opts) => (typeof filterCommands === 'function' ? filterCommands(query, cmds, opts) : cmds)
2074
1376
  runtime.humanizeError = (err) => (typeof humanizeError === 'function' ? humanizeError(err) : null)
2075
1377
  runtime.formatErrorToast = (err) => (typeof formatErrorToast === 'function' ? formatErrorToast(err) : null)
2076
- const toastObserver = typeof MutationObserver !== 'undefined' ? new MutationObserver((mutations) => {
2077
- for (const m of mutations) {
2078
- for (const node of m.addedNodes) {
2079
- if (!node || node.nodeType !== 1) continue
2080
- if (node.matches && (node.matches('.dsw-toast, [role="alert"], .toast, .ant-message-notice'))) {
2081
- const text = (node.innerText || node.textContent || '').trim()
2082
- if (/failed to fetch|network error|econnrefused|etimedout/i.test(text)) {
2083
- const h = typeof humanizeError === 'function' ? humanizeError(text) : null
2084
- if (h && h.title) {
2085
- const targetEl = node.querySelector('.toast-body, .ant-message-custom-content') || node
1378
+ const toastObserver = typeof MutationObserver !== 'undefined' ? new MutationObserver((mutations) => {for (const m of mutations) {for (const node of m.addedNodes) {if (!node || node.nodeType !== 1) continue
1379
+ if (node.matches && (node.matches('.dsw-toast, [role="alert"], .toast, .ant-message-notice'))) {const text = (node.innerText || node.textContent || '').trim()
1380
+ if (/failed to fetch|network error|econnrefused|etimedout/i.test(text)) {const h = typeof humanizeError === 'function' ? humanizeError(text) : null
1381
+ if (h && h.title) {const targetEl = node.querySelector('.toast-body, .ant-message-custom-content') || node
2086
1382
  targetEl.textContent = h.title + ': ' + h.message
2087
1383
  }
2088
1384
  }
@@ -2090,17 +1386,13 @@ targetEl.textContent = h.title + ': ' + h.message
2090
1386
  }
2091
1387
  }
2092
1388
  }) : null
2093
- ctx.effect(() => {
2094
- if (toastObserver && typeof document !== 'undefined' && document.body) {
2095
- toastObserver.observe(document.body, { childList: true, subtree: true })
1389
+ ctx.effect(() => {if (toastObserver && typeof document !== 'undefined' && document.body) {toastObserver.observe(document.body, { childList: true, subtree: true })
2096
1390
  }
2097
1391
  return () => { if (toastObserver) toastObserver.disconnect() }
2098
1392
  }, 'dsh-russian-lang: toast-humanizer')
2099
- ctx.effect(() => {
2100
- document.addEventListener('input', layoutOnInput, true)
1393
+ ctx.effect(() => {document.addEventListener('input', layoutOnInput, true)
2101
1394
  document.addEventListener('keydown', layoutOnKeydown, true)
2102
- return () => {
2103
- unsubscribeLayout()
1395
+ return () => {unsubscribeLayout()
2104
1396
  document.removeEventListener('input', layoutOnInput, true)
2105
1397
  document.removeEventListener('keydown', layoutOnKeydown, true)
2106
1398
  layoutDismiss()
@@ -2108,125 +1400,61 @@ layoutBadgeHide()
2108
1400
  }
2109
1401
  }, 'dsh-russian-lang: layout')
2110
1402
  if (!ctx.slots || !React) return
2111
- const toggleRu = (wantRu) => {
2112
- try {
2113
- if (runtime.getLocale().active === wantRu) return
1403
+ const toggleRu = (wantRu) => {try {if (runtime.getLocale().active === wantRu) return
2114
1404
  runtime.setLocale(wantRu ? 'ru' : 'en')
2115
1405
  } catch (err) { console.warn('dsh-russian-lang: toggle failed', err) }
2116
1406
  }
2117
- try {
2118
- ctx.slots.inject('plugins.item', () =>
2119
- ctx.slots.register({
2120
- name: 'plugins.item',
2121
- id: SETTINGS_NS_NAME,
2122
- order: 60,
2123
- label: () => 'Russian language',
2124
- locale: SETTINGS_NS_NAME,
2125
- inject: () => ({ scope, runtime, toggleRu }),
2126
- }, SettingsCard),
2127
- )
1407
+ try {ctx.slots.inject('plugins.item', () =>
1408
+ ctx.slots.register({name: 'plugins.item',id: SETTINGS_NS_NAME,order: 60,label: () => 'Russian language',locale: SETTINGS_NS_NAME,inject: () => ({ scope, runtime, toggleRu }),}, SettingsCard),)
2128
1409
  ctx.slots.inject('plugins.row.config', () =>
2129
- ctx.slots.register({
2130
- name: 'plugins.row.config',
2131
- key: ROW_CONFIG_KEY,
2132
- locale: SETTINGS_NS_NAME,
2133
- inject: () => ({ scope, runtime, toggleRu }),
2134
- }, SettingsCard),
2135
- )
1410
+ ctx.slots.register({name: 'plugins.row.config',key: ROW_CONFIG_KEY,locale: SETTINGS_NS_NAME,inject: () => ({ scope, runtime, toggleRu }),}, SettingsCard),)
2136
1411
  ctx.slots.inject('settings.plugin.item', () =>
2137
- ctx.slots.register({
2138
- name: 'settings.plugin.item',
2139
- key: SETTINGS_NS_NAME,
2140
- locale: SETTINGS_NS_NAME,
2141
- inject: () => ({ scope, runtime, toggleRu }),
2142
- }, SettingsCard),
2143
- )
1412
+ ctx.slots.register({name: 'settings.plugin.item',key: SETTINGS_NS_NAME,locale: SETTINGS_NS_NAME,inject: () => ({ scope, runtime, toggleRu }),}, SettingsCard),)
2144
1413
  } catch (err) { console.warn('dsh-russian-lang: settings slot unavailable', err) }
2145
- try {
2146
- ctx.slots.inject('conversation.session.header.utilities', () =>
2147
- ctx.slots.register({
2148
- name: 'conversation.session.header.utilities',
2149
- id: 'dsh-russian-lang-quick-switch',
2150
- order: 100,
2151
- locale: SETTINGS_NS_NAME,
2152
- inject: () => ({ runtime, toggleRu }),
2153
- }, QuickLangSwitch),
2154
- )
1414
+ try {ctx.slots.inject('conversation.session.header.utilities', () =>
1415
+ ctx.slots.register({name: 'conversation.session.header.utilities',id: 'dsh-russian-lang-quick-switch',order: 100,locale: SETTINGS_NS_NAME,inject: () => ({ runtime, toggleRu }),}, QuickLangSwitch),)
2155
1416
  } catch (err) { }
2156
- try {
2157
- ctx.slots.inject('conversation.chat.assistant-actions', () =>
2158
- ctx.slots.register({
2159
- name: 'conversation.chat.assistant-actions',
2160
- id: 'dsh-russian-lang-translate-action',
2161
- order: 50,
2162
- locale: SETTINGS_NS_NAME,
2163
- inject: () => ({ runtime }),
2164
- }, TranslateTurnAction),
2165
- )
1417
+ try {ctx.slots.inject('conversation.chat.assistant-actions', () =>
1418
+ ctx.slots.register({name: 'conversation.chat.assistant-actions',id: 'dsh-russian-lang-translate-action',order: 50,locale: SETTINGS_NS_NAME,inject: () => ({ runtime }),}, TranslateTurnAction),)
2166
1419
  } catch (err) { }
2167
- try {
2168
- ctx.slots.inject('conversation.session.header.utilities', () =>
2169
- ctx.slots.register({
2170
- name: 'conversation.session.header.utilities',
2171
- id: 'dsh-russian-lang-md-export',
2172
- order: 101,
2173
- locale: SETTINGS_NS_NAME,
2174
- inject: () => ({ runtime }),
2175
- }, ExportMarkdownButton),
2176
- )
1420
+ try {ctx.slots.inject('conversation.session.header.utilities', () =>
1421
+ ctx.slots.register({name: 'conversation.session.header.utilities',id: 'dsh-russian-lang-md-export',order: 101,locale: SETTINGS_NS_NAME,inject: () => ({ runtime }),}, ExportMarkdownButton),)
2177
1422
  } catch (err) { }
2178
1423
  }
2179
- function QuickLangSwitch(props) {
2180
- const inj = typeof props.inject === 'function' ? (props.inject() || {}) : (props.inject || {})
1424
+ function QuickLangSwitch(props) {const inj = typeof props.inject === 'function' ? (props.inject() || {}) : (props.inject || {})
2181
1425
  const runtime = props.runtime || inj.runtime
2182
1426
  const toggleRu = props.toggleRu || inj.toggleRu
2183
1427
  const [locale, setLocaleState] = React.useState(runtime ? (runtime.getLocale().active || 'en') : 'ru')
2184
- React.useEffect(() => {
2185
- if (!runtime) return
2186
- return runtime.subscribe(() => {
2187
- try { setLocaleState(runtime.getLocale().active || 'en') } catch (e) { }
1428
+ React.useEffect(() => {if (!runtime) return
1429
+ return runtime.subscribe(() => {try { setLocaleState(runtime.getLocale().active || 'en') } catch (e) { }
2188
1430
  })
2189
1431
  }, [runtime])
2190
1432
  const isRu = locale === 'ru'
2191
- return React.createElement('button', {
2192
- type: 'button',
2193
- className: 'rl-lang-chip' + (isRu ? ' rl-lang-chip-active' : ''),
2194
- title: isRu ? 'Интерфейс: Русский (нажмите для переключения на EN)' : 'Interface: English (click for RU)',
2195
- onClick: () => { if (toggleRu) toggleRu(!isRu) },
2196
- }, React.createElement('span', { className: 'rl-lang-text' }, isRu ? 'RU' : 'EN'))
2197
- }
2198
- function ExportMarkdownButton(props) {
2199
- const [done, setDone] = React.useState(false)
2200
- const onExport = () => {
2201
- try {
2202
- const titleEl = document.querySelector('.dsw-session-title, [data-session-title], header h1, header h2, [class*="title"]')
1433
+ 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'))
1434
+ }
1435
+ function ExportMarkdownButton(props) {const [done, setDone] = React.useState(false)
1436
+ const onExport = () => {try {const titleEl = document.querySelector('.dsw-session-title, [data-session-title], header h1, header h2, [class*="title"]')
2203
1437
  const rawTitle = (titleEl && titleEl.textContent.trim()) || document.title || 'Диалог DSH'
2204
1438
  const title = rawTitle.replace(/\s*—\s*DeepSeek Harness\s*$/, '').trim() || 'Диалог DSH'
2205
1439
  const flow = document.querySelector('[data-chat-flow]') || document.querySelector('[data-chat-flow-scroll]') || document.body
2206
1440
  const flowItems = Array.from(flow.querySelectorAll('[data-chat-flow-kind]'))
2207
1441
  const messages = []
2208
- if (flowItems.length > 0) {
2209
- flowItems.forEach((node) => {
2210
- const kind = node.getAttribute('data-chat-flow-kind')
2211
- if (kind === 'user' || kind === 'steering') {
2212
- const bubble = node.querySelector('[class*="bubble"]') || node
1442
+ if (flowItems.length > 0) {flowItems.forEach((node) => {const kind = node.getAttribute('data-chat-flow-kind')
1443
+ if (kind === 'user' || kind === 'steering') {const bubble = node.querySelector('[class*="bubble"]') || node
2213
1444
  const clone = bubble.cloneNode(true)
2214
1445
  clone.querySelectorAll('button, svg, [class*="actions"], [class*="Actions"]').forEach((b) => b.remove())
2215
1446
  const text = clone.innerText.trim()
2216
1447
  if (text) messages.push({ role: 'user', content: text })
2217
- } else if (kind === 'assistant-step') {
2218
- const clone = node.cloneNode(true)
1448
+ } else if (kind === 'assistant-step') {const clone = node.cloneNode(true)
2219
1449
  clone.querySelectorAll('button, svg, [class*="actions"], [class*="Actions"], .rl-turn-translation').forEach((b) => b.remove())
2220
1450
  const text = clone.innerText.trim()
2221
1451
  if (text) messages.push({ role: 'assistant', content: text })
2222
1452
  }
2223
1453
  })
2224
1454
  }
2225
- if (messages.length === 0) {
2226
- const allElements = Array.from(document.querySelectorAll('[class*="userRow"], [class*="UserRow"], [class*="assistant-step"], [class*="AssistantMarkdown"], .dsw-turn-node, [data-role]'))
1455
+ if (messages.length === 0) {const allElements = Array.from(document.querySelectorAll('[class*="userRow"], [class*="UserRow"], [class*="assistant-step"], [class*="AssistantMarkdown"], .dsw-turn-node, [data-role]'))
2227
1456
  const seen = new Set()
2228
- allElements.forEach((node) => {
2229
- const isUser = node.matches('[class*="userRow"], [class*="UserRow"], [data-role="user"]') || !!node.querySelector('[data-role="user"]')
1457
+ allElements.forEach((node) => {const isUser = node.matches('[class*="userRow"], [class*="UserRow"], [data-role="user"]') || !!node.querySelector('[data-role="user"]')
2230
1458
  const role = isUser ? 'user' : 'assistant'
2231
1459
  const clone = node.cloneNode(true)
2232
1460
  clone.querySelectorAll('button, svg, [class*="actions"], [class*="Actions"], .rl-turn-translation, [data-turn-tail]').forEach((b) => b.remove())
@@ -2236,10 +1464,7 @@ seen.add(text)
2236
1464
  messages.push({ role, content: text })
2237
1465
  })
2238
1466
  }
2239
- const session = {
2240
- title,
2241
- createdAt: new Date().toISOString(),
2242
- messages
1467
+ const session = {title,createdAt: new Date().toISOString(),messages
2243
1468
  }
2244
1469
  const md = exportSessionToMarkdown(session)
2245
1470
  const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' })
@@ -2252,175 +1477,65 @@ a.download = safeTitle + '-' + new Date().toISOString().slice(0, 10) + '.md'
2252
1477
  document.body.appendChild(a)
2253
1478
  a.click()
2254
1479
  setDone(true)
2255
- setTimeout(() => {
2256
- try { document.body.removeChild(a); URL.revokeObjectURL(url) } catch (e) { void e; }
1480
+ setTimeout(() => {try { document.body.removeChild(a); URL.revokeObjectURL(url) } catch (e) { void e; }
2257
1481
  }, 30000)
2258
1482
  setTimeout(() => setDone(false), 2000)
2259
- } catch (err) {
2260
- console.warn('dsh-russian-lang: export md failed', err)
2261
- }
2262
- }
2263
- return React.createElement('button', {
2264
- type: 'button',
2265
- className: 'rl-lang-chip rl-export-md-btn',
2266
- title: 'Экспорт диалога в Markdown (.md)',
2267
- onClick: onExport,
2268
- }, React.createElement('span', { className: 'rl-lang-text' }, done ? '✓ MD' : '📥 MD'))
2269
- }
2270
- async function translateTurnContent(text) {
2271
- if (!text || typeof text !== 'string') return { error: 'Текст для перевода пуст.' }
2272
- try {
2273
- const res = await fetch('/api/dsh-russian-lang/translate', {
2274
- method: 'POST',
2275
- headers: { 'content-type': 'application/json' },
2276
- body: JSON.stringify({ text })
1483
+ } catch (err) {console.warn('dsh-russian-lang: export md failed', err)
1484
+ }
1485
+ }
1486
+ 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'))
1487
+ }
1488
+ async function translateTurnContent(text) {if (!text || typeof text !== 'string') return { error: 'Текст для перевода пуст.' }
1489
+ 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 })
2277
1490
  })
2278
1491
  const data = await res.json().catch(() => ({}))
2279
- if (!res.ok) {
2280
- return { error: data.error || ('Ошибка перевода (HTTP ' + res.status + ')') }
1492
+ if (!res.ok) {return { error: data.error || ('Ошибка перевода (HTTP ' + res.status + ')') }
2281
1493
  }
2282
1494
  return { translatedText: data.translatedText || text }
2283
- } catch (err) {
2284
- return { error: 'Не удалось связаться с хостом DSH: ' + (err.message || err) }
1495
+ } catch (err) {return { error: 'Не удалось связаться с хостом DSH: ' + (err.message || err) }
2285
1496
  }
2286
1497
  }
2287
- function TranslateTurnAction(props) {
2288
- const t = typeof props.t === 'function' ? props.t : ((k) => k)
1498
+ function TranslateTurnAction(props) {const t = typeof props.t === 'function' ? props.t : ((k) => k)
2289
1499
  const [loading, setLoading] = React.useState(false)
2290
1500
  const [open, setOpen] = React.useState(false)
2291
- const [upStatus, setUpStatus] = React.useState({
2292
- currentVersion: '0.3.7',
2293
- latestVersion: undefined,
2294
- updateAvailable: false
2295
- })
2296
- const [upLoading, setUpLoading] = React.useState(false)
2297
- const [upMsg, setUpMsg] = React.useState(null)
2298
- const checkUpdate = () => {
2299
- setUpLoading(true)
2300
- setUpMsg(null)
2301
- fetch('/api/dsh-russian-lang/update')
2302
- .then((r) => r.json())
2303
- .then((data) => {
2304
- setUpStatus(data)
2305
- setUpLoading(false)
2306
- })
2307
- .catch(() => {
2308
- setUpLoading(false)
2309
- setUpMsg({ type: 'err', text: t('updaterFailed') })
2310
- })
2311
- }
2312
- const triggerUpdate = () => {
2313
- setUpLoading(true)
2314
- setUpMsg(null)
2315
- fetch('/api/dsh-russian-lang/update', {
2316
- method: 'POST',
2317
- headers: { 'x-dsh-plugin-update': '1' }
2318
- })
2319
- .then((r) => r.json())
2320
- .then((data) => {
2321
- setUpLoading(false)
2322
- if (data.restartRequired || data.updatedVersion) {
2323
- setUpStatus(data)
2324
- setUpMsg({ type: 'ok', text: t('updaterSuccess') })
2325
- } else if (data.error) {
2326
- setUpMsg({ type: 'err', text: data.error })
2327
- }
2328
- })
2329
- .catch(() => {
2330
- setUpLoading(false)
2331
- setUpMsg({ type: 'err', text: t('updaterFailed') })
2332
- })
2333
- }
2334
- const [transStatus, setTransStatus] = React.useState(null)
2335
- const [transStarting, setTransStarting] = React.useState(false)
2336
- const [transMsg, setTransMsg] = React.useState(null)
2337
- const fetchTransStatus = () => {
2338
- fetch('/api/dsh-russian-lang/translator/status')
2339
- .then((r) => r.json())
2340
- .then((data) => setTransStatus(data))
2341
- .catch(() => setTransStatus(null))
2342
- }
2343
- const startLibreTranslate = () => {
2344
- setTransStarting(true)
2345
- setTransMsg(null)
2346
- fetch('/api/dsh-russian-lang/translator/setup', { method: 'POST' })
2347
- .then((r) => r.json())
2348
- .then((data) => {
2349
- setTransStarting(false)
2350
- if (data.ok) {
2351
- setTransMsg({ type: 'ok', text: data.message || 'Контейнер LibreTranslate запущен.' })
2352
- fetchTransStatus()
2353
- } else {
2354
- setTransMsg({ type: 'err', text: data.error || 'Ошибка запуска контейнера.' })
2355
- }
2356
- })
2357
- .catch((err) => {
2358
- setTransStarting(false)
2359
- setTransMsg({ type: 'err', text: 'Ошибка: ' + (err.message || err) })
2360
- })
2361
- }
2362
- React.useEffect(() => {
2363
- if (open) {
2364
- checkUpdate()
2365
- fetchTransStatus()
2366
- }
2367
- }, [open])
2368
- return React.createElement('button', {
2369
- type: 'button',
2370
- className: 'rl-action-btn' + (open ? ' rl-action-btn-active' : ''),
2371
- title: t('translateTurn'),
2372
- disabled: loading,
2373
- onClick: async (ev) => {
2374
- ev.stopPropagation()
1501
+ return React.createElement('button', {type: 'button',className: 'rl-action-btn' + (open ? ' rl-action-btn-active' : ''),title: t('translateTurn'),disabled: loading,onClick: async (ev) => {ev.stopPropagation()
2375
1502
  const btn = ev.currentTarget
2376
1503
  const tailFlowItem = btn.closest('[data-chat-flow-kind="turn-tail"]') || btn.closest('[data-chat-flow-kind]') || btn.closest('[data-turn-tail]')?.closest('[data-chat-flow-kind]') || btn.closest('[data-turn-tail]')
2377
1504
  if (!tailFlowItem) return
2378
1505
  const parentContainer = tailFlowItem.parentElement
2379
1506
  let box = parentContainer ? parentContainer.querySelector('.rl-turn-translation[data-tail-key="' + (tailFlowItem.getAttribute('data-chat-flow-key') || '') + '"]') : null
2380
- if (!box) {
2381
- box = tailFlowItem.querySelector('.rl-turn-translation')
1507
+ if (!box) {box = tailFlowItem.querySelector('.rl-turn-translation')
2382
1508
  }
2383
- if (box) {
2384
- box.style.display = box.style.display === 'none' ? 'block' : 'none'
1509
+ if (box) {box.style.display = box.style.display === 'none' ? 'block' : 'none'
2385
1510
  setOpen(box.style.display !== 'none')
2386
1511
  return
2387
1512
  }
2388
1513
  setLoading(true)
2389
- try {
2390
- const assistantNodes = []
1514
+ try {const assistantNodes = []
2391
1515
  let prev = tailFlowItem.previousElementSibling
2392
- while (prev && prev.getAttribute('data-chat-flow-kind') !== 'user') {
2393
- if (prev.getAttribute('data-chat-flow-kind') === 'assistant-step') {
2394
- assistantNodes.unshift(prev)
1516
+ while (prev && prev.getAttribute('data-chat-flow-kind') !== 'user') {if (prev.getAttribute('data-chat-flow-kind') === 'assistant-step') {assistantNodes.unshift(prev)
2395
1517
  }
2396
1518
  prev = prev.previousElementSibling
2397
1519
  }
2398
1520
  let rawText = ''
2399
- if (assistantNodes.length > 0) {
2400
- rawText = assistantNodes.map((node) => {
2401
- const clone = node.cloneNode(true)
1521
+ if (assistantNodes.length > 0) {rawText = assistantNodes.map((node) => {const clone = node.cloneNode(true)
2402
1522
  clone.querySelectorAll('button, svg, [class*="actions"], [class*="Actions"]').forEach((b) => b.remove())
2403
1523
  return clone.innerText.trim()
2404
1524
  }).filter(Boolean).join('\n\n')
2405
- } else if (parentContainer) {
2406
- const allSteps = Array.from(parentContainer.querySelectorAll('[data-chat-flow-kind="assistant-step"]'))
2407
- if (allSteps.length > 0) {
2408
- const beforeTail = allSteps.filter((s) => (s.compareDocumentPosition(tailFlowItem) & Node.DOCUMENT_POSITION_FOLLOWING))
1525
+ } else if (parentContainer) {const allSteps = Array.from(parentContainer.querySelectorAll('[data-chat-flow-kind="assistant-step"]'))
1526
+ if (allSteps.length > 0) {const beforeTail = allSteps.filter((s) => (s.compareDocumentPosition(tailFlowItem) & Node.DOCUMENT_POSITION_FOLLOWING))
2409
1527
  const targetSteps = beforeTail.length > 0 ? [beforeTail[beforeTail.length - 1]] : [allSteps[allSteps.length - 1]]
2410
- rawText = targetSteps.map((s) => {
2411
- const clone = s.cloneNode(true)
1528
+ rawText = targetSteps.map((s) => {const clone = s.cloneNode(true)
2412
1529
  clone.querySelectorAll('button, svg, [class*="actions"], [class*="Actions"]').forEach((b) => b.remove())
2413
1530
  return clone.innerText.trim()
2414
1531
  }).filter(Boolean).join('\n\n')
2415
1532
  }
2416
1533
  }
2417
- if (!rawText) {
2418
- const prose = tailFlowItem.querySelector('.dsw-prose, [data-block-kind="text"], .dsw-markdown-view, p')
1534
+ if (!rawText) {const prose = tailFlowItem.querySelector('.dsw-prose, [data-block-kind="text"], .dsw-markdown-view, p')
2419
1535
  if (prose) rawText = prose.innerText.trim()
2420
1536
  }
2421
1537
  let transResult = { error: 'Не удалось обнаружить текст сообщения ассистента для перевода.' }
2422
- if (rawText) {
2423
- transResult = await translateTurnContent(rawText)
1538
+ if (rawText) {transResult = await translateTurnContent(rawText)
2424
1539
  }
2425
1540
  const isErr = !!transResult.error
2426
1541
  const displayText = transResult.translatedText || transResult.error || ''
@@ -2438,36 +1553,27 @@ box.innerHTML = '<div class="rl-trans-head">' +
2438
1553
  '<div class="rl-trans-body"' + (isErr ? ' style="color: var(--dsw-alias-state-warning-primary); font-size: 13px;"' : '') + '></div>'
2439
1554
  box.querySelector('.rl-trans-body').textContent = displayText
2440
1555
  const copyBtn = box.querySelector('.rl-btn-copy')
2441
- if (copyBtn) {
2442
- copyBtn.addEventListener('click', (e) => {
2443
- e.stopPropagation()
2444
- try { navigator.clipboard.writeText(displayText) } catch (err) { void e; }
1556
+ if (copyBtn) {copyBtn.addEventListener('click', (e) => {e.stopPropagation()
1557
+ try { navigator.clipboard.writeText(displayText) } catch (err) { void err; }
2445
1558
  copyBtn.textContent = '✓ Скопировано'
2446
1559
  setTimeout(() => { copyBtn.textContent = '📋 Копировать' }, 2000)
2447
1560
  })
2448
1561
  }
2449
1562
  const closeBtn = box.querySelector('.rl-btn-close')
2450
- closeBtn.addEventListener('click', (e) => {
2451
- e.stopPropagation()
1563
+ closeBtn.addEventListener('click', (e) => {e.stopPropagation()
2452
1564
  box.style.display = 'none'
2453
1565
  setOpen(false)
2454
1566
  })
2455
- if (parentContainer) {
2456
- parentContainer.insertBefore(box, tailFlowItem)
2457
- } else {
2458
- tailFlowItem.appendChild(box)
1567
+ if (parentContainer) {parentContainer.insertBefore(box, tailFlowItem)
1568
+ } else {tailFlowItem.appendChild(box)
2459
1569
  }
2460
1570
  setOpen(true)
2461
- } catch (err) {
2462
- console.warn('dsh-russian-lang: translate turn failed', err)
2463
- } finally {
2464
- setLoading(false)
1571
+ } catch (err) {console.warn('dsh-russian-lang: translate turn failed', err)
1572
+ } finally {setLoading(false)
2465
1573
  }
2466
- },
2467
- }, React.createElement('span', null, loading ? '...' : (open ? 'RU ✓' : 'RU ↗')))
1574
+ },}, React.createElement('span', null, loading ? '...' : (open ? 'RU ✓' : 'RU ↗')))
2468
1575
  }
2469
- function SettingsCard(props) {
2470
- const inj = typeof props.inject === 'function'
1576
+ function SettingsCard(props) {const inj = typeof props.inject === 'function'
2471
1577
  ? (props.inject() || {})
2472
1578
  : (props.inject || {})
2473
1579
  const scope = props.scope || inj.scope
@@ -2481,20 +1587,73 @@ const [ruActive, setRuActive] = React.useState(
2481
1587
  () => { try { return runtime.getLocale().active === 'ru' } catch (e) { return false } })
2482
1588
  const [typo, setTypoState] = React.useState(() =>
2483
1589
  (snap.value && snap.value.typography) || {})
2484
- React.useEffect(() => {
2485
- if (!scope || !scope.subscribe) return undefined
2486
- const un = scope.subscribe(() => {
2487
- const s = scope.getSnapshot()
1590
+ const [upStatus, setUpStatus] = React.useState({currentVersion: '0.3.9',latestVersion: undefined,updateAvailable: false
1591
+ })
1592
+ const [upLoading, setUpLoading] = React.useState(false)
1593
+ const [upMsg, setUpMsg] = React.useState(null)
1594
+ const checkUpdate = () => {setUpLoading(true)
1595
+ setUpMsg(null)
1596
+ fetch('/api/dsh-russian-lang/update', {headers: { 'x-dsh-plugin-update': '1' }
1597
+ })
1598
+ .then((r) => r.json())
1599
+ .then((data) => {setUpStatus(data)
1600
+ setUpLoading(false)
1601
+ })
1602
+ .catch(() => {setUpLoading(false)
1603
+ setUpMsg({ type: 'err', text: t('updaterFailed') })
1604
+ })
1605
+ }
1606
+ const triggerUpdate = () => {setUpLoading(true)
1607
+ setUpMsg(null)
1608
+ fetch('/api/dsh-russian-lang/update', {method: 'POST',headers: { 'x-dsh-plugin-update': '1' }
1609
+ })
1610
+ .then((r) => r.json())
1611
+ .then((data) => {setUpLoading(false)
1612
+ if (data.restartRequired || data.updatedVersion) {setUpStatus(data)
1613
+ setUpMsg({ type: 'ok', text: t('updaterSuccess') })
1614
+ } else if (data.error) {setUpMsg({ type: 'err', text: data.error })
1615
+ }
1616
+ })
1617
+ .catch(() => {setUpLoading(false)
1618
+ setUpMsg({ type: 'err', text: t('updaterFailed') })
1619
+ })
1620
+ }
1621
+ const [transStatus, setTransStatus] = React.useState(null)
1622
+ const [transStarting, setTransStarting] = React.useState(false)
1623
+ const [transMsg, setTransMsg] = React.useState(null)
1624
+ const fetchTransStatus = () => {fetch('/api/dsh-russian-lang/translator/status')
1625
+ .then((r) => r.json())
1626
+ .then((data) => setTransStatus(data))
1627
+ .catch(() => setTransStatus(null))
1628
+ }
1629
+ const startLibreTranslate = () => {setTransStarting(true)
1630
+ setTransMsg(null)
1631
+ fetch('/api/dsh-russian-lang/translator/setup', {method: 'POST',headers: { 'x-dsh-translator': '1' }
1632
+ })
1633
+ .then((r) => r.json())
1634
+ .then((data) => {setTransStarting(false)
1635
+ if (data.ok) {setTransMsg({ type: 'ok', text: data.message || 'Контейнер LibreTranslate запущен.' })
1636
+ fetchTransStatus()
1637
+ } else {setTransMsg({ type: 'err', text: data.error || 'Ошибка запуска контейнера.' })
1638
+ }
1639
+ })
1640
+ .catch((err) => {setTransStarting(false)
1641
+ setTransMsg({ type: 'err', text: 'Ошибка: ' + (err.message || err) })
1642
+ })
1643
+ }
1644
+ React.useEffect(() => {if (open || (props && props.view === 'page')) {checkUpdate()
1645
+ fetchTransStatus()
1646
+ }
1647
+ }, [open, props])
1648
+ React.useEffect(() => {if (!scope || !scope.subscribe) return undefined
1649
+ const un = scope.subscribe(() => {const s = scope.getSnapshot()
2488
1650
  setSnap(s)
2489
1651
  if (s.value && s.value.typography) setTypoState(s.value.typography)
2490
1652
  })
2491
1653
  setSnap(scope.getSnapshot())
2492
1654
  return un
2493
1655
  }, [scope])
2494
- React.useEffect(() => {
2495
- try {
2496
- const un = runtime.subscribe(() => {
2497
- try { setRuActive(runtime.getLocale().active === 'ru') } catch (e) { }
1656
+ React.useEffect(() => {try {const un = runtime.subscribe(() => {try { setRuActive(runtime.getLocale().active === 'ru') } catch (e) { }
2498
1657
  })
2499
1658
  return un
2500
1659
  } catch (e) { return undefined }
@@ -2506,428 +1665,146 @@ const overrides = value.overrides || {}
2506
1665
  const overridesCount = Object.keys(overrides).length
2507
1666
  const [newKey, setNewKey] = React.useState('')
2508
1667
  const [newVal, setNewVal] = React.useState('')
2509
- const setTypo = (patch) => {
2510
- const next = Object.assign({}, typo, patch)
1668
+ const setTypo = (patch) => {const next = Object.assign({}, typo, patch)
2511
1669
  setTypoState(next)
2512
- try {
2513
- const r = scope.set('typography', next)
2514
- if (r && typeof r.catch === 'function') r.catch((err) => {
2515
- console.warn('dsh-russian-lang: scope.set typography failed', err && err.message || err)
1670
+ try {const r = scope.set('typography', next)
1671
+ if (r && typeof r.catch === 'function') r.catch((err) => {console.warn('dsh-russian-lang: scope.set typography failed', err && err.message || err)
2516
1672
  })
2517
- } catch (err) {
2518
- console.warn('dsh-russian-lang: scope.set typography sync threw', err && err.message || err)
1673
+ } catch (err) {console.warn('dsh-russian-lang: scope.set typography sync threw', err && err.message || err)
2519
1674
  }
2520
1675
  }
2521
1676
  const onEnabled = (ev) => { if (toggleRu) toggleRu(ev.target.checked) }
2522
1677
  const checkbox = (checked, onChange, disabled) =>
2523
- React.createElement('input', {
2524
- type: 'checkbox', checked: !!checked, disabled: !!disabled,
2525
- className: 'rl-check', onChange: (ev) => onChange(ev),
2526
- })
1678
+ React.createElement('input', {type: 'checkbox', checked: !!checked, disabled: !!disabled,className: 'rl-check', onChange: (ev) => onChange(ev),})
2527
1679
  const disabled = status !== 'ready'
2528
1680
  const statusLine = status === 'ready'
2529
1681
  ? ''
2530
1682
  : (status === 'unavailable' ? t('statusUnavailable') : t('statusLoading'))
2531
1683
  let ChevronIcon = null
2532
- try {
2533
- const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
1684
+ try {const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
2534
1685
  ChevronIcon = primitives && primitives.IconChevronDownOutline14
2535
1686
  } catch (_) { ChevronIcon = null }
2536
- const FallbackChevron = () => React.createElement('svg', {
2537
- width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none',
2538
- 'aria-hidden': 'true',
2539
- }, React.createElement('path', {
2540
- d: 'M3.5 5.25 7 8.75l3.5-3.5', stroke: 'currentColor',
2541
- strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round',
2542
- }))
1687
+ const FallbackChevron = () => React.createElement('svg', {width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none','aria-hidden': 'true',}, React.createElement('path', {d: 'M3.5 5.25 7 8.75l3.5-3.5', stroke: 'currentColor',strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round',}))
2543
1688
  const Chevron = ChevronIcon || FallbackChevron
2544
1689
  const presetKey = value.agentPromptPreset || 'technical_expert'
2545
1690
  const presetInfo = typeof SYSTEM_PROMPT_PRESETS !== 'undefined' ? SYSTEM_PROMPT_PRESETS[presetKey] : null
2546
- if (props && props.view === 'summary') {
2547
- return React.createElement('div', { className: 'rl-sub' }, statusLine || t('cardSub'))
1691
+ if (props && props.view === 'summary') {return React.createElement('div', { className: 'rl-sub' }, statusLine || t('cardSub'))
2548
1692
  }
2549
1693
  const page = !!(props && props.view === 'page')
2550
- return React.createElement('div', { className: page ? 'rl-page-seat' : 'rl-card' },
2551
- React.createElement('button', {
2552
- type: 'button',
2553
- className: 'rl-head',
2554
- style: page ? { display: 'none' } : undefined,
2555
- 'aria-expanded': page ? 'true' : String(open),
2556
- onClick: () => setOpen(!open),
2557
- },
2558
- React.createElement('span', { className: 'rl-head-main' },
2559
- React.createElement('div', { className: 'rl-title' },
2560
- '🇷🇺 ' + t('cardTitle'),
2561
- React.createElement('span', { className: 'rl-badge ' + (ruActive ? 'rl-badge-ok' : 'rl-badge-warn') },
2562
- ruActive ? t('badgeRu') : t('badgeEn')),
2563
- React.createElement('span', { className: 'rl-badge rl-badge-ok' }, t('badgeCoverage'))
2564
- ),
2565
- React.createElement('div', { className: 'rl-sub' },
2566
- statusLine || t('cardSub'))),
2567
- React.createElement('span', {
2568
- className: 'rl-chev' + (open ? ' rl-chev-open' : ''),
2569
- }, React.createElement(Chevron, null))),
2570
- (page || open) && React.createElement('div', { className: 'rl-body' },
2571
- React.createElement('div', { className: 'rl-page' },
2572
- React.createElement('div', { className: 'rl-section-card' },
2573
- React.createElement('div', { className: 'rl-section-title' },
2574
- React.createElement('span', null, t('secLanguage')),
2575
- React.createElement('span', { className: 'rl-badge ' + (ruActive ? 'rl-badge-ok' : 'rl-badge-dim') },
2576
- ruActive ? t('badgeRu') : t('badgeEn'))
2577
- ),
2578
- React.createElement('div', { className: 'rl-section-desc' }, t('secLanguageDesc')),
2579
- React.createElement('div', { className: 'rl-item-card' },
2580
- React.createElement('div', { className: 'rl-item-head' },
2581
- React.createElement('label', { className: 'rl-item-label' },
2582
- checkbox(ruActive, onEnabled, disabled),
2583
- t('enabled')
1694
+ 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'))
1695
+ ),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'))
1696
+ ),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')
2584
1697
  )
2585
- ),
2586
- React.createElement('div', { className: 'rl-item-desc' }, t('enabledDesc'))
2587
- ),
2588
- React.createElement('div', { className: 'rl-hint-text' }, t('quickSwitchNote')),
2589
- overridesCount > 0 ? React.createElement('div', { className: 'rl-badge rl-badge-dim', style: { alignSelf: 'flex-start' } },
2590
- t('overridesCount') + ': ' + overridesCount) : null
2591
- ),
2592
- React.createElement('div', { className: 'rl-section-card' },
2593
- React.createElement('div', { className: 'rl-section-title' },
2594
- React.createElement('span', null, t('secTypography')),
2595
- React.createElement('span', { className: 'rl-badge rl-badge-ok' }, t('badgeSmartUx'))
2596
- ),
2597
- React.createElement('div', { className: 'rl-section-desc' }, t('secTypographyDesc')),
2598
- React.createElement('div', { className: 'rl-grid-2' },
2599
- React.createElement('div', { className: 'rl-item-card' },
2600
- React.createElement('div', { className: 'rl-item-head' },
2601
- React.createElement('label', { className: 'rl-item-label' },
2602
- checkbox(typography.enabled !== false && ruActive,
2603
- (ev) => setTypo({ enabled: ev.target.checked }), !ruActive || disabled),
2604
- t('typography')
1698
+ ),React.createElement('div', { className: 'rl-item-desc' }, t('enabledDesc'))
1699
+ ),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
1700
+ ),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'))
1701
+ ),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')
2605
1702
  )
2606
- ),
2607
- React.createElement('div', { className: 'rl-item-desc' }, t('typographyDesc'))
2608
- ),
2609
- React.createElement('div', { className: 'rl-item-card' },
2610
- React.createElement('div', { className: 'rl-item-head' },
2611
- React.createElement('label', { className: 'rl-item-label' },
2612
- checkbox(typography.liveInput !== false && ruActive,
2613
- (ev) => setTypo({ liveInput: ev.target.checked }), !ruActive || disabled),
2614
- t('liveInput')
1703
+ ),React.createElement('div', { className: 'rl-item-desc' }, t('typographyDesc'))
1704
+ ),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')
2615
1705
  )
2616
- ),
2617
- React.createElement('div', { className: 'rl-item-desc' }, t('liveInputDesc'))
2618
- ),
2619
- React.createElement('div', { className: 'rl-item-card' },
2620
- React.createElement('div', { className: 'rl-item-head' },
2621
- React.createElement('label', { className: 'rl-item-label' },
2622
- checkbox(typography.yo === true && ruActive,
2623
- (ev) => setTypo({ yo: ev.target.checked }), !ruActive || disabled),
2624
- t('yo')
1706
+ ),React.createElement('div', { className: 'rl-item-desc' }, t('liveInputDesc'))
1707
+ ),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')
2625
1708
  )
2626
- ),
2627
- React.createElement('div', { className: 'rl-item-desc' }, t('yoDesc'))
2628
- ),
2629
- React.createElement('div', { className: 'rl-item-card' },
2630
- React.createElement('div', { className: 'rl-item-head' },
2631
- React.createElement('label', { className: 'rl-item-label' },
2632
- checkbox(value.slashAliases !== false && ruActive,
2633
- (ev) => {
2634
- try { scope.set('slashAliases', ev.target.checked) } catch (err) { void e; }
2635
- }, !ruActive || disabled),
2636
- t('slashAliases')
1709
+ ),React.createElement('div', { className: 'rl-item-desc' }, t('yoDesc'))
1710
+ ),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; }
1711
+ }, !ruActive || disabled),t('slashAliases')
2637
1712
  )
2638
- ),
2639
- React.createElement('div', { className: 'rl-item-desc' }, t('slashAliasesDesc'))
1713
+ ),React.createElement('div', { className: 'rl-item-desc' }, t('slashAliasesDesc'))
2640
1714
  )
2641
- ),
2642
- React.createElement('div', { className: 'rl-hotkey-box' },
2643
- React.createElement('span', { className: 'rl-hotkey-tag' }, '⌨️ Alt+L'),
2644
- React.createElement('span', null, t('altLHintText'))
1715
+ ),React.createElement('div', { className: 'rl-hotkey-box' },React.createElement('span', { className: 'rl-hotkey-tag' }, '⌨️ Alt+L'),React.createElement('span', null, t('altLHintText'))
2645
1716
  )
2646
- ),
2647
- React.createElement('div', { className: 'rl-section-card' },
2648
- React.createElement('div', { className: 'rl-section-title' },
2649
- React.createElement('span', null, t('secAgentPrompt')),
2650
- React.createElement('span', { className: 'rl-badge ' + (value.agentPrompt ? 'rl-badge-ok' : 'rl-badge-dim') },
2651
- value.agentPrompt ? 'Активен' : 'Выключен')
2652
- ),
2653
- React.createElement('div', { className: 'rl-section-desc' }, t('secAgentPromptDesc')),
2654
- React.createElement('div', { className: 'rl-item-card' },
2655
- React.createElement('div', { className: 'rl-item-head' },
2656
- React.createElement('label', { className: 'rl-item-label' },
2657
- checkbox(value.agentPrompt === true,
2658
- (ev) => {
2659
- try {
2660
- const r = scope.set('agentPrompt', ev.target.checked)
2661
- if (r && typeof r.catch === 'function') r.catch((err) => {
2662
- console.warn('dsh-russian-lang: scope.set agentPrompt failed', err && err.message || err)
1717
+ ),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 ? 'Активен' : 'Выключен')
1718
+ ),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)
1719
+ if (r && typeof r.catch === 'function') r.catch((err) => {console.warn('dsh-russian-lang: scope.set agentPrompt failed', err && err.message || err)
2663
1720
  })
2664
- } catch (err) {
2665
- console.warn('dsh-russian-lang: scope.set agentPrompt sync threw', err && err.message || err)
1721
+ } catch (err) {console.warn('dsh-russian-lang: scope.set agentPrompt sync threw', err && err.message || err)
2666
1722
  }
2667
- }, false),
2668
- t('agentPrompt')
1723
+ }, false),t('agentPrompt')
2669
1724
  )
2670
- ),
2671
- React.createElement('div', { className: 'rl-item-desc' }, t('agentPromptDesc'))
2672
- ),
2673
- value.agentPrompt ? React.createElement(React.Fragment, null,
2674
- React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
2675
- React.createElement('label', { className: 'rl-item-label', style: { fontWeight: 500 } }, t('agentPromptPreset')),
2676
- React.createElement('select', {
2677
- className: 'rl-select',
2678
- value: value.agentPromptPreset || 'technical_expert',
2679
- onChange: (ev) => {
2680
- try {
2681
- const val = ev.target.value
1725
+ ),React.createElement('div', { className: 'rl-item-desc' }, t('agentPromptDesc'))
1726
+ ),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
2682
1727
  scope.set('agentPromptPreset', val)
2683
1728
  } catch (err) { console.warn('dsh-russian-lang: set agentPromptPreset failed', err) }
2684
1729
  }
2685
- },
2686
- React.createElement('option', { value: 'technical_expert' }, t('presetExpert')),
2687
- React.createElement('option', { value: 'tech_writer' }, t('presetWriter')),
2688
- React.createElement('option', { value: 'concise' }, t('presetConcise')),
2689
- React.createElement('option', { value: 'code_reviewer' }, t('presetReviewer')),
2690
- React.createElement('option', { value: 'architect' }, t('presetArchitect')),
2691
- React.createElement('option', { value: 'tutor' }, t('presetTutor'))
1730
+ },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'))
2692
1731
  )
2693
- ),
2694
- presetInfo ? React.createElement('div', { className: 'rl-preview-box' },
2695
- React.createElement('div', { style: { fontWeight: 600, marginBottom: '4px', color: 'var(--dsw-alias-label-primary)' } }, '💬 ' + presetInfo.label + ':'),
2696
- presetInfo.text
1732
+ ),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
2697
1733
  ) : null
2698
1734
  ) : null
2699
- ),
2700
- React.createElement('div', { className: 'rl-section-card' },
2701
- React.createElement('div', { className: 'rl-section-title' },
2702
- React.createElement('span', null, t('secUpdater')),
2703
- React.createElement('span', { className: 'rl-badge ' + (upStatus.updateAvailable ? 'rl-badge-warn' : 'rl-badge-ok') },
2704
- upStatus.updateAvailable ? t('badgeUpdateAvailable') : t('badgeUpToDate'))
2705
- ),
2706
- React.createElement('div', { className: 'rl-section-desc' }, t('secUpdaterDesc')),
2707
- React.createElement('div', { className: 'rl-item-card' },
2708
- React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
2709
- React.createElement('div', null,
2710
- React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
2711
- t('updaterCurrent').replace('{version}', upStatus.currentVersion || '0.3.7')),
2712
- upStatus.updateAvailable
2713
- ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-warning-primary)', marginTop: '2px', fontWeight: 500 } },
2714
- t('updaterLatest').replace('{version}', upStatus.latestVersion || ''))
2715
- : React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', marginTop: '2px' } },
2716
- t('updaterUpToDate'))
2717
- ),
2718
- upStatus.updateAvailable
2719
- ? React.createElement('button', {
2720
- type: 'button',
2721
- className: 'rl-btn rl-btn-primary',
2722
- disabled: upLoading,
2723
- onClick: triggerUpdate,
2724
- }, upLoading ? t('updaterUpdating') : t('updaterBtn').replace('{version}', upStatus.latestVersion || ''))
2725
- : React.createElement('button', {
2726
- type: 'button',
2727
- className: 'rl-btn',
2728
- disabled: upLoading,
2729
- onClick: checkUpdate,
2730
- }, upLoading ? t('updaterChecking') : t('updaterCheckBtn'))
2731
- ),
2732
- upMsg ? React.createElement('div', {
2733
- style: {
2734
- marginTop: '10px', padding: '8px 12px', borderRadius: '8px',
2735
- 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)',
2736
- color: upMsg.type === 'ok' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontSize: '12px', fontWeight: 500
1735
+ ),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'))
1736
+ ),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.9')),upStatus.updateAvailable
1737
+ ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-warning-primary)', marginTop: '2px', fontWeight: 500 } },t('updaterLatest').replace('{version}', upStatus.latestVersion || ''))
1738
+ : React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', marginTop: '2px' } },t('updaterUpToDate'))
1739
+ ),upStatus.updateAvailable
1740
+ ? React.createElement('button', {type: 'button',className: 'rl-btn rl-btn-primary',disabled: upLoading,onClick: triggerUpdate,}, upLoading ? t('updaterUpdating') : t('updaterBtn').replace('{version}', upStatus.latestVersion || ''))
1741
+ : React.createElement('button', {type: 'button',className: 'rl-btn',disabled: upLoading,onClick: checkUpdate,}, upLoading ? t('updaterChecking') : t('updaterCheckBtn'))
1742
+ ),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
2737
1743
  }
2738
1744
  }, upMsg.text) : null
2739
1745
  )
2740
- ),
2741
- React.createElement('div', { className: 'rl-section-card' },
2742
- React.createElement('div', { className: 'rl-section-title' },
2743
- React.createElement('span', null, t('secTranslator')),
2744
- React.createElement('span', {
2745
- className: 'rl-badge ' + (value.translateEngine === 'local' ? 'rl-badge-ok' : (value.translateEngine === 'google' ? 'rl-badge-warn' : 'rl-badge-dim'))
1746
+ ),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'))
2746
1747
  }, value.translateEngine === 'local' ? 'Локально' : (value.translateEngine === 'google' ? 'Google' : 'Выключен'))
2747
- ),
2748
- React.createElement('div', { className: 'rl-section-desc' }, t('secTranslatorDesc')),
2749
- React.createElement('div', { className: 'rl-item-card' },
2750
- React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
2751
- React.createElement('label', { className: 'rl-item-label', style: { fontWeight: 500 } }, t('translateEngine')),
2752
- React.createElement('select', {
2753
- className: 'rl-select',
2754
- value: value.translateEngine || 'off',
2755
- onChange: (ev) => {
2756
- try {
2757
- const val = ev.target.value
1748
+ ),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
2758
1749
  scope.set('translateEngine', val)
2759
1750
  if (val === 'local') fetchTransStatus()
2760
1751
  } catch (err) { console.warn('dsh-russian-lang: set translateEngine failed', err) }
2761
1752
  }
2762
- },
2763
- React.createElement('option', { value: 'off' }, t('engineOff')),
2764
- React.createElement('option', { value: 'local' }, t('engineLocal')),
2765
- React.createElement('option', { value: 'google' }, t('engineGoogle'))
1753
+ },React.createElement('option', { value: 'off' }, t('engineOff')),React.createElement('option', { value: 'local' }, t('engineLocal')),React.createElement('option', { value: 'google' }, t('engineGoogle'))
2766
1754
  )
2767
- ),
2768
- value.translateEngine === 'google' ? React.createElement('div', {
2769
- style: {
2770
- marginTop: '10px', padding: '8px 12px', borderRadius: '8px',
2771
- 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)',
2772
- color: 'var(--dsw-alias-state-warning-primary)', fontSize: '12px', fontWeight: 500, lineHeight: 1.4
2773
- }
2774
- }, t('googleWarn')) : null,
2775
- value.translateEngine === 'local' ? React.createElement('div', {
2776
- style: { marginTop: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }
2777
- },
2778
- React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, t('localInfo')),
2779
- React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
2780
- React.createElement('span', {
2781
- className: 'rl-badge ' + (transStatus && transStatus.running ? 'rl-badge-ok' : 'rl-badge-dim')
2782
- }, transStatus && transStatus.running ? t('localStatusRunning') : t('localStatusStopped')),
2783
- (!transStatus || !transStatus.running) ? React.createElement('button', {
2784
- type: 'button',
2785
- className: 'rl-btn rl-btn-primary',
2786
- disabled: transStarting,
2787
- onClick: startLibreTranslate
1755
+ ),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
1756
+ }
1757
+ }, t('googleWarn')) : null,value.translateEngine === 'local' ? React.createElement('div', {style: { marginTop: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }
1758
+ },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')
1759
+ }, 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
2788
1760
  }, transStarting ? t('localStarting') : t('localStartBtn')) : null
2789
- ),
2790
- transMsg ? React.createElement('div', {
2791
- style: {
2792
- marginTop: '6px', padding: '8px 12px', borderRadius: '8px',
2793
- 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)',
2794
- color: transMsg.type === 'ok' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontSize: '12px', fontWeight: 500
1761
+ ),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
2795
1762
  }
2796
1763
  }, transMsg.text) : null
2797
1764
  ) : null
2798
1765
  )
2799
- ),
2800
- React.createElement('div', { className: 'rl-section-card' },
2801
- React.createElement('div', { className: 'rl-section-title' },
2802
- React.createElement('span', null, t('secOverrides')),
2803
- React.createElement('span', { className: 'rl-badge rl-badge-dim' },
2804
- String(overridesCount) + ' ' + (typeof plural === 'function' ? plural(overridesCount, ['запись', 'записи', 'записей']) : 'записей'))
2805
- ),
2806
- React.createElement('div', { className: 'rl-section-desc' }, t('secOverridesDesc')),
2807
- React.createElement('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
2808
- React.createElement('input', {
2809
- className: 'rl-select',
2810
- style: { flex: '1 1 180px', maxWidth: 'none' },
2811
- placeholder: t('overrideKeyPlaceholder'),
2812
- value: newKey,
2813
- onChange: (e) => setNewKey(e.target.value),
2814
- }),
2815
- React.createElement('input', {
2816
- className: 'rl-select',
2817
- style: { flex: '2 1 240px', maxWidth: 'none' },
2818
- placeholder: t('overrideValuePlaceholder'),
2819
- value: newVal,
2820
- onChange: (e) => setNewVal(e.target.value),
2821
- }),
2822
- React.createElement('button', {
2823
- type: 'button',
2824
- className: 'rl-btn rl-btn-primary',
2825
- disabled: !newKey.trim() || !newVal.trim(),
2826
- onClick: () => {
2827
- const k = newKey.trim()
1766
+ ),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, ['запись', 'записи', 'записей']) : 'записей'))
1767
+ ),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()
2828
1768
  const v = newVal.trim()
2829
1769
  if (!k || !v) return
2830
1770
  const next = Object.assign({}, overrides, { [k]: v })
2831
- try { scope.set('overrides', updated) } catch (_e) { void _e }
1771
+ try { scope.set('overrides', next) } catch (_e) { void _e }
2832
1772
  setNewKey('')
2833
1773
  setNewVal('')
2834
- },
2835
- }, t('overrideAddBtn'))
2836
- ),
2837
- overridesCount === 0
1774
+ },}, t('overrideAddBtn'))
1775
+ ),overridesCount === 0
2838
1776
  ? React.createElement('div', { className: 'rl-hint-text', style: { fontStyle: 'italic', padding: '4px 0' } }, t('overrideEmpty'))
2839
- : React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '6px' } },
2840
- Object.keys(overrides).map((k) =>
2841
- React.createElement('div', {
2842
- key: k,
2843
- 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' },
2844
- },
2845
- React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', overflow: 'hidden' } },
2846
- React.createElement('code', { style: { color: 'var(--dsw-alias-state-brand-primary)', fontWeight: 600 } }, k),
2847
- React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, '→'),
2848
- React.createElement('span', { style: { color: 'var(--dsw-alias-label-primary)' } }, overrides[k])
2849
- ),
2850
- React.createElement('button', {
2851
- type: 'button',
2852
- className: 'rl-btn',
2853
- style: { padding: '2px 8px', height: '24px', fontSize: '11px', color: 'var(--dsw-alias-state-error-primary)' },
2854
- onClick: () => {
2855
- const next = Object.assign({}, overrides)
1777
+ : React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '6px' } },Object.keys(overrides).map((k) =>
1778
+ 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])
1779
+ ),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)
2856
1780
  delete next[k]
2857
- try { scope.set('overrides', updated) } catch (_e) { void _e }
2858
- },
2859
- }, t('overrideDelete'))
1781
+ try { scope.set('overrides', next) } catch (_e) { void _e }
1782
+ },}, t('overrideDelete'))
2860
1783
  )
2861
1784
  )
2862
- ),
2863
- React.createElement('div', { className: 'rl-hint-text', style: { marginTop: '8px' } }, t('overrideTip')),
2864
- React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', marginTop: '10px' } },
2865
- React.createElement('button', {
2866
- type: 'button',
2867
- className: 'rl-btn' + (isInspectorActive ? ' rl-btn-primary' : ''),
2868
- onClick: () => {
2869
- isInspectorActive = !isInspectorActive
1785
+ ),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
2870
1786
  updateInspectorPill()
2871
- },
2872
- }, isInspectorActive ? '🔍 Инспектор перевода (включён)' : '🔍 Включить инспектор перевода (Alt+I)'),
2873
- React.createElement('button', {
2874
- type: 'button',
2875
- className: 'rl-btn',
2876
- style: { fontSize: '11px' },
2877
- onClick: () => {
2878
- try {
2879
- navigator.clipboard.writeText(JSON.stringify(overrides, null, 2))
1787
+ },}, isInspectorActive ? '🔍 Инспектор перевода (включён)' : '🔍 Включить инспектор перевода (Alt+I)'),React.createElement('button', {type: 'button',className: 'rl-btn',style: { fontSize: '11px' },onClick: () => {try {navigator.clipboard.writeText(JSON.stringify(overrides, null, 2))
2880
1788
  alert('Оверрайды скопированы в буфер обмена как JSON!')
2881
1789
  } catch (err) { void err; }
2882
1790
  }
2883
- }, '📤 Экспорт JSON'),
2884
- React.createElement('button', {
2885
- type: 'button',
2886
- className: 'rl-btn',
2887
- style: { fontSize: '11px' },
2888
- onClick: () => {
2889
- const input = prompt('Вставьте JSON с оверрайдами:')
1791
+ }, '📤 Экспорт JSON'),React.createElement('button', {type: 'button',className: 'rl-btn',style: { fontSize: '11px' },onClick: () => {const input = prompt('Вставьте JSON с оверрайдами:')
2890
1792
  if (!input) return
2891
- try {
2892
- const parsed = JSON.parse(input)
2893
- if (parsed && typeof parsed === 'object') {
2894
- const merged = Object.assign({}, overrides, parsed)
1793
+ try {const parsed = JSON.parse(input)
1794
+ if (parsed && typeof parsed === 'object') {const merged = Object.assign({}, overrides, parsed)
2895
1795
  scope.set('overrides', merged)
2896
1796
  }
2897
1797
  } catch (err) { alert('Ошибка разбора JSON: ' + (err && err.message || err)) }
2898
1798
  }
2899
1799
  }, '📥 Импорт JSON')
2900
1800
  )
2901
- ),
2902
- React.createElement('div', { className: 'rl-section-card' },
2903
- React.createElement('div', { className: 'rl-section-title' },
2904
- React.createElement('span', null, t('secSupport')),
2905
- React.createElement('span', { className: 'rl-badge rl-badge-ok' }, '🟢 100.0%')
2906
- ),
2907
- React.createElement('div', { className: 'rl-section-desc' }, t('secSupportDesc')),
2908
- React.createElement('div', { className: 'rl-grid-3' },
2909
- React.createElement('div', { className: 'rl-stat-box' },
2910
- React.createElement('div', { className: 'rl-stat-val' }, '113'),
2911
- React.createElement('div', { className: 'rl-stat-label' }, t('statNamespaces'))
2912
- ),
2913
- React.createElement('div', { className: 'rl-stat-box' },
2914
- React.createElement('div', { className: 'rl-stat-val' }, '1 225'),
2915
- React.createElement('div', { className: 'rl-stat-label' }, t('statCoreKeys'))
2916
- ),
2917
- React.createElement('div', { className: 'rl-stat-box' },
2918
- React.createElement('div', { className: 'rl-stat-val' }, '5 924'),
2919
- React.createElement('div', { className: 'rl-stat-label' }, t('statPluginKeys'))
1801
+ ),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%')
1802
+ ),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'))
1803
+ ),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'))
1804
+ ),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'))
2920
1805
  )
2921
- ),
2922
- React.createElement('div', { className: 'rl-actions-row' },
2923
- React.createElement('a', {
2924
- href: makeIssueUrl({}, '0.3.7'),
2925
- target: '_blank',
2926
- rel: 'noopener noreferrer',
2927
- className: 'rl-btn rl-btn-primary'
2928
- }, '💬 ' + t('reportIssue')),
2929
- React.createElement('div', { className: 'rl-hint-text', style: { flex: 1 } },
2930
- t('exportMdHint')
1806
+ ),React.createElement('div', { className: 'rl-actions-row' },React.createElement('a', {href: makeIssueUrl({}, '0.3.9'),target: '_blank',rel: 'noopener noreferrer',className: 'rl-btn rl-btn-primary'
1807
+ }, '💬 ' + t('reportIssue')),React.createElement('div', { className: 'rl-hint-text', style: { flex: 1 } },t('exportMdHint')
2931
1808
  )
2932
1809
  )
2933
1810
  )
@@ -2936,69 +1813,9 @@ t('exportMdHint')
2936
1813
  )
2937
1814
  }
2938
1815
  const RL_CSS = [
2939
- '.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)}',
2940
- '.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}',
2941
- '.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}',
2942
- '.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}',
2943
- '.rl-head:hover{background:var(--dsw-alias-bg-layer-2)}',
2944
- '.rl-head-main{flex:1;display:flex;flex-direction:column;gap:4px}',
2945
- '.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}',
2946
- '.rl-sub{color:var(--dsw-alias-label-secondary);font-size:13px}',
2947
- '.rl-chev{margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);display:inline-flex;transition:transform .16s}',
2948
- '.rl-chev-open{transform:rotate(180deg)}',
2949
- '.rl-body{border-top:1px solid var(--dsw-alias-border-l2);padding:18px}',
2950
- '.rl-page{display:flex;flex-direction:column;gap:16px;max-width:960px}',
2951
- '.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}',
2952
- '.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}',
2953
- '.rl-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}',
2954
- '.rl-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:10px}',
2955
- '.rl-grid-3{display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:10px}',
2956
- '.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}',
2957
- '.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)}',
2958
- '.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)}',
2959
- '.rl-badge-dim{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3)}',
2960
- '.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}',
2961
- '.rl-item-head{display:flex;align-items:center;justify-content:space-between;gap:8px}',
2962
- '.rl-item-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:8px;cursor:pointer}',
2963
- '.rl-item-desc{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.4}',
2964
- '.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}',
2965
- '.rl-stat-val{font-size:16px;font-weight:700;color:var(--dsw-alias-label-primary)}',
2966
- '.rl-stat-label{font-size:11px;color:var(--dsw-alias-label-secondary);letter-spacing:0.3px}',
2967
- '.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}',
2968
- '.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}',
2969
- '.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}',
2970
- '.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}',
2971
- '.rl-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-secondary)}',
2972
- '.rl-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}',
2973
- '.rl-btn-primary:hover:not(:disabled){opacity:0.9}',
2974
- '.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}',
2975
- '.rl-select:focus{border-color:var(--dsw-alias-state-brand-primary)}',
2976
- '.rl-check{width:16px;height:16px;accent-color:var(--dsw-alias-label-primary);cursor:pointer;flex-shrink:0}',
2977
- '.rl-hint-text{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.4}',
2978
- '.rl-actions-row{display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding-top:4px}',
2979
- '.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}',
2980
- '.rl-lang-chip:hover{border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-label-primary)}',
2981
- '.rl-lang-chip-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}',
2982
- '.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}',
2983
- '.rl-action-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3)}',
2984
- '.rl-action-btn-active{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2)}',
2985
- '.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}',
2986
- '.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}',
2987
- '.rl-trans-title{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:0.5px}',
2988
- '.rl-trans-tools{display:flex;gap:4px}',
2989
- '.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}',
2990
- '.rl-trans-btn:hover{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}',
2991
- '.rl-trans-body{color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;user-select:text}',
2992
- '.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}',
2993
- '.rl-export-md-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}',
2994
- '.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}',
2995
- '.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}',
2996
- '.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}',
2997
- '.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)}',
2998
- ].join('')
1816
+ '.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('')
2999
1817
  const STYLE_ID = 'dsh-russian-lang-styles'
3000
- if (typeof document !== 'undefined' && (document.getElementById && !document.getElementById(STYLE_ID))) {
3001
- const tag = document.createElement('style')
1818
+ if (typeof document !== 'undefined' && (document.getElementById && !document.getElementById(STYLE_ID))) {const tag = document.createElement('style')
3002
1819
  tag.id = STYLE_ID
3003
1820
  tag.dataset.dshPlugin = 'dsh-russian-lang'
3004
1821
  tag.dataset.plugin = '@goodandready/dsh-russian-lang'
@@ -3008,5 +1825,4 @@ document.head.appendChild(tag)
3008
1825
  }
3009
1826
  module.exports = { apply, inject: ['slots', 'locale', 'configForms'] }
3010
1827
  return module.exports
3011
- },
3012
- })
1828
+ },})