@goodandready/dsh-russian-lang 0.2.20 → 0.2.22

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
@@ -5,7 +5,7 @@
5
5
  <h3>Полная русская локализация, умная типографика и исправление раскладки клавиатуры для DeepSeek Harness</h3>
6
6
 
7
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>
8
+ <a href="https://www.npmjs.com/package/@goodandready/dsh-russian-lang"><img src="https://img.shields.io/badge/npm-v0.2.22-6366f1.svg?style=for-the-badge&labelColor=1e1b4b" alt="npm version"></a>
9
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
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
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>
@@ -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
@@ -5,7 +5,7 @@
5
5
  <h3>Полная русская локализация, умная типографика и исправление раскладки клавиатуры для DeepSeek Harness</h3>
6
6
 
7
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>
8
+ <a href="https://www.npmjs.com/package/@goodandready/dsh-russian-lang"><img src="https://img.shields.io/badge/npm-v0.2.22-6366f1.svg?style=for-the-badge&labelColor=1e1b4b" alt="npm version"></a>
9
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
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
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>
@@ -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
 
@@ -752,7 +752,45 @@ const exportSessionToMarkdown = (session, options = {}) => {
752
752
  '只能读,任何写入都需要审批。': 'Только чтение, любая запись требует подтверждения.',
753
753
  '工作区内可写;工作区外的操作请求人工审批。': 'Запись в рабочей области разрешена; операции вне рабочей области требуют подтверждения.',
754
754
  '全放行,不弹审批。': 'Полный доступ, запросы на подтверждение не выводятся.',
755
- '确定性规则 + 两阶段分类器自动决定;危险或故障时 fail-closed。': 'Детерминированные правила + двухэтапный классификатор; при рисках — безопасная блокировка.'
755
+ '确定性规则 + 两阶段分类器自动决定;危险或故障时 fail-closed。': 'Детерминированные правила + двухэтапный классификатор; при рисках — безопасная блокировка.',
756
+ '手动测试': 'Ручное тестирование',
757
+ '事件': 'Событие',
758
+ '模拟 (看匹配)': 'Симуляция (проверка)',
759
+ '执行 (真实触发)': 'Выполнение (реальный триггер)',
760
+ '通知渠道测试': 'Тест каналов уведомлений',
761
+ '发送测试通知': 'Отправить тестовое уведомление',
762
+ '发送测试卡片': 'Отправить тестовую карточку',
763
+ '飞书通知': 'Уведомления Feishu',
764
+ '已断开': 'Отключено',
765
+ '已连接': 'Подключено',
766
+ '扫码连接飞书': 'Подключить Feishu по QR-коду',
767
+ '确定断开飞书连接': 'Отключить Feishu',
768
+ '断开连接': 'Отключить',
769
+ '断开失败': 'Не удалось отключить',
770
+ '卡片预览': 'Предпросмотр карточки',
771
+ '卡片内容最长': 'Макс. длина карточки',
772
+ '卡片截断长度': 'Длина усечения карточки',
773
+ '截断长度即时生效': 'Длина усечения применяется мгновенно',
774
+ '按当前截断长度生成的卡片正文预览': 'Предпросмотр текста карточки при текущей длине усечения',
775
+ '执行历史': 'История выполнения',
776
+ '全部原因': 'Все причины',
777
+ '清空过滤': 'Очистить фильтр',
778
+ '暂无记录': 'Нет записей',
779
+ '系统通知': 'Системные уведомления',
780
+ '通知机器人': 'Бот уведомлений',
781
+ '飞书扫码授权二维码': 'QR-код авторизации Feishu',
782
+ '在浏览器中打开飞书授权链接': 'Открыть ссылку авторизации Feishu в браузере',
783
+ '重新扫码会覆盖现有应用凭据与本': 'Повторное сканирование перезапишет учётные данные',
784
+ '扫码者本人接收通知卡片': 'Получатель карточки уведомлений — авторизованный пользователь',
785
+ '如未立即生效请重启': 'Если изменения не применились, перезапустите DSH',
786
+ '网络请求失败': 'Сетевой запрос не удался',
787
+ '重试': 'Повторить',
788
+ '已保存': 'Сохранено',
789
+ '保存失败': 'Не удалось сохранить',
790
+ '编辑': 'Редактировать',
791
+ '取消编辑': 'Отмена',
792
+ '启用': 'Включить',
793
+ '停用': 'Отключить'
756
794
  }
757
795
  for (const [k, v] of Object.entries(CORE_ZH_PRESETS)) ZH_EXACT.set(k, v)
758
796
 
@@ -787,7 +825,10 @@ const exportSessionToMarkdown = (session, options = {}) => {
787
825
  }
788
826
  const DOM_EN_ATTRS = {
789
827
  'Streaming preview': 'Предпросмотр стриминга',
790
- 'Visualization streaming preview': 'Предпросмотр визуализации'
828
+ 'Visualization streaming preview': 'Предпросмотр визуализации',
829
+ 'Security Auditor Shield': 'Защитный щит аудитора',
830
+ 'Shield: Safe': 'Щит: Безопасно',
831
+ 'Shield: Alert': 'Щит: Тревога'
791
832
  }
792
833
  const DOM_EN_TEXT = {
793
834
  'Low': 'Низкий',
@@ -799,7 +840,26 @@ const exportSessionToMarkdown = (session, options = {}) => {
799
840
  'X search only': 'Только поиск в X',
800
841
  'Auto mode': 'Автоматический режим',
801
842
  'Full access': 'Полный доступ',
802
- 'Read only': 'Только чтение'
843
+ 'Read only': 'Только чтение',
844
+ 'Security Auditor Shield': 'Защитный щит аудитора',
845
+ 'Shield: Safe': 'Щит: Безопасно',
846
+ 'Shield: Alert': 'Щит: Тревога',
847
+ 'Lens': 'Линза',
848
+ 'Gallery': 'Галерея',
849
+ 'SSH: Local': 'SSH: Локально',
850
+ 'SSH: Remote': 'SSH: Удалённо',
851
+ '7/7 rot': '7/7 рот.',
852
+ 'Smoke chat': 'Тестовый чат',
853
+ 'Register in Models': 'Зарегистрировать в моделях',
854
+ 'Base URL': 'Базовый URL',
855
+ 'API key env / credential name': 'Имя переменной окружения / ключа API',
856
+ 'Default model': 'Модель по умолчанию',
857
+ 'Enabled': 'Включено',
858
+ 'Save': 'Сохранить',
859
+ 'Saved': 'Сохранено',
860
+ 'Refresh': 'Обновить',
861
+ 'Loading settings…': 'Загрузка настроек…',
862
+ 'Put the ar-… key in DSH credentials or process env under this name. Never paste the key into this form.': 'Укажите ключ ar-… в учётных данных DSH или переменной окружения. Не вставляйте ключ в форму.'
803
863
  }
804
864
  const ZH_WALKER = (root) => {
805
865
  try {
@@ -812,7 +872,11 @@ const exportSessionToMarkdown = (session, options = {}) => {
812
872
  const trimmed = node.nodeValue.trim()
813
873
  if (DOM_EN_TEXT[trimmed]) {
814
874
  const p = node.parentElement
815
- if (p && (p.closest('[class*="effort"], [class*="slider"], [class*="reasoning"], [class*="selector"], [class*="Menu"], [class*="menu"], [role="menu"]') || p.getAttribute('role') === 'option' || p.getAttribute('role') === 'menuitem')) {
875
+ if (p && (
876
+ p.closest('[class*="effort"], [class*="slider"], [class*="reasoning"], [class*="selector"], [class*="Menu"], [class*="menu"], [role="menu"], [class*="status"], [class*="chip"], [class*="badge"], [class*="ar-"], [class*="card"], [class*="header"]') ||
877
+ p.getAttribute('role') === 'option' || p.getAttribute('role') === 'menuitem' ||
878
+ p.tagName === 'BUTTON' || p.tagName === 'LABEL' || p.tagName === 'SPAN' || p.tagName === 'A'
879
+ )) {
816
880
  node.nodeValue = node.nodeValue.replace(trimmed, DOM_EN_TEXT[trimmed])
817
881
  }
818
882
  }
@@ -1505,62 +1569,21 @@ const exportSessionToMarkdown = (session, options = {}) => {
1505
1569
  }
1506
1570
 
1507
1571
  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(' '))
1572
+ if (!text || typeof text !== 'string') return { error: 'Текст для перевода пуст.' }
1573
+ try {
1574
+ const res = await fetch('/api/dsh-russian-lang/translate', {
1575
+ method: 'POST',
1576
+ headers: { 'content-type': 'application/json' },
1577
+ body: JSON.stringify({ text })
1578
+ })
1579
+ const data = await res.json().catch(() => ({}))
1580
+ if (!res.ok) {
1581
+ return { error: data.error || ('Ошибка перевода (HTTP ' + res.status + ')') }
1560
1582
  }
1561
- parts[i] = translatedParas.join('\n\n')
1583
+ return { translatedText: data.translatedText || text }
1584
+ } catch (err) {
1585
+ return { error: 'Не удалось связаться с хостом DSH: ' + (err.message || err) }
1562
1586
  }
1563
- return parts.join('\n\n')
1564
1587
  }
1565
1588
 
1566
1589
  function TranslateTurnAction(props) {
@@ -1568,7 +1591,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
1568
1591
  const [loading, setLoading] = React.useState(false)
1569
1592
  const [open, setOpen] = React.useState(false)
1570
1593
  const [upStatus, setUpStatus] = React.useState({
1571
- currentVersion: '0.2.20',
1594
+ currentVersion: '0.2.22',
1572
1595
  latestVersion: undefined,
1573
1596
  updateAvailable: false
1574
1597
  })
@@ -1613,8 +1636,42 @@ const exportSessionToMarkdown = (session, options = {}) => {
1613
1636
  })
1614
1637
  }
1615
1638
 
1639
+ const [transStatus, setTransStatus] = React.useState(null)
1640
+ const [transStarting, setTransStarting] = React.useState(false)
1641
+ const [transMsg, setTransMsg] = React.useState(null)
1642
+
1643
+ const fetchTransStatus = () => {
1644
+ fetch('/api/dsh-russian-lang/translator/status')
1645
+ .then((r) => r.json())
1646
+ .then((data) => setTransStatus(data))
1647
+ .catch(() => setTransStatus(null))
1648
+ }
1649
+
1650
+ const startLibreTranslate = () => {
1651
+ setTransStarting(true)
1652
+ setTransMsg(null)
1653
+ fetch('/api/dsh-russian-lang/translator/setup', { method: 'POST' })
1654
+ .then((r) => r.json())
1655
+ .then((data) => {
1656
+ setTransStarting(false)
1657
+ if (data.ok) {
1658
+ setTransMsg({ type: 'ok', text: data.message || 'Контейнер LibreTranslate запущен.' })
1659
+ fetchTransStatus()
1660
+ } else {
1661
+ setTransMsg({ type: 'err', text: data.error || 'Ошибка запуска контейнера.' })
1662
+ }
1663
+ })
1664
+ .catch((err) => {
1665
+ setTransStarting(false)
1666
+ setTransMsg({ type: 'err', text: 'Ошибка: ' + (err.message || err) })
1667
+ })
1668
+ }
1669
+
1616
1670
  React.useEffect(() => {
1617
- if (open) checkUpdate()
1671
+ if (open) {
1672
+ checkUpdate()
1673
+ fetchTransStatus()
1674
+ }
1618
1675
  }, [open])
1619
1676
 
1620
1677
 
@@ -1676,29 +1733,36 @@ const exportSessionToMarkdown = (session, options = {}) => {
1676
1733
  if (prose) rawText = prose.innerText.trim()
1677
1734
  }
1678
1735
 
1679
- const translated = rawText ? await translateTurnContent(rawText) : 'Не удалось обнаружить текст сообщения ассистента для перевода.'
1736
+ let transResult = { error: 'Не удалось обнаружить текст сообщения ассистента для перевода.' }
1737
+ if (rawText) {
1738
+ transResult = await translateTurnContent(rawText)
1739
+ }
1740
+ const isErr = !!transResult.error
1741
+ const displayText = transResult.translatedText || transResult.error || ''
1680
1742
 
1681
1743
  box = document.createElement('div')
1682
1744
  box.className = 'rl-turn-translation'
1683
1745
  const key = tailFlowItem.getAttribute('data-chat-flow-key')
1684
1746
  if (key) box.dataset.tailKey = key
1685
1747
  box.innerHTML = '<div class="rl-trans-head">' +
1686
- '<span class="rl-trans-title">🌐 Перевод на русский</span>' +
1748
+ '<span class="rl-trans-title">' + (isErr ? '⚠️ Машинный перевод' : '🌐 Перевод на русский') + '</span>' +
1687
1749
  '<div class="rl-trans-tools">' +
1688
- '<button type="button" class="rl-trans-btn rl-btn-copy" title="Скопировать перевод">📋 Копировать</button>' +
1750
+ (!isErr ? '<button type="button" class="rl-trans-btn rl-btn-copy" title="Скопировать перевод">📋 Копировать</button>' : '') +
1689
1751
  '<button type="button" class="rl-trans-btn rl-btn-close" title="Закрыть">✕</button>' +
1690
1752
  '</div>' +
1691
1753
  '</div>' +
1692
- '<div class="rl-trans-body"></div>'
1693
- box.querySelector('.rl-trans-body').textContent = translated
1754
+ '<div class="rl-trans-body"' + (isErr ? ' style="color: var(--dsw-alias-color-warning, #eab308); font-size: 13px;"' : '') + '></div>'
1755
+ box.querySelector('.rl-trans-body').textContent = displayText
1694
1756
 
1695
1757
  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
- })
1758
+ if (copyBtn) {
1759
+ copyBtn.addEventListener('click', (e) => {
1760
+ e.stopPropagation()
1761
+ try { navigator.clipboard.writeText(displayText) } catch (err) {}
1762
+ copyBtn.textContent = '✓ Скопировано'
1763
+ setTimeout(() => { copyBtn.textContent = '📋 Копировать' }, 2000)
1764
+ })
1765
+ }
1702
1766
 
1703
1767
  const closeBtn = box.querySelector('.rl-btn-close')
1704
1768
  closeBtn.addEventListener('click', (e) => {
@@ -1968,7 +2032,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
1968
2032
  React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
1969
2033
  React.createElement('div', null,
1970
2034
  React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
1971
- t('updaterCurrent').replace('{version}', upStatus.currentVersion || '0.2.20')),
2035
+ t('updaterCurrent').replace('{version}', upStatus.currentVersion || '0.2.22')),
1972
2036
  upStatus.updateAvailable
1973
2037
  ? React.createElement('div', { style: { color: 'var(--dsw-alias-color-warning, #eab308)', marginTop: '2px', fontWeight: 500 } },
1974
2038
  t('updaterLatest').replace('{version}', upStatus.latestVersion || ''))
@@ -1999,6 +2063,66 @@ const exportSessionToMarkdown = (session, options = {}) => {
1999
2063
  )
2000
2064
  ),
2001
2065
 
2066
+ React.createElement('div', { className: 'rl-section-card' },
2067
+ React.createElement('div', { className: 'rl-section-title' },
2068
+ React.createElement('span', null, t('secTranslator')),
2069
+ React.createElement('span', {
2070
+ className: 'rl-badge ' + (value.translateEngine === 'local' ? 'rl-badge-ok' : (value.translateEngine === 'google' ? 'rl-badge-warn' : 'rl-badge-dim'))
2071
+ }, value.translateEngine === 'local' ? 'Локально' : (value.translateEngine === 'google' ? 'Google' : 'Выключен'))
2072
+ ),
2073
+ React.createElement('div', { className: 'rl-section-desc' }, t('secTranslatorDesc')),
2074
+ React.createElement('div', { className: 'rl-item-card' },
2075
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
2076
+ React.createElement('label', { className: 'rl-item-label', style: { fontWeight: 500 } }, t('translateEngine')),
2077
+ React.createElement('select', {
2078
+ className: 'rl-select',
2079
+ value: value.translateEngine || 'off',
2080
+ onChange: (ev) => {
2081
+ try {
2082
+ const val = ev.target.value
2083
+ scope.set('translateEngine', val)
2084
+ if (val === 'local') fetchTransStatus()
2085
+ } catch (err) { console.warn('dsh-russian-lang: set translateEngine failed', err) }
2086
+ }
2087
+ },
2088
+ React.createElement('option', { value: 'off' }, t('engineOff')),
2089
+ React.createElement('option', { value: 'local' }, t('engineLocal')),
2090
+ React.createElement('option', { value: 'google' }, t('engineGoogle'))
2091
+ )
2092
+ ),
2093
+ value.translateEngine === 'google' ? React.createElement('div', {
2094
+ style: {
2095
+ marginTop: '10px', padding: '8px 12px', borderRadius: '8px',
2096
+ background: 'rgba(234, 179, 8, 0.12)', border: '1px solid rgba(234, 179, 8, 0.3)',
2097
+ color: 'var(--dsw-alias-color-warning, #eab308)', fontSize: '12px', fontWeight: 500, lineHeight: 1.4
2098
+ }
2099
+ }, t('googleWarn')) : null,
2100
+ value.translateEngine === 'local' ? React.createElement('div', {
2101
+ style: { marginTop: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }
2102
+ },
2103
+ React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, t('localInfo')),
2104
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
2105
+ React.createElement('span', {
2106
+ className: 'rl-badge ' + (transStatus && transStatus.running ? 'rl-badge-ok' : 'rl-badge-dim')
2107
+ }, transStatus && transStatus.running ? t('localStatusRunning') : t('localStatusStopped')),
2108
+ (!transStatus || !transStatus.running) ? React.createElement('button', {
2109
+ type: 'button',
2110
+ className: 'rl-btn rl-btn-primary',
2111
+ disabled: transStarting,
2112
+ onClick: startLibreTranslate
2113
+ }, transStarting ? t('localStarting') : t('localStartBtn')) : null
2114
+ ),
2115
+ transMsg ? React.createElement('div', {
2116
+ style: {
2117
+ marginTop: '6px', padding: '8px 12px', borderRadius: '8px',
2118
+ background: transMsg.type === 'ok' ? 'rgba(34, 197, 94, 0.12)' : 'rgba(239, 68, 68, 0.12)',
2119
+ color: transMsg.type === 'ok' ? '#16a34a' : '#dc2626', fontSize: '12px', fontWeight: 500
2120
+ }
2121
+ }, transMsg.text) : null
2122
+ ) : null
2123
+ )
2124
+ ),
2125
+
2002
2126
  React.createElement('div', { className: 'rl-section-card' },
2003
2127
  React.createElement('div', { className: 'rl-section-title' },
2004
2128
  React.createElement('span', null, t('secSupport')),
@@ -2021,7 +2145,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
2021
2145
  ),
2022
2146
  React.createElement('div', { className: 'rl-actions-row' },
2023
2147
  React.createElement('a', {
2024
- href: makeIssueUrl({}, '0.2.20'),
2148
+ href: makeIssueUrl({}, '0.2.22'),
2025
2149
  target: '_blank',
2026
2150
  rel: 'noopener noreferrer',
2027
2151
  className: 'rl-btn rl-btn-primary'
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
  }