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