@goodandready/dsh-russian-lang 0.3.26 → 0.3.28

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