@goodandready/dsh-russian-lang 0.2.20 → 0.2.21

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 CHANGED
@@ -40,6 +40,9 @@
40
40
 
41
41
  ---
42
42
 
43
+ > [!NOTE]
44
+ > **Языковая политика проекта**: `dsh-russian-lang` — специализированный плагин русской локализации интерфейса DSH. Каноническая документация ведётся исключительно на русском языке (`README.md`, `README.ru.md`). Общее требование трёхъязычной документации к нему не применяется.
45
+
43
46
  ## ⚡ Обзор
44
47
 
45
48
  **`dsh-russian-lang`** — официальный, всеобъемлющий пакет русской локализации для веб-интерфейса [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh).
@@ -217,5 +220,12 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
217
220
  - Мгновенная конвертация набранного транслита в кириллицу и обратно прямо в поле ввода (`privet` ⇄ `привет`).
218
221
  - *(Deprecated since v0.2.15: экспериментальный UI-перехватчик удалён для обеспечения беспрепятственного нативного ввода Lexical; программная функция `phoneticTranslit` сохранена в `lib/pure.js`).*
219
222
 
220
- 7. **Инлайн-перевод реплик ассистента (`RU ↗`)**:
221
- - Кнопка `RU ↗` в панели действий ответа ассистента переводит текст ответа на русский язык с сохранением блоков кода и выводит карточку перевода прямо под сообщением с кнопкой быстрого копирования (`📋 Копировать`).
223
+ 7. **Инлайн-перевод реплик ассистента (`RU ↗`) и приватность**:
224
+ - Кнопка `RU ↗` в панели действий ответа ассистента позволяет переводить ответы модели на русский язык с сохранением блоков кода (`📋 Копировать`).
225
+ - 🔒 **Конфиденциальность и выбор движка**:
226
+ - **По умолчанию (Выключен)**: Никакие данные сообщений никуда не отправляются. Клик по кнопке информирует о необходимости выбрать движок.
227
+ - **Локальный LibreTranslate** *(Рекомендуется)*: Работает на 100% автономно на вашем сервере в Docker-контейнере (`~500–700 МБ RAM`). Запускается в 1 клик прямо из карточки настроек DSH или вручную:
228
+ ```bash
229
+ docker run -d --name libretranslate --restart unless-stopped -p 127.0.0.1:5000:5000 libretranslate/libretranslate --load-only en,ru,zh
230
+ ```
231
+ - **Google Translate (Онлайн)**: Для пользователей, которым удобен онлайн-перевод без выделения ресурсов RAM. В настройках отображается явное предупреждение о передаче текста на публичные серверы Google.
package/README.ru.md CHANGED
@@ -40,6 +40,9 @@
40
40
 
41
41
  ---
42
42
 
43
+ > [!NOTE]
44
+ > **Языковая политика проекта**: `dsh-russian-lang` — специализированный плагин русской локализации интерфейса DSH. Каноническая документация ведётся исключительно на русском языке (`README.md`, `README.ru.md`). Общее требование трёхъязычной документации к нему не применяется.
45
+
43
46
  ## ⚡ Обзор
44
47
 
45
48
  **`dsh-russian-lang`** — официальный, всеобъемлющий пакет русской локализации для веб-интерфейса [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh).
@@ -217,5 +220,12 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
217
220
  - Мгновенная конвертация набранного транслита в кириллицу и обратно прямо в поле ввода (`privet` ⇄ `привет`).
218
221
  - *(Deprecated since v0.2.14: экспериментальный UI-перехватчик удалён для обеспечения беспрепятственного нативного ввода Lexical; программная функция `phoneticTranslit` сохранена в `lib/pure.js`).*
219
222
 
220
- 7. **Инлайн-перевод реплик ассистента (`RU ↗`)**:
221
- - Кнопка `RU ↗` в панели действий ответа ассистента переводит текст ответа на русский язык с сохранением блоков кода и выводит карточку перевода прямо под сообщением с кнопкой быстрого копирования (`📋 Копировать`).
223
+ 7. **Инлайн-перевод реплик ассистента (`RU ↗`) и приватность**:
224
+ - Кнопка `RU ↗` в панели действий ответа ассистента позволяет переводить ответы модели на русский язык с сохранением блоков кода (`📋 Копировать`).
225
+ - 🔒 **Конфиденциальность и выбор движка**:
226
+ - **По умолчанию (Выключен)**: Никакие данные сообщений никуда не отправляются. Клик по кнопке информирует о необходимости выбрать движок.
227
+ - **Локальный LibreTranslate** *(Рекомендуется)*: Работает на 100% автономно на вашем сервере в Docker-контейнере (`~500–700 МБ RAM`). Запускается в 1 клик прямо из карточки настроек DSH или вручную:
228
+ ```bash
229
+ docker run -d --name libretranslate --restart unless-stopped -p 127.0.0.1:5000:5000 libretranslate/libretranslate --load-only en,ru,zh
230
+ ```
231
+ - **Google Translate (Онлайн)**: Для пользователей, которым удобен онлайн-перевод без выделения ресурсов RAM. В настройках отображается явное предупреждение о передаче текста на публичные серверы Google.
package/lib/client.js CHANGED
@@ -522,7 +522,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
522
522
  /** zh-строка -> ru-строка для DOM-перевода панелей вне locale-ядра. */
523
523
  const ZH_RU = {}
524
524
 
525
- 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": "Поддержка русских команд: /цель -> /goal, /сжать -> /compact, /план -> /plan, /справка -> /help, /память -> /memory.", "altLHintText": "Мгновенная конвертация раскладки текущего поля (ghbdtn ⇄ привет, /vjltkm ⇄ /model). В углу поля ввода также отображается метка раскладки.", "secAgentPrompt": "🤖 Системный промпт агента", "secAgentPromptDesc": "Официальная секция расширения DSH systemPrompt для ведения диалога на русском языке.", "agentPrompt": "Русский промпт агента", "agentPromptDesc": "Добавляет в системный промпт инструкцию отвечать по-русски в выбранном стиле.", "agentPromptPreset": "Стиль ответов агента", "presetExpert": "Технический эксперт (строгая терминология, чистый код)", "presetWriter": "Технический писатель (Markdown, таблицы, ГОСТ)", "presetConcise": "Лаконичный режим (кратко, без лишней воды)", "secUpdater": "🔄 Обновление языкового пакета", "secUpdaterDesc": "Проверка наличия новых релизов в реестре npm и обновление в один клик.", "updaterCurrent": "Текущая версия: v{version}", "updaterLatest": "Доступна новая версия: v{version}", "updaterUpToDate": "Установлена актуальная версия", "updaterChecking": "Проверка…", "updaterCheckBtn": "Проверить обновления", "updaterBtn": "Обновить до v{version} в 1 клик", "updaterUpdating": "Установка обновления…", "updaterSuccess": "✅ Плагин успешно обновлён! Перезапустите DSH для применения.", "updaterFailed": "❌ Не удалось проверить/обновить плагин. Проверьте сеть или логи сервера.", "badgeUpdateAvailable": "Доступно обновление", "badgeUpToDate": "Актуальная версия", "secSupport": "📊 Покрытие экосистемы и поддержка", "secSupportDesc": "Словари синхронизированы с DSH v0.1.6-alpha.1. 100.0% UI-покрытие ядра и всех установленных плагинов (7,902 ключа) без черновых машинных переводов.", "statNamespaces": "Пространств имён", "statCoreKeys": "Ключей ядра", "statPluginKeys": "Ключей плагинов", "overridesCount": "Своих переопределений", "statusLoading": "Настройки загружаются…", "statusUnavailable": "Настройки недоступны на этом хосте", "translateTurn": "Перевести на русский", "reportIssue": "Сообщить о неточности перевода", "exportMdHint": "Экспорт диалога в Markdown доступен по кнопке [ 📥 MD ] в шапке сессии.", "translateTurnHint": "Перевод ответов ассистента на русский доступен по кнопке [ RU ↗ ] на блоках сообщений."}
525
+ 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": "Поддержка русских команд: /цель -> /goal, /сжать -> /compact, /план -> /plan, /справка -> /help, /память -> /memory.", "altLHintText": "Мгновенная конвертация раскладки текущего поля (ghbdtn ⇄ привет, /vjltkm ⇄ /model). В углу поля ввода также отображается метка раскладки.", "secAgentPrompt": "🤖 Системный промпт агента", "secAgentPromptDesc": "Официальная секция расширения DSH systemPrompt для ведения диалога на русском языке.", "agentPrompt": "Русский промпт агента", "agentPromptDesc": "Добавляет в системный промпт инструкцию отвечать по-русски в выбранном стиле.", "agentPromptPreset": "Стиль ответов агента", "presetExpert": "Технический эксперт (строгая терминология, чистый код)", "presetWriter": "Технический писатель (Markdown, таблицы, ГОСТ)", "presetConcise": "Лаконичный режим (кратко, без лишней воды)", "secUpdater": "🔄 Обновление языкового пакета", "secUpdaterDesc": "Проверка наличия новых релизов в реестре npm и обновление в один клик.", "updaterCurrent": "Текущая версия: v{version}", "updaterLatest": "Доступна новая версия: v{version}", "updaterUpToDate": "Установлена актуальная версия", "updaterChecking": "Проверка…", "updaterCheckBtn": "Проверить обновления", "updaterBtn": "Обновить до v{version} в 1 клик", "updaterUpdating": "Установка обновления…", "updaterSuccess": "✅ Плагин успешно обновлён! Перезапустите DSH для применения.", "updaterFailed": "❌ Не удалось проверить/обновить плагин. Проверьте сеть или логи сервера.", "badgeUpdateAvailable": "Доступно обновление", "badgeUpToDate": "Актуальная версия", "secTranslator": "🌐 Перевод сообщений ассистента", "secTranslatorDesc": "Настройка перевода ответов модели в чате на русский язык (локально в Docker или онлайн).", "translateEngine": "Движок перевода", "engineOff": "Выключен (по умолчанию, без сетевых вызовов)", "engineLocal": "Локальный LibreTranslate (приватно, ~600 МБ RAM)", "engineGoogle": "Google Translate (онлайн)", "googleWarn": "⚠️ Внимание: при онлайн-переводе текст сообщений ассистента передаётся на публичные серверы Google для перевода на лету. Не используйте для конфиденциальных данных.", "localStatusRunning": "🟢 Контейнер LibreTranslate активен", "localStatusStopped": "⚪ Контейнер LibreTranslate не запущен", "localStartBtn": "Запустить LibreTranslate в Docker (~600 МБ RAM)", "localStarting": "Запуск контейнера…", "localInfo": "LibreTranslate запускается изолированно в Docker на сервере, потребляет ~500–700 МБ RAM и переводит на 100% локально.", "secSupport": "📊 Покрытие экосистемы и поддержка", "secSupportDesc": "Словари синхронизированы с DSH v0.1.6-alpha.1. 100.0% UI-покрытие ядра и всех установленных плагинов (7,902 ключа) без черновых машинных переводов.", "statNamespaces": "Пространств имён", "statCoreKeys": "Ключей ядра", "statPluginKeys": "Ключей плагинов", "overridesCount": "Своих переопределений", "statusLoading": "Настройки загружаются…", "statusUnavailable": "Настройки недоступны на этом хосте", "translateTurn": "Перевести на русский", "reportIssue": "Сообщить о неточности перевода", "exportMdHint": "Экспорт диалога в Markdown доступен по кнопке [ 📥 MD ] в шапке сессии.", "translateTurnHint": "Перевод ответов ассистента на русский доступен по кнопке [ RU ↗ ] на блоках сообщений."}
526
526
 
527
527
  const SETTINGS_NS_NAME = 'russian-lang'
528
528
 
@@ -1505,62 +1505,21 @@ const exportSessionToMarkdown = (session, options = {}) => {
1505
1505
  }
1506
1506
 
1507
1507
  async function translateTurnContent(text) {
1508
- if (!text || typeof text !== 'string') return ''
1509
- const cyr = (text.match(/[\u0430-\u044f\u0451]/gi) || []).length
1510
- const lat = (text.match(/[a-z]/gi) || []).length
1511
- if (cyr > lat && cyr > 15) return text
1512
-
1513
- const parts = text.split(/(```[\s\S]*?```)/g)
1514
- for (let i = 0; i < parts.length; i += 2) {
1515
- const chunk = parts[i].trim()
1516
- if (!chunk) continue
1517
- const paragraphs = chunk.split(/\n\n+/)
1518
- const translatedParas = []
1519
- for (const para of paragraphs) {
1520
- const pTrim = para.trim()
1521
- if (!pTrim) continue
1522
-
1523
- const sentences = pTrim.match(/[^.!?\n]+[.!?\n]*/g) || [pTrim]
1524
- const translatedSentences = []
1525
-
1526
- for (const s of sentences) {
1527
- let translated = ''
1528
-
1529
- try {
1530
- const gurl = 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=ru&dt=t&q=' + encodeURIComponent(s)
1531
- const gres = await fetch(gurl)
1532
- if (gres.ok) {
1533
- const gdata = await gres.json()
1534
- if (Array.isArray(gdata) && Array.isArray(gdata[0])) {
1535
- translated = gdata[0].map((it) => it[0]).join('')
1536
- }
1537
- }
1538
- } catch (e1) {}
1539
-
1540
- if (!translated) {
1541
- try {
1542
- const isZh = /[\u3400-\u9fff]/.test(s)
1543
- const langPair = isZh ? 'zh-CN|ru' : 'en|ru'
1544
- const murl = 'https://api.mymemory.translated.net/get?q=' + encodeURIComponent(s.slice(0, 450)) + '&langpair=' + langPair
1545
- const mres = await fetch(murl)
1546
- if (mres.ok) {
1547
- const mdata = await mres.json()
1548
- if (mdata && mdata.responseData && mdata.responseData.translatedText) {
1549
- const txtEl = document.createElement('textarea')
1550
- txtEl.innerHTML = mdata.responseData.translatedText
1551
- translated = txtEl.value
1552
- }
1553
- }
1554
- } catch (e2) {}
1555
- }
1556
-
1557
- translatedSentences.push(translated || s)
1558
- }
1559
- translatedParas.push(translatedSentences.join(' '))
1508
+ if (!text || typeof text !== 'string') return { error: 'Текст для перевода пуст.' }
1509
+ try {
1510
+ const res = await fetch('/api/dsh-russian-lang/translate', {
1511
+ method: 'POST',
1512
+ headers: { 'content-type': 'application/json' },
1513
+ body: JSON.stringify({ text })
1514
+ })
1515
+ const data = await res.json().catch(() => ({}))
1516
+ if (!res.ok) {
1517
+ return { error: data.error || ('Ошибка перевода (HTTP ' + res.status + ')') }
1560
1518
  }
1561
- parts[i] = translatedParas.join('\n\n')
1519
+ return { translatedText: data.translatedText || text }
1520
+ } catch (err) {
1521
+ return { error: 'Не удалось связаться с хостом DSH: ' + (err.message || err) }
1562
1522
  }
1563
- return parts.join('\n\n')
1564
1523
  }
1565
1524
 
1566
1525
  function TranslateTurnAction(props) {
@@ -1613,8 +1572,42 @@ const exportSessionToMarkdown = (session, options = {}) => {
1613
1572
  })
1614
1573
  }
1615
1574
 
1575
+ const [transStatus, setTransStatus] = React.useState(null)
1576
+ const [transStarting, setTransStarting] = React.useState(false)
1577
+ const [transMsg, setTransMsg] = React.useState(null)
1578
+
1579
+ const fetchTransStatus = () => {
1580
+ fetch('/api/dsh-russian-lang/translator/status')
1581
+ .then((r) => r.json())
1582
+ .then((data) => setTransStatus(data))
1583
+ .catch(() => setTransStatus(null))
1584
+ }
1585
+
1586
+ const startLibreTranslate = () => {
1587
+ setTransStarting(true)
1588
+ setTransMsg(null)
1589
+ fetch('/api/dsh-russian-lang/translator/setup', { method: 'POST' })
1590
+ .then((r) => r.json())
1591
+ .then((data) => {
1592
+ setTransStarting(false)
1593
+ if (data.ok) {
1594
+ setTransMsg({ type: 'ok', text: data.message || 'Контейнер LibreTranslate запущен.' })
1595
+ fetchTransStatus()
1596
+ } else {
1597
+ setTransMsg({ type: 'err', text: data.error || 'Ошибка запуска контейнера.' })
1598
+ }
1599
+ })
1600
+ .catch((err) => {
1601
+ setTransStarting(false)
1602
+ setTransMsg({ type: 'err', text: 'Ошибка: ' + (err.message || err) })
1603
+ })
1604
+ }
1605
+
1616
1606
  React.useEffect(() => {
1617
- if (open) checkUpdate()
1607
+ if (open) {
1608
+ checkUpdate()
1609
+ fetchTransStatus()
1610
+ }
1618
1611
  }, [open])
1619
1612
 
1620
1613
 
@@ -1676,29 +1669,36 @@ const exportSessionToMarkdown = (session, options = {}) => {
1676
1669
  if (prose) rawText = prose.innerText.trim()
1677
1670
  }
1678
1671
 
1679
- const translated = rawText ? await translateTurnContent(rawText) : 'Не удалось обнаружить текст сообщения ассистента для перевода.'
1672
+ let transResult = { error: 'Не удалось обнаружить текст сообщения ассистента для перевода.' }
1673
+ if (rawText) {
1674
+ transResult = await translateTurnContent(rawText)
1675
+ }
1676
+ const isErr = !!transResult.error
1677
+ const displayText = transResult.translatedText || transResult.error || ''
1680
1678
 
1681
1679
  box = document.createElement('div')
1682
1680
  box.className = 'rl-turn-translation'
1683
1681
  const key = tailFlowItem.getAttribute('data-chat-flow-key')
1684
1682
  if (key) box.dataset.tailKey = key
1685
1683
  box.innerHTML = '<div class="rl-trans-head">' +
1686
- '<span class="rl-trans-title">🌐 Перевод на русский</span>' +
1684
+ '<span class="rl-trans-title">' + (isErr ? '⚠️ Машинный перевод' : '🌐 Перевод на русский') + '</span>' +
1687
1685
  '<div class="rl-trans-tools">' +
1688
- '<button type="button" class="rl-trans-btn rl-btn-copy" title="Скопировать перевод">📋 Копировать</button>' +
1686
+ (!isErr ? '<button type="button" class="rl-trans-btn rl-btn-copy" title="Скопировать перевод">📋 Копировать</button>' : '') +
1689
1687
  '<button type="button" class="rl-trans-btn rl-btn-close" title="Закрыть">✕</button>' +
1690
1688
  '</div>' +
1691
1689
  '</div>' +
1692
- '<div class="rl-trans-body"></div>'
1693
- box.querySelector('.rl-trans-body').textContent = translated
1690
+ '<div class="rl-trans-body"' + (isErr ? ' style="color: var(--dsw-alias-color-warning, #eab308); font-size: 13px;"' : '') + '></div>'
1691
+ box.querySelector('.rl-trans-body').textContent = displayText
1694
1692
 
1695
1693
  const copyBtn = box.querySelector('.rl-btn-copy')
1696
- copyBtn.addEventListener('click', (e) => {
1697
- e.stopPropagation()
1698
- try { navigator.clipboard.writeText(translated) } catch (err) {}
1699
- copyBtn.textContent = '✓ Скопировано'
1700
- setTimeout(() => { copyBtn.textContent = '📋 Копировать' }, 2000)
1701
- })
1694
+ if (copyBtn) {
1695
+ copyBtn.addEventListener('click', (e) => {
1696
+ e.stopPropagation()
1697
+ try { navigator.clipboard.writeText(displayText) } catch (err) {}
1698
+ copyBtn.textContent = '✓ Скопировано'
1699
+ setTimeout(() => { copyBtn.textContent = '📋 Копировать' }, 2000)
1700
+ })
1701
+ }
1702
1702
 
1703
1703
  const closeBtn = box.querySelector('.rl-btn-close')
1704
1704
  closeBtn.addEventListener('click', (e) => {
@@ -1999,6 +1999,66 @@ const exportSessionToMarkdown = (session, options = {}) => {
1999
1999
  )
2000
2000
  ),
2001
2001
 
2002
+ React.createElement('div', { className: 'rl-section-card' },
2003
+ React.createElement('div', { className: 'rl-section-title' },
2004
+ React.createElement('span', null, t('secTranslator')),
2005
+ React.createElement('span', {
2006
+ className: 'rl-badge ' + (value.translateEngine === 'local' ? 'rl-badge-ok' : (value.translateEngine === 'google' ? 'rl-badge-warn' : 'rl-badge-dim'))
2007
+ }, value.translateEngine === 'local' ? 'Локально' : (value.translateEngine === 'google' ? 'Google' : 'Выключен'))
2008
+ ),
2009
+ React.createElement('div', { className: 'rl-section-desc' }, t('secTranslatorDesc')),
2010
+ React.createElement('div', { className: 'rl-item-card' },
2011
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
2012
+ React.createElement('label', { className: 'rl-item-label', style: { fontWeight: 500 } }, t('translateEngine')),
2013
+ React.createElement('select', {
2014
+ className: 'rl-select',
2015
+ value: value.translateEngine || 'off',
2016
+ onChange: (ev) => {
2017
+ try {
2018
+ const val = ev.target.value
2019
+ scope.set('translateEngine', val)
2020
+ if (val === 'local') fetchTransStatus()
2021
+ } catch (err) { console.warn('dsh-russian-lang: set translateEngine failed', err) }
2022
+ }
2023
+ },
2024
+ React.createElement('option', { value: 'off' }, t('engineOff')),
2025
+ React.createElement('option', { value: 'local' }, t('engineLocal')),
2026
+ React.createElement('option', { value: 'google' }, t('engineGoogle'))
2027
+ )
2028
+ ),
2029
+ value.translateEngine === 'google' ? React.createElement('div', {
2030
+ style: {
2031
+ marginTop: '10px', padding: '8px 12px', borderRadius: '8px',
2032
+ background: 'rgba(234, 179, 8, 0.12)', border: '1px solid rgba(234, 179, 8, 0.3)',
2033
+ color: 'var(--dsw-alias-color-warning, #eab308)', fontSize: '12px', fontWeight: 500, lineHeight: 1.4
2034
+ }
2035
+ }, t('googleWarn')) : null,
2036
+ value.translateEngine === 'local' ? React.createElement('div', {
2037
+ style: { marginTop: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }
2038
+ },
2039
+ React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, t('localInfo')),
2040
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
2041
+ React.createElement('span', {
2042
+ className: 'rl-badge ' + (transStatus && transStatus.running ? 'rl-badge-ok' : 'rl-badge-dim')
2043
+ }, transStatus && transStatus.running ? t('localStatusRunning') : t('localStatusStopped')),
2044
+ (!transStatus || !transStatus.running) ? React.createElement('button', {
2045
+ type: 'button',
2046
+ className: 'rl-btn rl-btn-primary',
2047
+ disabled: transStarting,
2048
+ onClick: startLibreTranslate
2049
+ }, transStarting ? t('localStarting') : t('localStartBtn')) : null
2050
+ ),
2051
+ transMsg ? React.createElement('div', {
2052
+ style: {
2053
+ marginTop: '6px', padding: '8px 12px', borderRadius: '8px',
2054
+ background: transMsg.type === 'ok' ? 'rgba(34, 197, 94, 0.12)' : 'rgba(239, 68, 68, 0.12)',
2055
+ color: transMsg.type === 'ok' ? '#16a34a' : '#dc2626', fontSize: '12px', fontWeight: 500
2056
+ }
2057
+ }, transMsg.text) : null
2058
+ ) : null
2059
+ )
2060
+ ),
2061
+
2002
2062
  React.createElement('div', { className: 'rl-section-card' },
2003
2063
  React.createElement('div', { className: 'rl-section-title' },
2004
2064
  React.createElement('span', null, t('secSupport')),
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { registerPluginUpdater } from './plugin-updater.js'
2
+ import { registerTranslator } from './translator.js'
2
3
  import { getPluginDictionaries, getZhRuMap } from './locales.js'
3
4
  // dsh-russian-lang — серверная половина.
4
5
  //
@@ -22,11 +23,15 @@ const RussianLangSettingsSchema = z.object({
22
23
  agentPrompt: z.boolean().required(false),
23
24
  agentPromptPreset: z.string().required(false),
24
25
  slashAliases: z.boolean().required(false),
25
- quickSwitch: z.boolean().required(false)
26
+ quickSwitch: z.boolean().required(false),
27
+ translateEngine: z.string().required(false),
28
+ localApiUrl: z.string().required(false)
26
29
  })
27
30
 
28
31
  export function apply(ctx) {
32
+ let globalSettingsCtx = null
29
33
  ctx.inject(['settings'], (settingsCtx) => {
34
+ globalSettingsCtx = settingsCtx
30
35
  settingsCtx.settings.register('russian-lang', RussianLangSettingsSchema)
31
36
  })
32
37
 
@@ -52,7 +57,7 @@ export function apply(ctx) {
52
57
  sync()
53
58
  })
54
59
 
55
- // Web endpoints: One-click plugin updater & modular plugin dictionary serving
60
+ // Web endpoints: One-click plugin updater, modular plugin dictionary serving, & translator gateway
56
61
  if (typeof ctx.inject === 'function') {
57
62
  ctx.inject(['webServer'], (wctx) => {
58
63
  try {
@@ -63,6 +68,8 @@ export function apply(ctx) {
63
68
  manifestUrl: new URL('../package.json', import.meta.url),
64
69
  })
65
70
 
71
+ const unregTranslator = registerTranslator(wctx, globalSettingsCtx)
72
+
66
73
  let ws = null
67
74
  try { ws = wctx?.webServer } catch (_) { ws = null }
68
75
 
@@ -90,6 +97,7 @@ export function apply(ctx) {
90
97
 
91
98
  return () => {
92
99
  if (typeof unregUpdater === 'function') unregUpdater()
100
+ if (typeof unregTranslator === 'function') unregTranslator()
93
101
  if (typeof unregDict === 'function') unregDict()
94
102
  }
95
103
  }
@@ -0,0 +1,303 @@
1
+ import { execFile } from 'node:child_process'
2
+
3
+ /**
4
+ * Host-side translation services & LibreTranslate integration
5
+ * for @goodandready/dsh-russian-lang.
6
+ */
7
+
8
+ function execPromise(cmd, args, timeoutMs = 15000) {
9
+ return new Promise((resolve, reject) => {
10
+ execFile(cmd, args, { timeout: timeoutMs }, (err, stdout, stderr) => {
11
+ if (err) return reject(new Error(stderr?.trim() || err.message))
12
+ resolve(stdout?.trim() || '')
13
+ })
14
+ })
15
+ }
16
+
17
+ function readJsonBody(request, limit = 500_000) {
18
+ return new Promise((resolve, reject) => {
19
+ let body = ''
20
+ request.on('data', (chunk) => {
21
+ body += chunk
22
+ if (body.length > limit) {
23
+ reject(new Error('Payload too large'))
24
+ }
25
+ })
26
+ request.on('end', () => {
27
+ try {
28
+ resolve(body ? JSON.parse(body) : {})
29
+ } catch (err) {
30
+ reject(err)
31
+ }
32
+ })
33
+ request.on('error', reject)
34
+ })
35
+ }
36
+
37
+ export async function getTranslatorStatus(settingsCtx) {
38
+ const currentSettings = settingsCtx?.settings?.get('russian-lang') || {}
39
+ const engine = currentSettings.translateEngine || 'off'
40
+ const localUrl = (currentSettings.localApiUrl || 'http://127.0.0.1:5000').replace(/\/+$/, '')
41
+
42
+ let dockerAvailable = false
43
+ let installed = false
44
+ let running = false
45
+ let responsive = false
46
+
47
+ try {
48
+ const statusOut = await execPromise('docker', [
49
+ 'ps', '-a', '--filter', 'name=^libretranslate$', '--format', '{{.Status}}'
50
+ ], 3000)
51
+ dockerAvailable = true
52
+ if (statusOut) {
53
+ installed = true
54
+ running = statusOut.toLowerCase().startsWith('up')
55
+ }
56
+ } catch (_) {
57
+ dockerAvailable = false
58
+ }
59
+
60
+ // Ping HTTP endpoint if running or local
61
+ try {
62
+ const res = await fetch(`${localUrl}/languages`, { signal: AbortSignal.timeout(1500) })
63
+ if (res.ok) responsive = true
64
+ } catch (_) {}
65
+
66
+ return {
67
+ engine,
68
+ localUrl,
69
+ dockerAvailable,
70
+ installed,
71
+ running,
72
+ responsive,
73
+ memory: '~500–700 МБ RAM',
74
+ recommended: 'libretranslate (CTranslate2 / int8)'
75
+ }
76
+ }
77
+
78
+ export async function setupLibreTranslateContainer() {
79
+ // Check if container exists
80
+ try {
81
+ const nameOut = await execPromise('docker', [
82
+ 'ps', '-a', '--filter', 'name=^libretranslate$', '--format', '{{.Names}}'
83
+ ], 3000)
84
+ if (nameOut && nameOut.trim() === 'libretranslate') {
85
+ // Start stopped container
86
+ await execPromise('docker', ['start', 'libretranslate'], 10000)
87
+ return { ok: true, action: 'started', message: 'Контейнер libretranslate запущен.' }
88
+ }
89
+ } catch (err) {
90
+ throw new Error('Docker недоступен или нет прав на выполнение: ' + err.message)
91
+ }
92
+
93
+ // Run new container
94
+ try {
95
+ await execPromise('docker', [
96
+ 'run', '-d',
97
+ '--name', 'libretranslate',
98
+ '--restart', 'unless-stopped',
99
+ '-p', '127.0.0.1:5000:5000',
100
+ 'libretranslate/libretranslate',
101
+ '--load-only', 'en,ru,zh'
102
+ ], 30000)
103
+ return { ok: true, action: 'created', message: 'Контейнер libretranslate создан и запускается (~600 МБ RAM).' }
104
+ } catch (err) {
105
+ throw new Error('Ошибка запуска контейнера libretranslate: ' + err.message)
106
+ }
107
+ }
108
+
109
+ async function translateGoogle(text, targetLang = 'ru') {
110
+ const parts = text.split(/(```[\s\S]*?```)/g)
111
+ const resultParts = []
112
+ for (let i = 0; i < parts.length; i++) {
113
+ if (i % 2 === 1) {
114
+ resultParts.push(parts[i])
115
+ continue
116
+ }
117
+ const chunk = parts[i]
118
+ if (!chunk.trim()) {
119
+ resultParts.push(chunk)
120
+ continue
121
+ }
122
+ const paragraphs = chunk.split(/\n\n+/)
123
+ const transParas = []
124
+ for (const para of paragraphs) {
125
+ const pTrim = para.trim()
126
+ if (!pTrim) continue
127
+ const sentences = pTrim.match(/[^.!?\n]+[.!?\n]*/g) || [pTrim]
128
+ const transSents = []
129
+ for (const s of sentences) {
130
+ if (!s.trim()) continue
131
+ const gurl = 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=' + encodeURIComponent(targetLang) + '&dt=t&q=' + encodeURIComponent(s)
132
+ try {
133
+ const res = await fetch(gurl, { signal: AbortSignal.timeout(6000) })
134
+ if (res.ok) {
135
+ const data = await res.json()
136
+ if (Array.isArray(data) && Array.isArray(data[0])) {
137
+ transSents.push(data[0].map((it) => it[0]).join(''))
138
+ continue
139
+ }
140
+ }
141
+ } catch (_) {}
142
+ transSents.push(s)
143
+ }
144
+ transParas.push(transSents.join(''))
145
+ }
146
+ resultParts.push(transParas.join('\n\n'))
147
+ }
148
+ return resultParts.join('')
149
+ }
150
+
151
+ async function translateLibre(text, targetLang = 'ru', localUrl = 'http://127.0.0.1:5000') {
152
+ const endpoint = localUrl.replace(/\/+$/, '') + '/translate'
153
+ const res = await fetch(endpoint, {
154
+ method: 'POST',
155
+ headers: { 'content-type': 'application/json' },
156
+ body: JSON.stringify({
157
+ q: text,
158
+ source: 'auto',
159
+ target: targetLang,
160
+ format: 'text'
161
+ }),
162
+ signal: AbortSignal.timeout(15000)
163
+ })
164
+ if (!res.ok) {
165
+ const errText = await res.text().catch(() => '')
166
+ throw new Error(`LibreTranslate HTTP ${res.status}: ${errText}`)
167
+ }
168
+ const data = await res.json()
169
+ return data.translatedText || text
170
+ }
171
+
172
+ export function registerTranslator(wctx, settingsCtx) {
173
+ let ws = null
174
+ try { ws = wctx?.webServer } catch (_) { ws = null }
175
+ if (!ws || typeof ws.register !== 'function') return () => {}
176
+
177
+ const unregs = []
178
+
179
+ // 1. Status endpoint: GET /api/dsh-russian-lang/translator/status
180
+ unregs.push(
181
+ ws.register({
182
+ kind: 'exact',
183
+ path: '/api/dsh-russian-lang/translator/status',
184
+ handler: async (request, response) => {
185
+ try {
186
+ if (request.method !== 'GET') {
187
+ response.writeHead(405, { 'content-type': 'application/json' })
188
+ response.end(JSON.stringify({ error: 'Method not allowed' }))
189
+ return
190
+ }
191
+ const status = await getTranslatorStatus(settingsCtx)
192
+ response.writeHead(200, {
193
+ 'content-type': 'application/json; charset=utf-8',
194
+ 'cache-control': 'no-store'
195
+ })
196
+ response.end(JSON.stringify(status))
197
+ } catch (err) {
198
+ response.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
199
+ response.end(JSON.stringify({ error: String(err) }))
200
+ }
201
+ }
202
+ })
203
+ )
204
+
205
+ // 2. Setup endpoint: POST /api/dsh-russian-lang/translator/setup
206
+ unregs.push(
207
+ ws.register({
208
+ kind: 'exact',
209
+ path: '/api/dsh-russian-lang/translator/setup',
210
+ handler: async (request, response) => {
211
+ try {
212
+ if (request.method !== 'POST') {
213
+ response.writeHead(405, { 'content-type': 'application/json' })
214
+ response.end(JSON.stringify({ error: 'Method not allowed' }))
215
+ return
216
+ }
217
+ const result = await setupLibreTranslateContainer()
218
+ response.writeHead(200, {
219
+ 'content-type': 'application/json; charset=utf-8',
220
+ 'cache-control': 'no-store'
221
+ })
222
+ response.end(JSON.stringify(result))
223
+ } catch (err) {
224
+ response.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
225
+ response.end(JSON.stringify({ error: String(err) }))
226
+ }
227
+ }
228
+ })
229
+ )
230
+
231
+ // 3. Translation gateway: POST /api/dsh-russian-lang/translate
232
+ unregs.push(
233
+ ws.register({
234
+ kind: 'exact',
235
+ path: '/api/dsh-russian-lang/translate',
236
+ handler: async (request, response) => {
237
+ try {
238
+ if (request.method !== 'POST') {
239
+ response.writeHead(405, { 'content-type': 'application/json' })
240
+ response.end(JSON.stringify({ error: 'Method not allowed' }))
241
+ return
242
+ }
243
+
244
+ const body = await readJsonBody(request)
245
+ const text = body.text
246
+ const target = body.target || 'ru'
247
+
248
+ if (!text || typeof text !== 'string') {
249
+ response.writeHead(400, { 'content-type': 'application/json; charset=utf-8' })
250
+ response.end(JSON.stringify({ error: 'Параметр "text" обязателен для перевода.' }))
251
+ return
252
+ }
253
+
254
+ const settings = settingsCtx?.settings?.get('russian-lang') || {}
255
+ const engine = settings.translateEngine || 'off'
256
+ const localUrl = settings.localApiUrl || 'http://127.0.0.1:5000'
257
+
258
+ if (engine === 'off') {
259
+ response.writeHead(403, { 'content-type': 'application/json; charset=utf-8' })
260
+ response.end(JSON.stringify({
261
+ error: 'Машинный перевод отключен в настройках плагина. Включите локальный сервер (LibreTranslate) или Google Translate в настройках dsh-russian-lang.'
262
+ }))
263
+ return
264
+ }
265
+
266
+ let translatedText = ''
267
+ if (engine === 'local') {
268
+ try {
269
+ translatedText = await translateLibre(text, target, localUrl)
270
+ } catch (err) {
271
+ response.writeHead(503, { 'content-type': 'application/json; charset=utf-8' })
272
+ response.end(JSON.stringify({
273
+ error: 'Локальный переводчик LibreTranslate недоступен (' + err.message + '). Проверьте состояние контейнера в настройках плагина.'
274
+ }))
275
+ return
276
+ }
277
+ } else if (engine === 'google') {
278
+ translatedText = await translateGoogle(text, target)
279
+ } else {
280
+ response.writeHead(400, { 'content-type': 'application/json; charset=utf-8' })
281
+ response.end(JSON.stringify({ error: 'Неизвестный движок перевода: ' + engine }))
282
+ return
283
+ }
284
+
285
+ response.writeHead(200, {
286
+ 'content-type': 'application/json; charset=utf-8',
287
+ 'cache-control': 'no-store'
288
+ })
289
+ response.end(JSON.stringify({ translatedText, engine }))
290
+ } catch (err) {
291
+ response.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
292
+ response.end(JSON.stringify({ error: String(err) }))
293
+ }
294
+ }
295
+ })
296
+ )
297
+
298
+ return () => {
299
+ for (const unreg of unregs) {
300
+ try { unreg() } catch (_) {}
301
+ }
302
+ }
303
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-russian-lang",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "Russian localization for DeepSeek Harness (DSH v0.1.6-alpha.1+) web UI: adds Русский as native language option. 100.0% coverage across 11,295 keys (127 namespaces). Features full DSH v0.1.6-alpha.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",
@@ -15,7 +15,6 @@
15
15
  "cordis.patch.yml",
16
16
  "README.md",
17
17
  "README.ru.md",
18
- "README.zh.md",
19
18
  "LICENSE"
20
19
  ],
21
20
  "dsh": {
package/README.zh.md DELETED
@@ -1,102 +0,0 @@
1
- # 📦 @goodandready/dsh-russian-lang
2
-
3
- <div align="center">
4
-
5
- <h3>DeepSeek Harness 完整俄语本地化、智能排版与键盘布局自动修复插件</h3>
6
-
7
- <p align="center">
8
- <a href="https://www.npmjs.com/package/@goodandready/dsh-russian-lang"><img src="https://img.shields.io/badge/npm-v0.2.20-6366f1.svg?style=for-the-badge&labelColor=1e1b4b" alt="npm version"></a>
9
- <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
10
- <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
11
- <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
12
- </p>
13
-
14
- <p align="center">
15
- <img src="https://img.shields.io/badge/DSH-v0.1.5--rc.2%2B-blue.svg?style=for-the-badge&labelColor=1e1b4b" alt="DSH v0.1.6-alpha.1+">
16
- <img src="https://img.shields.io/badge/覆盖率-100%25-10b981.svg?style=for-the-badge&labelColor=064e3b" alt="覆盖率 100%">
17
- <img src="https://img.shields.io/badge/UI词条-11295-6366f1.svg?style=for-the-badge&labelColor=1e1b4b" alt="11295 词条">
18
- <img src="https://img.shields.io/badge/人工校验-100%25-0ea5e9.svg?style=for-the-badge&labelColor=082f49" alt="100% 人工校验">
19
- </p>
20
-
21
- <p align="center">
22
- <a href="https://goodandready.app/"><img src="https://img.shields.io/badge/作者所有项目-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="作者所有项目"></a>
23
- </p>
24
-
25
- <table align="center">
26
- <tr>
27
- <td align="center">
28
- ⭐ <strong>如果您喜欢这个插件,请在 GitHub 上点个 Star</strong> —— 这能让我知道该插件对您有所帮助,并激励我持续维护和演进。
29
- <br><br>
30
- 🐛 <strong>如果您发现了 Bug 或有任何建议</strong>,欢迎随时在 GitHub 提交 Issue(支持中文、俄文或英文)。
31
- </td>
32
- </tr>
33
- </table>
34
-
35
- </div>
36
-
37
- ---
38
-
39
- ## ⚡ 概述
40
-
41
- **`dsh-russian-lang`** 是面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) Web 界面的全功能俄语本地化增强插件。
42
-
43
- 本插件不仅覆盖 DSH 核心及社区 70+ 个主流扩展插件的俄语翻译,更针对俄语用户深度定制了智能化人机交互特性:**100% 专家人工审校的技术术语**、**俄语复数语法形式(Plural)**、**屏幕排版自动规范化**(书名号《》、长破折号、不换行空格),以及通过快捷键 <kbd>Alt+L</kbd> 实现的**错误键盘布局文本一键纠正**。
44
-
45
- ---
46
-
47
- ## ✨ 核心特性
48
-
49
- ### 1. 🌐 100% 覆盖 DSH 核心 (45+ 命名空间,1780+ 词条)
50
- 完全覆盖终端、会话归档、命令面板、工作区、右侧边栏、模型设置等所有 DSH 原生功能模块。
51
-
52
- ### 2. 🧩 内置 70+ 社区插件模块化翻译 (9500+ 词条)
53
- 采用按需模块化分发架构(单文件 < 50 KiB),包含市场、看板、定时任务、语音 TTS、模型限额、会话控制等全部热门扩展插件。
54
-
55
- ### 3. 🔢 俄语复数语法精确匹配 (Plural)
56
- 自动计算俄语特有的数词变格规则(`one`、`few`、`many`),彻底避免机械英汉翻译中的语法违和感。
57
-
58
- ### 4. ✍️ 智能输入排版增强 (`russian-lang.typography`)
59
- 在输入和渲染时规范排版格式,智能识别单字母连词并在其后插入不换行空格,且绝对保留 Markdown 代码块和行内代码语法不变。
60
-
61
- ### 5. ⌨️ 误打布局实时纠正 (<kbd>Alt+L</kbd>)
62
- 实时检测英俄误打输入(例如错输入 `ghbdtn` 自动识别为 `привет`),按快捷键即可就地无缝纠正。
63
-
64
- ### 6. ⚙️ 自定义键值覆写 (`russian-lang.overrides`)
65
- 支持在 **设置 → 插件 → 俄语本地化** 中自定义特定术语翻译,无需修改任何源代码。
66
-
67
- ### 7. 🔤 DSH 原生架构集成
68
- 原生注册至 **设置 → 通用 → 语言** 菜单,无缝切换 `<html lang="ru-RU">` 并自动激活浏览器拼写检查。
69
-
70
- ---
71
-
72
- ## 📦 安装方法
73
-
74
- 在终端运行以下命令:
75
-
76
- ```bash
77
- dsh plugin --profile web add @goodandready/dsh-russian-lang
78
- ```
79
-
80
- 安装完成后,打开 DSH 网页端,前往 **设置 (Settings) → 通用 (General) → 语言 (Language)**,选择 **«Русский»** 即可。
81
-
82
- ---
83
-
84
- ## ⚙️ 配置说明 (`settings.yaml`)
85
-
86
- 配置命名空间为 `russian-lang`,所有选项均为可选字段:
87
-
88
- ```yaml
89
- russian-lang:
90
- enabled: true # 启用俄语界面
91
- typography:
92
- enabled: true # 启用智能排版(引号、破折号、不换行空格)
93
- yo: false # 自动还原字母 «ё»(默认关闭)
94
- agentPrompt: false # 提示智能体优先使用俄语回复
95
- overrides: {} # 针对任意词条的自定义翻译映射
96
- ```
97
-
98
- ---
99
-
100
- ## 📄 开源许可
101
-
102
- MIT License © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)