@goodandready/dsh-russian-lang 0.3.14 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +12 -8
- package/lib/client.template.js +13 -3
- package/lib/index.js +137 -27
- package/lib/translator.js +27 -5
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -394,16 +394,19 @@ const RU = {"common":{"back":"Назад","cancel":"Отмена","close":"За
|
|
|
394
394
|
const ZH_RU = {}
|
|
395
395
|
RU['russian-lang'] = {"cardTitle":"Русская локализация","cardSub":"Язык интерфейса, типографика, раскладка","badgeRu":"🟢 RU активен","badgeEn":"⚪ EN активен","badgeCoverage":"🟢 100% (7,902 ключа)","badgeSmartUx":"⚡ Smart UX активен","secLanguage":"🌐 Язык интерфейса","secLanguageDesc":"Нативное переключение языка интерфейса DSH на русский без перезагрузки страницы.","enabled":"Русский язык включён","enabledDesc":"Переключает язык интерфейса DeepSeek Harness на русский.","quickSwitchNote":"Быстрый переключатель RU ⇄ EN доступен в шапке сессии рядом с кнопками диалога.","secTypography":"✍️ Умная типографика и ввод (Smart UX)","secTypographyDesc":"Нормы русской типографики во время диалога и набора.","typography":"Типографика вывода","typographyDesc":"Кавычки «», тире — вместо дефисов, неразрывные пробелы.","yo":"Буква «ё»","yoDesc":"Восстановление «ё» в частых словах (ещё, идёт и др.).","liveInput":"Живая типографика инпута","liveInputDesc":"Кавычки «», тире — и неразрывные пробелы перед отправкой.","slashAliases":"Русские алиасы команд","slashAliasesDesc":"Команды: /цель, /сжать, /план, /справка, /память.","altLHintText":"Конвертация раскладки текущего поля (ghbdtn ⇄ привет).","secAgentPrompt":"🤖 Системный промпт агента","secAgentPromptDesc":"Официальная секция расширения DSH systemPrompt для ведения диалога на русском языке.","agentPrompt":"Русский промпт агента","agentPromptDesc":"Добавляет в системный промпт инструкцию отвечать по-русски в выбранном стиле.","agentPromptPreset":"Стиль ответов агента","presetExpert":"Технический эксперт (строгая терминология, чистый код)","presetWriter":"Технический писатель (Markdown, таблицы, ГОСТ)","presetConcise":"Лаконичный режим (кратко, без лишней воды)","presetReviewer":"Код-ревьюер (поиск багов, безопасность, аудит)","presetArchitect":"Системный архитектор (контракты, масштабируемость)","presetTutor":"Наставник (понятные объяснения, пошаговый разбор)","secUpdater":"🔄 Обновление языкового пакета","secUpdaterDesc":"Проверка наличия новых релизов в реестре npm и обновление в один клик.","updaterCurrent":"Текущая версия: v{version}","updaterLatest":"Доступна новая версия: v{version}","updaterUpToDate":"Установлена актуальная версия","updaterChecking":"Проверка…","updaterCheckBtn":"Проверить обновления","updaterBtn":"Обновить до v{version} в 1 клик","updaterUpdating":"Установка обновления…","updaterSuccess":"✅ Плагин успешно обновлён! Перезапустите DSH для применения.","updaterFailed":"❌ Не удалось проверить/обновить плагин. Проверьте сеть или логи сервера.","badgeUpdateAvailable":"Доступно обновление","badgeUpToDate":"Актуальная версия","secTranslator":"🌐 Перевод сообщений ассистента","secTranslatorDesc":"Перевод ответов модели в чате на русский язык.","translateEngine":"Движок перевода","engineOff":"Выключен (по умолчанию, без сетевых вызовов)","engineLocal":"Локальный LibreTranslate (приватно, ~600 МБ RAM)","engineGoogle":"Google Translate (онлайн)","googleWarn":"⚠️ Текст сообщений передаётся на публичные серверы Google.","localStatusRunning":"🟢 Контейнер LibreTranslate активен","localStatusStopped":"⚪ Контейнер LibreTranslate не запущен","localStartBtn":"Запустить LibreTranslate в Docker (~600 МБ RAM)","localStarting":"Запуск контейнера…","localInfo":"LibreTranslate в Docker: изолированно, ~600 МБ RAM.","secOverrides":"✍️ Пользовательские переопределения (Custom Overrides)","secOverridesDesc":"Замена любой формулировки интерфейса DSH на собственную.","overrideKeyPlaceholder":"Ключ (например, common.back или settings.title)","overrideValuePlaceholder":"Ваш русский перевод","overrideAddBtn":"Добавить","overrideEmpty":"Переопределений пока нет.","overrideDelete":"Удалить","overrideTip":"💡 Зажмите Alt и кликните на элемент UI для быстрой правки.","secSupport":"📊 Покрытие экосистемы и поддержка","secSupportDesc":"100% UI-покрытие ядра и плагинов DSH v0.1.6-alpha.1.","statNamespaces":"Пространств имён","statCoreKeys":"Ключей ядра","statPluginKeys":"Ключей плагинов","overridesCount":"Своих переопределений","statusLoading":"Настройки загружаются…","statusUnavailable":"Настройки недоступны на этом хосте","translateTurn":"Перевести на русский","reportIssue":"Сообщить о неточности перевода","exportMdHint":"Экспорт диалога в Markdown доступен по кнопке [ 📥 MD ] в шапке сессии.","translateTurnHint":"Перевод ответов ассистента на русский доступен по кнопке [ RU ↗ ] на блоках сообщений."}
|
|
396
396
|
const SETTINGS_NS_NAME = 'russian-lang'
|
|
397
|
+
const FORM_NS = 'dsh-russian-lang'
|
|
398
|
+
const LEGACY_NS = 'russian-lang'
|
|
397
399
|
const ROW_CONFIG_KEY = '@goodandready/dsh-russian-lang#russian-lang'
|
|
398
400
|
function apply(ctx) {const runtime = ctx.locale
|
|
399
|
-
const resolveScope = (c) => {try {if (c && c.configForms) return c.configForms.get(
|
|
400
|
-
if (c && c.settingsScope) return c.settingsScope.bind({ namespace:
|
|
401
|
+
const resolveScope = (c) => {try {if (c && c.configForms) return c.configForms.get(FORM_NS)
|
|
402
|
+
if (c && c.settingsScope) return c.settingsScope.bind({ namespace: LEGACY_NS })
|
|
401
403
|
} catch (err) { void err }
|
|
402
404
|
return null
|
|
403
405
|
}
|
|
404
406
|
const rawScope = resolveScope(ctx)
|
|
405
407
|
const scope = (rawScope && typeof rawScope.getSnapshot === 'function') ? rawScope : {getSnapshot: () => ({ status: 'ready', value: {} }),subscribe: () => () => {},set: () => Promise.resolve()
|
|
406
408
|
}
|
|
409
|
+
const legacyFlagMode = !(ctx && ctx.configForms)
|
|
407
410
|
for (const ns of Object.keys(RU)) {ctx.effect(() => {try { return ctx.locale.register(ns, 'ru', RU[ns]) } catch (err) { return () => {} }}, 'dsh-russian-lang: ' + ns)
|
|
408
411
|
}
|
|
409
412
|
const registerDictMap = (dictMap) => {if (!dictMap || typeof dictMap !== 'object') return
|
|
@@ -540,7 +543,8 @@ for (const p of RUNTIME_PROP_NAMES) {if (origProps[p] !== undefined) runtime[p]
|
|
|
540
543
|
else delete runtime[p]
|
|
541
544
|
}
|
|
542
545
|
if (typeof removeLang === 'function') { try { removeLang() } catch (_) { } }}}, 'dsh-russian-lang: runtime-lifecycle')
|
|
543
|
-
const syncFlag = () => {
|
|
546
|
+
const syncFlag = () => {if (!legacyFlagMode) return
|
|
547
|
+
try {const wantRu = runtime.getLocale().active === 'ru'
|
|
544
548
|
const value = scope.getSnapshot().value || {}
|
|
545
549
|
if (!!value.enabled !== wantRu) {const r = scope.set('enabled', wantRu)
|
|
546
550
|
if (r && typeof r.catch === 'function') {r.catch((err) => {console.warn('dsh-russian-lang: scope.set enabled failed', err && err.message || err)
|
|
@@ -554,7 +558,7 @@ runtime.setLocale('ru')
|
|
|
554
558
|
let booted = false
|
|
555
559
|
const tryBoot = () => {if (booted) return
|
|
556
560
|
try {const value = scope.getSnapshot().value
|
|
557
|
-
if (value && value.enabled === true) { booted = true; activate() }} catch (err) { }}
|
|
561
|
+
if (legacyFlagMode && value && value.enabled === true) { booted = true; activate() }} catch (err) { }}
|
|
558
562
|
ctx.effect(() => scope.subscribe(tryBoot), 'dsh-russian-lang: boot')
|
|
559
563
|
tryBoot()
|
|
560
564
|
const unsubscribeLang = runtime.subscribe(syncLang)
|
|
@@ -1122,7 +1126,7 @@ setTimeout(() => { copyKeyBtn.textContent = '📋' }, 1500)
|
|
|
1122
1126
|
}}
|
|
1123
1127
|
const copyReportBtn = modal.querySelector('.rl-copy-report-btn')
|
|
1124
1128
|
if (copyReportBtn) {copyReportBtn.onclick = () => {const snippet = typeof generateBugReportSnippet === 'function'
|
|
1125
|
-
? generateBugReportSnippet({ key: k, ns, original: origText || rawText, current: rawText, override: textarea.value.trim(), pkgVersion: '0.3.
|
|
1129
|
+
? generateBugReportSnippet({ key: k, ns, original: origText || rawText, current: rawText, override: textarea.value.trim(), pkgVersion: '0.3.15' })
|
|
1126
1130
|
: ('### Репорт: ' + (k || rawText) + '\n- Исходный: ' + (origText || rawText) + '\n- Перевод: ' + textarea.value.trim())
|
|
1127
1131
|
try { navigator.clipboard.writeText(snippet) } catch (err) { void err; }
|
|
1128
1132
|
copyReportBtn.textContent = '✓ Скопировано'
|
|
@@ -1409,7 +1413,7 @@ const [snap, setSnap] = React.useState(() => (scope && scope.getSnapshot ? scope
|
|
|
1409
1413
|
const [ruActive, setRuActive] = React.useState(() => { try { return runtime.getLocale().active === 'ru' } catch (e) { return false } })
|
|
1410
1414
|
const [typo, setTypoState] = React.useState(() =>
|
|
1411
1415
|
(snap.value && snap.value.typography) || {})
|
|
1412
|
-
const [upStatus, setUpStatus] = React.useState({currentVersion: '0.3.
|
|
1416
|
+
const [upStatus, setUpStatus] = React.useState({currentVersion: '0.3.15',latestVersion: undefined,updateAvailable: false
|
|
1413
1417
|
})
|
|
1414
1418
|
const [upLoading, setUpLoading] = React.useState(false)
|
|
1415
1419
|
const [upMsg, setUpMsg] = React.useState(null)
|
|
@@ -1502,7 +1506,7 @@ scope.set('agentPromptPreset', val)
|
|
|
1502
1506
|
} catch (err) { console.warn('dsh-russian-lang: set agentPromptPreset failed', err) }}},React.createElement('option', { value: 'technical_expert' }, t('presetExpert')),React.createElement('option', { value: 'tech_writer' }, t('presetWriter')),React.createElement('option', { value: 'concise' }, t('presetConcise')),React.createElement('option', { value: 'code_reviewer' }, t('presetReviewer')),React.createElement('option', { value: 'architect' }, t('presetArchitect')),React.createElement('option', { value: 'tutor' }, t('presetTutor')))),presetInfo ? React.createElement('div', { className: 'rl-preview-box' },React.createElement('div', { style: { fontWeight: 600, marginBottom: '4px', color: 'var(--dsw-alias-label-primary)' } }, '💬 ' + presetInfo.label + ':'),presetInfo.text
|
|
1503
1507
|
) : null
|
|
1504
1508
|
) : null
|
|
1505
|
-
),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secUpdater')),React.createElement('span', { className: 'rl-badge ' + (upStatus.updateAvailable ? 'rl-badge-warn' : 'rl-badge-ok') },upStatus.updateAvailable ? t('badgeUpdateAvailable') : t('badgeUpToDate'))),React.createElement('div', { className: 'rl-section-desc' }, t('secUpdaterDesc')),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },React.createElement('div', null,React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },t('updaterCurrent').replace('{version}', upStatus.currentVersion || '0.3.
|
|
1509
|
+
),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secUpdater')),React.createElement('span', { className: 'rl-badge ' + (upStatus.updateAvailable ? 'rl-badge-warn' : 'rl-badge-ok') },upStatus.updateAvailable ? t('badgeUpdateAvailable') : t('badgeUpToDate'))),React.createElement('div', { className: 'rl-section-desc' }, t('secUpdaterDesc')),React.createElement('div', { className: 'rl-item-card' },React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },React.createElement('div', null,React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },t('updaterCurrent').replace('{version}', upStatus.currentVersion || '0.3.15')),upStatus.updateAvailable
|
|
1506
1510
|
? React.createElement('div', { style: { color: 'var(--dsw-alias-state-warning-primary)', marginTop: '2px', fontWeight: 500 } },t('updaterLatest').replace('{version}', upStatus.latestVersion || ''))
|
|
1507
1511
|
: React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', marginTop: '2px' } },t('updaterUpToDate'))),upStatus.updateAvailable
|
|
1508
1512
|
? React.createElement('button', {type: 'button',className: 'rl-btn rl-btn-primary',disabled: upLoading,onClick: triggerUpdate,}, upLoading ? t('updaterUpdating') : t('updaterBtn').replace('{version}', upStatus.latestVersion || ''))
|
|
@@ -1540,7 +1544,7 @@ if (!input) return
|
|
|
1540
1544
|
try {const parsed = JSON.parse(input)
|
|
1541
1545
|
if (parsed && typeof parsed === 'object') {const merged = Object.assign({}, overrides, parsed)
|
|
1542
1546
|
scope.set('overrides', merged)
|
|
1543
|
-
}} catch (err) { alert('Ошибка разбора JSON: ' + (err && err.message || err)) }}}, '📥 Импорт JSON'))),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secSupport')),React.createElement('span', { className: 'rl-badge rl-badge-ok' }, '🟢 100.0%')),React.createElement('div', { className: 'rl-section-desc' }, t('secSupportDesc')),React.createElement('div', { className: 'rl-grid-3' },React.createElement('div', { className: 'rl-stat-box' },React.createElement('div', { className: 'rl-stat-val' }, '113'),React.createElement('div', { className: 'rl-stat-label' }, t('statNamespaces'))),React.createElement('div', { className: 'rl-stat-box' },React.createElement('div', { className: 'rl-stat-val' }, '1 225'),React.createElement('div', { className: 'rl-stat-label' }, t('statCoreKeys'))),React.createElement('div', { className: 'rl-stat-box' },React.createElement('div', { className: 'rl-stat-val' }, '5 924'),React.createElement('div', { className: 'rl-stat-label' }, t('statPluginKeys')))),React.createElement('div', { className: 'rl-actions-row' },React.createElement('a', {href: makeIssueUrl({}, '0.3.
|
|
1547
|
+
}} catch (err) { alert('Ошибка разбора JSON: ' + (err && err.message || err)) }}}, '📥 Импорт JSON'))),React.createElement('div', { className: 'rl-section-card' },React.createElement('div', { className: 'rl-section-title' },React.createElement('span', null, t('secSupport')),React.createElement('span', { className: 'rl-badge rl-badge-ok' }, '🟢 100.0%')),React.createElement('div', { className: 'rl-section-desc' }, t('secSupportDesc')),React.createElement('div', { className: 'rl-grid-3' },React.createElement('div', { className: 'rl-stat-box' },React.createElement('div', { className: 'rl-stat-val' }, '113'),React.createElement('div', { className: 'rl-stat-label' }, t('statNamespaces'))),React.createElement('div', { className: 'rl-stat-box' },React.createElement('div', { className: 'rl-stat-val' }, '1 225'),React.createElement('div', { className: 'rl-stat-label' }, t('statCoreKeys'))),React.createElement('div', { className: 'rl-stat-box' },React.createElement('div', { className: 'rl-stat-val' }, '5 924'),React.createElement('div', { className: 'rl-stat-label' }, t('statPluginKeys')))),React.createElement('div', { className: 'rl-actions-row' },React.createElement('a', {href: makeIssueUrl({}, '0.3.15'),target: '_blank',rel: 'noopener noreferrer',className: 'rl-btn rl-btn-primary'
|
|
1544
1548
|
}, '💬 ' + t('reportIssue')),React.createElement('div', { className: 'rl-hint-text', style: { flex: 1 } },t('exportMdHint')))))))
|
|
1545
1549
|
}
|
|
1546
1550
|
const RL_CSS = ['.rl-modal-mask{position:fixed;top:0;left:0;width:100vw;height:100vh;background:var(--dsw-alias-bg-mask);display:flex;align-items:center;justify-content:center;z-index:99999;backdrop-filter:blur(2px)}.rl-modal-box{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;width:calc(100vw - 40px);max-width:500px;box-shadow:var(--dsw-alias-shadow-l3);display:flex;flex-direction:column;gap:12px;padding:18px}.rl-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;overflow:hidden;transition:border-color .15s ease}.rl-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 18px}.rl-head:hover{background:var(--dsw-alias-bg-layer-2)}.rl-head-main{flex:1;display:flex;flex-direction:column;gap:4px}.rl-title{color:var(--dsw-alias-label-primary);font-size:16px;font-weight:600;line-height:1.4;display:flex;align-items:center;flex-wrap:wrap;gap:8px}.rl-sub{color:var(--dsw-alias-label-secondary);font-size:13px}.rl-chev{margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);display:inline-flex;transition:transform .16s}.rl-chev-open{transform:rotate(180deg)}.rl-body{border-top:1px solid var(--dsw-alias-border-l2);padding:18px}.rl-page{display:flex;flex-direction:column;gap:16px;max-width:960px}.rl-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:16px 18px;display:flex;flex-direction:column;gap:12px}.rl-section-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px}.rl-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}.rl-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:10px}.rl-grid-3{display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:10px}.rl-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}.rl-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}.rl-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent)}.rl-badge-dim{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3)}.rl-item-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:4px}.rl-item-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.rl-item-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:8px;cursor:pointer}.rl-item-desc{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.4}.rl-stat-box{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:2px}.rl-stat-val{font-size:16px;font-weight:700;color:var(--dsw-alias-label-primary)}.rl-stat-label{font-size:11px;color:var(--dsw-alias-label-secondary);letter-spacing:0.3px}.rl-hotkey-box{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4;display:flex;align-items:flex-start;gap:10px}.rl-hotkey-tag{font-size:11px;font-weight:700;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);white-space:nowrap}.rl-preview-box{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}.rl-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:12px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease;text-decoration:none}.rl-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-secondary)}.rl-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}.rl-btn-primary:hover:not(:disabled){opacity:0.9}.rl-select{height:32px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 8px;font-size:12px;outline:none;width:100%;max-width:380px}.rl-select:focus{border-color:var(--dsw-alias-state-brand-primary)}.rl-check{width:16px;height:16px;accent-color:var(--dsw-alias-label-primary);cursor:pointer;flex-shrink:0}.rl-hint-text{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.4}.rl-actions-row{display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding-top:4px}.rl-lang-chip{appearance:none;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:700;padding:2px 7px;border-radius:6px;display:inline-flex;align-items:center;transition:all .15s;margin:0 4px}.rl-lang-chip:hover{border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-label-primary)}.rl-lang-chip-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}.rl-action-btn{appearance:none;background:0 0;border:1px solid transparent;border-radius:4px;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:11px;padding:2px 5px;display:inline-flex;align-items:center;transition:all .15s}.rl-action-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3)}.rl-action-btn-active{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2)}.rl-turn-translation{margin:8px 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;font-size:13px;line-height:1.5}.rl-trans-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:4px}.rl-trans-title{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:0.5px}.rl-trans-tools{display:flex;gap:4px}.rl-trans-btn{appearance:none;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);cursor:pointer;padding:2px 7px;border-radius:4px;font-size:11px}.rl-trans-btn:hover{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.rl-trans-body{color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;user-select:text}.rl-export-md-btn{appearance:none;border:1px solid var(--dsw-alias-border-l2);height:32px;color:var(--dsw-alias-label-primary);cursor:pointer;background:transparent;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:6px 12px;font-size:13px;font-weight:500;display:inline-flex;white-space:nowrap;margin-left:6px;transition:all .15s ease}.rl-export-md-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.rl-inspector-hover{position:absolute;pointer-events:none;border:2px solid var(--dsw-alias-state-brand-primary);border-radius:4px;background:color-mix(in srgb,var(--dsw-alias-state-brand-primary) 12%,transparent);box-shadow:0 0 10px color-mix(in srgb,var(--dsw-alias-state-brand-primary) 30%,transparent);z-index:99998;transition:all .06s ease;display:none}.rl-inspector-badge{position:absolute;bottom:calc(100% + 4px);left:0;background:var(--dsw-alias-state-brand-primary);color:var(--dsw-alias-label-inverse);font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;white-space:nowrap;box-shadow:var(--dsw-alias-shadow-l2);pointer-events:none}.rl-inspector-pill{position:fixed;bottom:18px;right:18px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:20px;padding:6px 12px;box-shadow:var(--dsw-alias-shadow-l3);display:none;align-items:center;gap:8px;font-size:12px;z-index:99990;user-select:none}.rl-inspector-pill-active{border-color:var(--dsw-alias-state-brand-primary);box-shadow:0 0 12px color-mix(in srgb,var(--dsw-alias-state-brand-primary) 35%,transparent)}'
|
package/lib/client.template.js
CHANGED
|
@@ -33,6 +33,11 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
RU['russian-lang'] = /*__CARD_RU__*/{}
|
|
34
34
|
|
|
35
35
|
const SETTINGS_NS_NAME = 'russian-lang'
|
|
36
|
+
// Namespace формы настроек: на 0.1.7+ это id записи загрузчика
|
|
37
|
+
// ('dsh-russian-lang' из cordis.patch.yml), а не имя, выбранное плагином
|
|
38
|
+
// ('russian-lang'), как было на 0.1.6 и ниже. Это разные вещи.
|
|
39
|
+
const FORM_NS = 'dsh-russian-lang'
|
|
40
|
+
const LEGACY_NS = 'russian-lang'
|
|
36
41
|
// Plugins page row seat (DSH 0.1.6-alpha.2): key = '<package name>#<row id>'.
|
|
37
42
|
const ROW_CONFIG_KEY = '@goodandready/dsh-russian-lang#russian-lang'
|
|
38
43
|
|
|
@@ -40,8 +45,8 @@ window.__ModuleLoader__.load({
|
|
|
40
45
|
const runtime = ctx.locale
|
|
41
46
|
const resolveScope = (c) => {
|
|
42
47
|
try {
|
|
43
|
-
if (c && c.configForms) return c.configForms.get(
|
|
44
|
-
if (c && c.settingsScope) return c.settingsScope.bind({ namespace:
|
|
48
|
+
if (c && c.configForms) return c.configForms.get(FORM_NS)
|
|
49
|
+
if (c && c.settingsScope) return c.settingsScope.bind({ namespace: LEGACY_NS })
|
|
45
50
|
} catch (err) { void err }
|
|
46
51
|
return null
|
|
47
52
|
}
|
|
@@ -51,6 +56,10 @@ window.__ModuleLoader__.load({
|
|
|
51
56
|
subscribe: () => () => {},
|
|
52
57
|
set: () => Promise.resolve()
|
|
53
58
|
}
|
|
59
|
+
// На 0.1.7+ выбор языка хранит сам DSH (locale.preference): собственный
|
|
60
|
+
// флаг там не читаем и не пишем, виджет работает просто с локалью.
|
|
61
|
+
// На 0.1.6 и ниже поведение прежнее — флаг живёт в настройках плагина.
|
|
62
|
+
const legacyFlagMode = !(ctx && ctx.configForms)
|
|
54
63
|
|
|
55
64
|
// 1. Словари ядра DSH: каждый namespace — свой эффект, снимается вместе с плагином
|
|
56
65
|
for (const ns of Object.keys(RU)) {
|
|
@@ -305,6 +314,7 @@ window.__ModuleLoader__.load({
|
|
|
305
314
|
// scope.set возвращает Promise<void> в v0.1.2-alpha.2, ошибка приходит
|
|
306
315
|
// через rejection и try/catch её не ловит — обрабатываем оба канала.
|
|
307
316
|
const syncFlag = () => {
|
|
317
|
+
if (!legacyFlagMode) return
|
|
308
318
|
try {
|
|
309
319
|
const wantRu = runtime.getLocale().active === 'ru'
|
|
310
320
|
const value = scope.getSnapshot().value || {}
|
|
@@ -338,7 +348,7 @@ window.__ModuleLoader__.load({
|
|
|
338
348
|
if (booted) return
|
|
339
349
|
try {
|
|
340
350
|
const value = scope.getSnapshot().value
|
|
341
|
-
if (value && value.enabled === true) { booted = true; activate() }
|
|
351
|
+
if (legacyFlagMode && value && value.enabled === true) { booted = true; activate() }
|
|
342
352
|
} catch (err) { /* ignore */ }
|
|
343
353
|
}
|
|
344
354
|
ctx.effect(() => scope.subscribe(tryBoot), 'dsh-russian-lang: boot')
|
package/lib/index.js
CHANGED
|
@@ -39,48 +39,158 @@ const sendJsonWithEtag = (request, response, obj, maxAge = 300) => {
|
|
|
39
39
|
|
|
40
40
|
export const name = '@goodandready/dsh-russian-lang'
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
// Легаси-namespace настроек (DSH <= 0.1.6). Namespace форм на 0.1.7+ —
|
|
43
|
+
// id записи загрузчика ('dsh-russian-lang' из cordis.patch.yml), схема
|
|
44
|
+
// попадает туда через экспортированный Config ниже.
|
|
45
|
+
const NS = 'russian-lang'
|
|
46
|
+
|
|
47
|
+
// `.volatile()` есть только в новом schemastery; на старом его вызов
|
|
48
|
+
// бросает TypeError на уровне модуля — поэтому только feature-detect.
|
|
49
|
+
const vol = (s) => (typeof s?.volatile === 'function' ? s.volatile() : s)
|
|
50
|
+
|
|
51
|
+
export const Config = z.object({
|
|
52
|
+
enabled: vol(z.boolean().required(false)),
|
|
53
|
+
overrides: vol(z.dict(z.string()).required(false)),
|
|
54
|
+
typography: vol(z.object({
|
|
46
55
|
enabled: z.boolean().required(false),
|
|
47
56
|
yo: z.boolean().required(false),
|
|
48
57
|
liveInput: z.boolean().required(false)
|
|
49
|
-
}).required(false),
|
|
50
|
-
agentPrompt: z.boolean().required(false),
|
|
51
|
-
agentPromptPreset: z.string().required(false),
|
|
52
|
-
slashAliases: z.boolean().required(false),
|
|
53
|
-
quickSwitch: z.boolean().required(false),
|
|
54
|
-
translateEngine: z.string().required(false),
|
|
55
|
-
localApiUrl: z.string().required(false)
|
|
58
|
+
}).required(false)),
|
|
59
|
+
agentPrompt: vol(z.boolean().required(false)),
|
|
60
|
+
agentPromptPreset: vol(z.string().required(false)),
|
|
61
|
+
slashAliases: vol(z.boolean().required(false)),
|
|
62
|
+
quickSwitch: vol(z.boolean().required(false)),
|
|
63
|
+
translateEngine: vol(z.string().required(false)),
|
|
64
|
+
localApiUrl: vol(z.string().required(false))
|
|
56
65
|
})
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
// Историческое имя схемы; сама схема — Config выше.
|
|
68
|
+
const RussianLangSettingsSchema = Config
|
|
69
|
+
|
|
70
|
+
// Plain-схема для легаси-ветки register(): старый провайдер ждёт schemastery-схему,
|
|
71
|
+
// поэтому сериализуем, рекурсивно снимаем meta.volatile и рехидрируем обратно
|
|
72
|
+
// в схему — иначе volatile-поля остались бы рефами и сломали бы старое хранилище.
|
|
73
|
+
// Любая ошибка — возвращаем исходную схему, наружу не бросаем.
|
|
74
|
+
function plainSchema(schema) {
|
|
75
|
+
try {
|
|
76
|
+
const json = (schema && typeof schema.toJSON === 'function') ? schema.toJSON() : schema
|
|
77
|
+
const clone = JSON.parse(JSON.stringify(json))
|
|
78
|
+
const seen = new Set()
|
|
79
|
+
const strip = (node) => {
|
|
80
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return
|
|
81
|
+
seen.add(node)
|
|
82
|
+
if (node.meta && typeof node.meta === 'object') {
|
|
83
|
+
try { delete node.meta.volatile } catch (_) { /* bestEffort */ void _ }
|
|
84
|
+
}
|
|
85
|
+
for (const key of Object.keys(node)) strip(node[key])
|
|
86
|
+
}
|
|
87
|
+
strip(clone)
|
|
88
|
+
return new z(clone)
|
|
89
|
+
} catch (_) {
|
|
90
|
+
return schema
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function apply(ctx, config) {
|
|
95
|
+
// Живой провайдер настроек; на 0.1.7+ у него есть configure(),
|
|
96
|
+
// на старых — register()/get(). Пусто до первого inject.
|
|
97
|
+
let settingsProvider = null
|
|
98
|
+
|
|
99
|
+
// Единый снимок значений: на 0.1.7+ разворачиваем volatile-рефы второго
|
|
100
|
+
// аргумента (ссылки стабильны — загрузчик обновляет их на месте),
|
|
101
|
+
// на старой ветке читаем документ провайдера. Никогда не бросает.
|
|
102
|
+
const readSettings = () => {
|
|
103
|
+
try {
|
|
104
|
+
const unwrap = (src) => {
|
|
105
|
+
if (!src || typeof src !== 'object') return {}
|
|
106
|
+
const out = {}
|
|
107
|
+
for (const key of Object.keys(src)) {
|
|
108
|
+
const v = src[key]
|
|
109
|
+
out[key] = (v && typeof v.get === 'function') ? v.get() : v
|
|
110
|
+
}
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
if (settingsProvider && typeof settingsProvider.configure === 'function') {
|
|
114
|
+
return unwrap(config)
|
|
115
|
+
}
|
|
116
|
+
if (settingsProvider && typeof settingsProvider.get === 'function') {
|
|
117
|
+
return settingsProvider.get(NS) || {}
|
|
118
|
+
}
|
|
119
|
+
return unwrap(config)
|
|
120
|
+
} catch (_) {
|
|
121
|
+
return {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
60
125
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
61
|
-
|
|
62
|
-
|
|
126
|
+
let provider = null
|
|
127
|
+
try {
|
|
128
|
+
provider = settingsCtx?.settings
|
|
129
|
+
?? (typeof settingsCtx?.get === 'function' ? settingsCtx.get('settings') : null)
|
|
130
|
+
?? null
|
|
131
|
+
} catch (_) { provider = null }
|
|
132
|
+
settingsProvider = provider
|
|
133
|
+
if (!provider) return
|
|
134
|
+
try {
|
|
135
|
+
if (typeof provider.configure === 'function') {
|
|
136
|
+
// 0.1.7+: схема уже у загрузчика через экспортированный Config;
|
|
137
|
+
// отключаем автогенерацию страницы (карточка у нас кастомная).
|
|
138
|
+
if (typeof settingsCtx?.effect === 'function') {
|
|
139
|
+
settingsCtx.effect(() => provider.configure({ auto: false }, ctx.fiber))
|
|
140
|
+
} else {
|
|
141
|
+
provider.configure({ auto: false }, ctx.fiber)
|
|
142
|
+
}
|
|
143
|
+
} else if (typeof provider.register === 'function') {
|
|
144
|
+
// Легаси: plain-схема без volatile-мета.
|
|
145
|
+
try {
|
|
146
|
+
provider.register(NS, plainSchema(RussianLangSettingsSchema))
|
|
147
|
+
} catch (_) {
|
|
148
|
+
provider.register(NS, RussianLangSettingsSchema)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Иначе тихая деградация: readSettings вернёт {} / развернёт рефы.
|
|
152
|
+
} catch (_) { /* never throw outwards */ void _ }
|
|
63
153
|
})
|
|
64
154
|
|
|
65
155
|
// #68: русский системный промпт агента (опционально). Регистрируем секцию
|
|
66
156
|
// промпта, которая задёт стиль ответов по-русски, когда включён флаг
|
|
67
157
|
// russian-lang.agentPrompt. Секция — официальная точка расширения ядра.
|
|
68
|
-
|
|
69
|
-
|
|
158
|
+
// Значения — через readSettings() (volatile-рефы на 0.1.7+, документ
|
|
159
|
+
// провайдера на легаси). Пересинхронизация: loader/volatile-update
|
|
160
|
+
// (свой ctx, 0.1.7) и settings/updated (старые ядра).
|
|
161
|
+
ctx.inject(['systemPrompt'], (promptCtx) => {
|
|
162
|
+
let prompt = promptCtx?.systemPrompt
|
|
163
|
+
try {
|
|
164
|
+
if (!prompt && typeof promptCtx?.get === 'function') prompt = promptCtx.get('systemPrompt')
|
|
165
|
+
} catch (_) { /* bestEffort */ void _ }
|
|
166
|
+
if (!prompt || typeof prompt.section !== 'function') return
|
|
70
167
|
const RU_SECTION = 'dsh-russian-lang-agent-prompt'
|
|
71
168
|
const DEFAULT_RU_TEXT = 'Отвечай пользователю на русском языке. Если пользователь пишет на другом языке, отвечай на его языке.'
|
|
169
|
+
let unregisterSection = null
|
|
72
170
|
let currentText = ''
|
|
73
171
|
const sync = () => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
172
|
+
try {
|
|
173
|
+
const value = readSettings()
|
|
174
|
+
const want = !!(value && value.agentPrompt)
|
|
175
|
+
const presetKey = value && value.agentPromptPreset
|
|
176
|
+
const preset = SYSTEM_PROMPT_PRESETS[presetKey]
|
|
177
|
+
const text = want ? (preset ? preset.text : DEFAULT_RU_TEXT) : ''
|
|
178
|
+
if (text === currentText) return
|
|
179
|
+
// section() не идемпотентен: повторная регистрация того же имени
|
|
180
|
+
// бросает, поэтому сначала снимаем прежнюю секцию через disposer.
|
|
181
|
+
if (typeof unregisterSection === 'function') {
|
|
182
|
+
try { unregisterSection() } catch (_) { /* bestEffort */ void _ }
|
|
183
|
+
}
|
|
184
|
+
unregisterSection = null
|
|
185
|
+
currentText = text
|
|
186
|
+
// Выключение флага снимает секцию, а не пушит пустой текст.
|
|
187
|
+
if (!text) return
|
|
188
|
+
const dispose = prompt.section({ name: RU_SECTION, order: 1000, text })
|
|
189
|
+
unregisterSection = typeof dispose === 'function' ? dispose : null
|
|
190
|
+
} catch (_) { /* never throw outwards */ void _ }
|
|
82
191
|
}
|
|
83
|
-
ctx.on('
|
|
192
|
+
try { ctx.on('loader/volatile-update', sync) } catch (_) { /* bestEffort */ void _ }
|
|
193
|
+
try { ctx.on('settings/updated', (ns) => { if (ns === NS) sync() }) } catch (_) { /* bestEffort */ void _ }
|
|
84
194
|
sync()
|
|
85
195
|
})
|
|
86
196
|
|
|
@@ -95,7 +205,7 @@ export function apply(ctx) {
|
|
|
95
205
|
manifestUrl: new URL('../package.json', import.meta.url),
|
|
96
206
|
})
|
|
97
207
|
|
|
98
|
-
const unregTranslator = registerTranslator(wctx,
|
|
208
|
+
const unregTranslator = registerTranslator(wctx, readSettings)
|
|
99
209
|
|
|
100
210
|
let ws = null
|
|
101
211
|
try { ws = wctx?.webServer } catch (_) { ws = null }
|
package/lib/translator.js
CHANGED
|
@@ -101,8 +101,30 @@ export function isTrustedTranslatorRequest(request) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Нормализует источник настроек к plain-объекту: принимает либо готовый
|
|
106
|
+
* объект, либо функцию-ридер (как readSettings из lib/index.js), либо —
|
|
107
|
+
* для обратной совместимости — легаси settings-контекст с settings.get().
|
|
108
|
+
*/
|
|
109
|
+
function resolveTranslatorSettings(source) {
|
|
110
|
+
try {
|
|
111
|
+
if (typeof source === 'function') return source() || {}
|
|
112
|
+
if (source && typeof source === 'object') {
|
|
113
|
+
if (source.settings && typeof source.settings.get === 'function') {
|
|
114
|
+
return source.settings.get('russian-lang') || {}
|
|
115
|
+
}
|
|
116
|
+
if (typeof source.translateEngine === 'string' || typeof source.localApiUrl === 'string'
|
|
117
|
+
|| typeof source.agentPrompt !== 'undefined' || typeof source.enabled !== 'undefined') {
|
|
118
|
+
return source
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (_) { /* bestEffort */ void _ }
|
|
122
|
+
return {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
export async function getTranslatorStatus(settingsSource) {
|
|
127
|
+
const currentSettings = resolveTranslatorSettings(settingsSource)
|
|
106
128
|
const engine = currentSettings.translateEngine || 'off'
|
|
107
129
|
const localUrl = (currentSettings.localApiUrl || 'http://127.0.0.1:5000').replace(/\/+$/, '')
|
|
108
130
|
|
|
@@ -236,7 +258,7 @@ async function translateLibre(text, targetLang = 'ru', localUrl = 'http://127.0.
|
|
|
236
258
|
return data.translatedText || text
|
|
237
259
|
}
|
|
238
260
|
|
|
239
|
-
export function registerTranslator(wctx,
|
|
261
|
+
export function registerTranslator(wctx, settingsSource) {
|
|
240
262
|
let ws = null
|
|
241
263
|
try { ws = wctx?.webServer } catch (_) { ws = null }
|
|
242
264
|
if (!ws || typeof ws.register !== 'function') return () => {}
|
|
@@ -255,7 +277,7 @@ export function registerTranslator(wctx, settingsCtx) {
|
|
|
255
277
|
response.end(JSON.stringify({ error: 'Method not allowed' }))
|
|
256
278
|
return
|
|
257
279
|
}
|
|
258
|
-
const status = await getTranslatorStatus(
|
|
280
|
+
const status = await getTranslatorStatus(settingsSource)
|
|
259
281
|
response.writeHead(200, {
|
|
260
282
|
'content-type': 'application/json; charset=utf-8',
|
|
261
283
|
'cache-control': 'no-store'
|
|
@@ -336,7 +358,7 @@ export function registerTranslator(wctx, settingsCtx) {
|
|
|
336
358
|
return
|
|
337
359
|
}
|
|
338
360
|
|
|
339
|
-
const settings =
|
|
361
|
+
const settings = resolveTranslatorSettings(settingsSource)
|
|
340
362
|
const engine = settings.translateEngine || 'off'
|
|
341
363
|
const localUrl = settings.localApiUrl || 'http://127.0.0.1:5000'
|
|
342
364
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-russian-lang",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"description": "Russian localization for DeepSeek Harness (DSH v0.1.7-rc.1+) web UI: adds Русский as native language option. 100.0% coverage across 11,295 keys (127 namespaces). Features full DSH v0.1.7-rc.1 core support (terminal, archive, command palette, Auto review, trajectory, PTC/Creator presets) and complete GoodAndReady plugin catalog (subscriptions, tts, image-gen, voice, key-limits, notify, commandcode, plugin-console).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|