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