@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 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(SETTINGS_NS_NAME)
400
- if (c && c.settingsScope) return c.settingsScope.bind({ namespace: SETTINGS_NS_NAME })
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 = () => {try {const wantRu = runtime.getLocale().active === 'ru'
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.14' })
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.14',latestVersion: undefined,updateAvailable: false
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.14')),upStatus.updateAvailable
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.14'),target: '_blank',rel: 'noopener noreferrer',className: 'rl-btn rl-btn-primary'
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)}'
@@ -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(SETTINGS_NS_NAME)
44
- if (c && c.settingsScope) return c.settingsScope.bind({ namespace: SETTINGS_NS_NAME })
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
- const RussianLangSettingsSchema = z.object({
43
- enabled: z.boolean().required(false),
44
- overrides: z.dict(z.string()).required(false),
45
- typography: z.object({
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
- export function apply(ctx) {
59
- let globalSettingsCtx = null
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
- globalSettingsCtx = settingsCtx
62
- settingsCtx.settings.register('russian-lang', RussianLangSettingsSchema)
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
- ctx.inject(['settings', 'systemPrompt'], (settingsCtx, prompt) => {
69
- const NS = 'russian-lang'
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
- const value = settingsCtx.settings.get(NS)
75
- const want = !!(value && value.agentPrompt)
76
- const presetKey = value && value.agentPromptPreset
77
- const preset = SYSTEM_PROMPT_PRESETS[presetKey]
78
- const text = want ? (preset ? preset.text : DEFAULT_RU_TEXT) : ''
79
- if (text === currentText) return
80
- currentText = text
81
- prompt.section({ name: RU_SECTION, order: 1000, text })
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('settings/updated', (ns) => { if (ns === NS) sync() })
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, globalSettingsCtx)
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
- export async function getTranslatorStatus(settingsCtx) {
105
- const currentSettings = settingsCtx?.settings?.get('russian-lang') || {}
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, settingsCtx) {
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(settingsCtx)
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 = settingsCtx?.settings?.get('russian-lang') || {}
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.14",
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",