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