@goodandready/dsh-russian-lang 0.2.6 → 0.2.8

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/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // в собственном namespace, чтобы запись переживала браузеры и машины через
6
6
  // штатный механизм настроек DSH.
7
7
  import z from '@deepseek-ai/schemastery'
8
+ import { SYSTEM_PROMPT_PRESETS } from './pure.js'
8
9
 
9
10
  export const name = '@goodandready/dsh-russian-lang'
10
11
 
@@ -13,9 +14,13 @@ const RussianLangSettingsSchema = z.object({
13
14
  overrides: z.dict(z.string()).required(false),
14
15
  typography: z.object({
15
16
  enabled: z.boolean().required(false),
16
- yo: z.boolean().required(false)
17
+ yo: z.boolean().required(false),
18
+ liveInput: z.boolean().required(false)
17
19
  }).required(false),
18
- agentPrompt: z.boolean().required(false)
20
+ agentPrompt: z.boolean().required(false),
21
+ agentPromptPreset: z.string().required(false),
22
+ slashAliases: z.boolean().required(false),
23
+ quickSwitch: z.boolean().required(false)
19
24
  })
20
25
 
21
26
  export function apply(ctx) {
@@ -24,19 +29,22 @@ export function apply(ctx) {
24
29
  })
25
30
 
26
31
  // #68: русский системный промпт агента (опционально). Регистрируем секцию
27
- // промпта, которая просит отвечать по-русски, когда включён флаг
32
+ // промпта, которая задаёт стиль ответов по-русски, когда включён флаг
28
33
  // russian-lang.agentPrompt. Секция — официальная точка расширения ядра.
29
34
  ctx.inject(['settings', 'systemPrompt'], (settingsCtx, prompt) => {
30
35
  const NS = 'russian-lang'
31
36
  const RU_SECTION = 'dsh-russian-lang-agent-prompt'
32
- const RU_TEXT = 'Отвечай пользователю на русском языке. Если пользователь пишет на другом языке, отвечай на его языке.'
33
- let current = false
37
+ const DEFAULT_RU_TEXT = 'Отвечай пользователю на русском языке. Если пользователь пишет на другом языке, отвечай на его языке.'
38
+ let currentText = ''
34
39
  const sync = () => {
35
40
  const value = settingsCtx.settings.get(NS)
36
41
  const want = !!(value && value.agentPrompt)
37
- if (want === current) return
38
- current = want
39
- prompt.section({ name: RU_SECTION, order: 1000, text: want ? RU_TEXT : '' })
42
+ const presetKey = value && value.agentPromptPreset
43
+ const preset = SYSTEM_PROMPT_PRESETS[presetKey]
44
+ const text = want ? (preset ? preset.text : DEFAULT_RU_TEXT) : ''
45
+ if (text === currentText) return
46
+ currentText = text
47
+ prompt.section({ name: RU_SECTION, order: 1000, text })
40
48
  }
41
49
  ctx.on('settings/updated', (ns) => { if (ns === NS) sync() })
42
50
  sync()
package/lib/pure.js CHANGED
@@ -341,3 +341,206 @@ export const makeLayout = (freq, localDict = new Set()) => {
341
341
  }
342
342
  return { ruWordFraction, candidate, learnWords, localDict }
343
343
  }
344
+
345
+
346
+ // ---------------------------------------------------- живая типографика инпута
347
+
348
+ // Предлоги и союзы, после которых ставится неразрывный пробел (U+00A0)
349
+ const SHORT_WORDS_RE = /(^|[\s(«])([вВнНсСпПоОкКуУиИаА]|из|от|до|за|по|со|во|ко|об|на|под|над|при|про|без|для|не|ни|но)( )/g
350
+
351
+ /**
352
+ * Живая типографика для поля ввода DSH.
353
+ * Заменяет кавычки "" на «», дефисы -- на тире —, многоточие ... на …,
354
+ * и добавляет неразрывные пробелы после коротких предлогов.
355
+ * ИГНОРИРУЕТ код внутри обратных кавычек `...` и блоков ```...```.
356
+ */
357
+ export const formatInputLive = (text) => {
358
+ if (!text || typeof text !== 'string') return text
359
+
360
+ // Разбиваем по блокам кода (inline `...` или fenced ```...```), чтобы не портить синтаксис
361
+ const parts = text.split(/(```[\s\S]*?```|`[^`\n]*`)/g)
362
+ for (let i = 0; i < parts.length; i += 2) {
363
+ let s = parts[i]
364
+ if (!s) continue
365
+
366
+ // 1. Троеточие
367
+ s = s.replace(/\.{3}/g, '…')
368
+
369
+ // 2. Длинное тире (-- -> —)
370
+ s = s.replace(/(^|\s)--(\s|$)/g, '$1—$2')
371
+
372
+ // 3. Кавычки-ёлочки («...») с учётом открывающейся/закрывающейся
373
+ s = s.replace(/(^|[\s([{-])"/g, '$1«')
374
+ s = s.replace(/"/g, '»')
375
+
376
+ // 4. Неразрывный пробел после коротких слов/предлогов
377
+ s = s.replace(SHORT_WORDS_RE, '$1$2\u00A0')
378
+
379
+ parts[i] = s
380
+ }
381
+ return parts.join('')
382
+ }
383
+
384
+ // ------------------------------------------------------- алиасы слэш-команд
385
+
386
+ export const RUSSIAN_SLASH_ALIASES = {
387
+ '/цель': '/goal',
388
+ '/сжать': '/compact',
389
+ '/план': '/plan',
390
+ '/экспорт': '/export',
391
+ '/отзыв': '/feedback',
392
+ '/разрешение': '/permission',
393
+ '/разрешения': '/permission',
394
+ '/память': '/memory',
395
+ '/помощь': '/help',
396
+ '/очистить': '/clear',
397
+ '/сессия': '/session',
398
+ '/модель': '/model'
399
+ }
400
+
401
+ /**
402
+ * Разворачивает русский алиас слэш-команды в каноническую команду DSH.
403
+ * Например: "/цель сделать тесты" -> "/goal сделать тесты"
404
+ */
405
+ export const expandSlashAlias = (input) => {
406
+ if (!input || typeof input !== 'string' || !input.startsWith('/')) return input
407
+ const match = input.match(/^(\s*\/[^\s]+)(.*)$/)
408
+ if (!match) return input
409
+ const [, cmd, rest] = match
410
+ const lowerCmd = cmd.trim().toLowerCase()
411
+ if (RUSSIAN_SLASH_ALIASES[lowerCmd]) {
412
+ return RUSSIAN_SLASH_ALIASES[lowerCmd] + rest
413
+ }
414
+ return input
415
+ }
416
+
417
+ // --------------------------------------------------- фонетический транслит
418
+
419
+ const PHONETIC_LAT_TO_CYR = [
420
+ ['shch', 'щ'], ['yo', 'ё'], ['zh', 'ж'], ['ch', 'ч'], ['sh', 'ш'],
421
+ ['yu', 'ю'], ['ya', 'я'], ['ts', 'ц'],
422
+ ['a', 'а'], ['b', 'б'], ['v', 'в'], ['g', 'г'], ['d', 'д'], ['e', 'е'],
423
+ ['z', 'з'], ['i', 'и'], ['j', 'й'], ['k', 'к'], ['l', 'л'], ['m', 'м'],
424
+ ['n', 'н'], ['o', 'о'], ['p', 'п'], ['r', 'р'], ['s', 'с'], ['t', 'т'],
425
+ ['u', 'у'], ['f', 'ф'], ['h', 'х'], ['c', 'ц'], ['y', 'ы'], ['x', 'кс']
426
+ ]
427
+
428
+ const PHONETIC_CYR_TO_LAT = [
429
+ ['щ', 'shch'], ['ё', 'yo'], ['ж', 'zh'], ['ч', 'ch'], ['ш', 'sh'],
430
+ ['ю', 'yu'], ['я', 'ya'], ['ц', 'ts'],
431
+ ['а', 'a'], ['б', 'b'], ['в', 'v'], ['г', 'g'], ['д', 'd'], ['е', 'e'],
432
+ ['з', 'z'], ['и', 'i'], ['й', 'j'], ['к', 'k'], ['л', 'l'], ['м', 'm'],
433
+ ['н', 'n'], ['о', 'o'], ['п', 'p'], ['р', 'r'], ['с', 's'], ['т', 't'],
434
+ ['у', 'u'], ['ф', 'f'], ['х', 'h'], ['ы', 'y'], ['э', 'e'], ['ъ', ''], ['ь', '']
435
+ ]
436
+
437
+ export const phoneticTranslit = (text, direction = 'lat2cyr') => {
438
+ if (!text || typeof text !== 'string') return text
439
+ let res = text
440
+ const pairs = direction === 'lat2cyr' ? PHONETIC_LAT_TO_CYR : PHONETIC_CYR_TO_LAT
441
+ for (const [from, to] of pairs) {
442
+ const fromUpper = from.toUpperCase()
443
+ const fromTitle = from[0].toUpperCase() + from.slice(1)
444
+ const toUpper = to.toUpperCase()
445
+ const toTitle = to[0] ? to[0].toUpperCase() + to.slice(1) : ''
446
+
447
+ if (from.length > 1) {
448
+ res = res.replaceAll(fromUpper, toUpper)
449
+ res = res.replaceAll(fromTitle, toTitle)
450
+ }
451
+ res = res.replaceAll(from, to)
452
+ if (from.length === 1) {
453
+ res = res.replaceAll(fromUpper, toUpper)
454
+ }
455
+ }
456
+ return res
457
+ }
458
+
459
+ // ---------------------------------------------------- детектор раскладки
460
+
461
+ /**
462
+ * Определяет преобладающий язык ввода в тексте: 'RU', 'EN' или null.
463
+ */
464
+ export const detectInputLayout = (text) => {
465
+ if (!text || typeof text !== 'string') return null
466
+ const clean = text.replace(/`[^`]*`/g, '').replace(/https?:\/\/\S+/g, '')
467
+ let cyr = 0
468
+ let lat = 0
469
+ for (const ch of clean) {
470
+ const code = ch.charCodeAt(0)
471
+ if ((code >= 0x0400 && code <= 0x04FF) || code === 0x0500) cyr++
472
+ else if ((code >= 0x41 && code <= 0x5A) || (code >= 0x61 && code <= 0x7A)) lat++
473
+ }
474
+ if (cyr > lat && cyr >= 2) return 'RU'
475
+ if (lat > cyr && lat >= 2) return 'EN'
476
+ return null
477
+ }
478
+
479
+ // ------------------------------------------------- пресеты промптов агента
480
+
481
+ export const SYSTEM_PROMPT_PRESETS = {
482
+ technical_expert: {
483
+ id: 'technical_expert',
484
+ label: 'Технический эксперт',
485
+ desc: 'Строгая русская инженерная терминология, чистый код и русские комментарии',
486
+ text: 'Отвечай пользователю на русском языке. Используй точную русскую инженерную терминологию, пиши чистый, идиоматичный код с русскими комментариями и следуй лучшим практикам разработки. Если пользователь пишет на другом языке, отвечай на его языке.'
487
+ },
488
+ tech_writer: {
489
+ id: 'tech_writer',
490
+ label: 'Технический писатель',
491
+ desc: 'Структурированные тексты, Markdown, ГОСТ/RFC и выверенная типографика',
492
+ text: 'Отвечай пользователю на русском языке. Оформляй документацию и пояснения в структурированном виде (Markdown, списки, таблицы), строго соблюдай правила русской типографики («кавычки-ёлочки», длинное тире, буква «ё») и требования к техническим текстам.'
493
+ },
494
+ concise: {
495
+ id: 'concise',
496
+ label: 'Лаконичный режим',
497
+ desc: 'Краткие ёмкие ответы без лишней вводной воды и повторов',
498
+ text: 'Отвечай пользователю на русском языке максимально кратко и ёмко. Без лишних вводных слов, повторов и пространных рассуждений. Сразу переходи к сути, коду или прямому решению задачи.'
499
+ }
500
+ }
501
+
502
+ // -------------------------------------------------- локализованный экспорт
503
+
504
+ /**
505
+ * Генерирует локализованный Markdown-отчёт по сессии диалога.
506
+ */
507
+ export const exportSessionToMarkdown = (session, options = {}) => {
508
+ if (!session) return ''
509
+ const title = session.title || session.name || 'Диалог DSH'
510
+ const date = session.createdAt ? new Date(session.createdAt) : new Date()
511
+ const dateStr = new Intl.DateTimeFormat('ru-RU', {
512
+ dateStyle: 'full',
513
+ timeStyle: 'medium'
514
+ }).format(date)
515
+
516
+ const messages = Array.isArray(session.messages) ? session.messages : []
517
+ let md = `# 💬 ${title}\n\n`
518
+ md += `> **Дата экспорта:** ${dateStr}\n`
519
+ if (session.model) md += `> **Модель:** \`${session.model}\`\n`
520
+ if (session.workspace) md += `> **Рабочая область:** \`${session.workspace}\`\n`
521
+ md += `> **Всего сообщений:** ${messages.length}\n\n---\n\n`
522
+
523
+ for (let idx = 0; idx < messages.length; idx++) {
524
+ const msg = messages[idx]
525
+ const role = msg.role || 'unknown'
526
+ const roleName = role === 'user' ? '👤 Пользователь' :
527
+ role === 'assistant' ? '🤖 Ассистент' :
528
+ role === 'system' ? '⚙️ Система' : `🔧 Инструмент (${role})`
529
+
530
+ md += `### ${roleName}\n\n`
531
+ if (msg.content) {
532
+ md += `${msg.content}\n\n`
533
+ }
534
+ if (Array.isArray(msg.toolCalls) && msg.toolCalls.length) {
535
+ md += `*Вызовы инструментов:*\n`
536
+ for (const tc of msg.toolCalls) {
537
+ md += `- **${tc.name || 'tool'}**: \`${JSON.stringify(tc.arguments || {})}\`\n`
538
+ }
539
+ md += `\n`
540
+ }
541
+ md += `---\n\n`
542
+ }
543
+
544
+ md += `*Сгенерировано с помощью [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) и @goodandready/dsh-russian-lang*\n`
545
+ return md
546
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-russian-lang",
3
- "version": "0.2.6",
4
- "description": "Russian localization for the DeepSeek Harness web UI: adds Русский as the third option in the native Settings - General - Language menu. Translates the full core surface (100% coverage of DSH 0.1.1-rc.2) plus 16 community plugin namespaces - 51 namespaces, 2899 strings (about 64% hand-reviewed, the rest machine-drafted and tracked as status: draft), with Russian plural forms, count-key plurals, a typography pass (guillemets, em dash, nbsp), a wrong-layout input hint and browser spellcheck. The locale runtime snapshot is extended at runtime on unpatched cores; no files in the DSH installation are modified. The choice is persisted through the plugin-owned russian-lang namespace.",
3
+ "version": "0.2.8",
4
+ "description": "Russian localization for DeepSeek Harness (DSH v0.1.5-alpha.1+) web UI: adds Русский as the native language option in Settings - General - Language. 100.0% coverage across 5395 keys (1144 core UI keys + 4251 keys across 40+ ecosystem plugins) with zero machine drafts. Features Russian plural forms (one/few/many), automatic typography pass (guillemets, em dashes, non-breaking spaces, optional ё), wrong-layout input hint (Alt+L), browser spellcheck, and runtime snapshot extension without modifying core files.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -96,13 +96,26 @@
96
96
  },
97
97
  "command": {
98
98
  "listbox.aria": "Совпадения /{command}",
99
- "notice.imagesUnsupported": "/{command} не принимает вложения-изображения сначала удалите их",
99
+ "notice.imagesUnsupported": "/{command} не принимает изображения; сначала удалите их",
100
100
  "overlay.aria": "Параметры /{command}",
101
101
  "search.aria": "Фильтр параметров",
102
102
  "search.placeholder": "Поиск…",
103
103
  "status.applying": "Применение…",
104
104
  "status.empty": "Нет параметров",
105
- "status.loading": "Загрузка параметров…"
105
+ "status.loading": "Загрузка параметров…",
106
+ "executing": "Выполнение команды…",
107
+ "completed": "Команда завершена",
108
+ "failed": "Сбой выполнения команды",
109
+ "cancelled": "Команда отменена",
110
+ "timeout": "Превышено время ожидания команды",
111
+ "running": "Выполняется",
112
+ "description.compact": "Сжать раннюю историю диалога",
113
+ "description.export": "Скачать журнал сессии в формате ZIP-архива",
114
+ "description.feedback": "Оставить отзыв о текущей сессии",
115
+ "description.goal": "Установить или просмотреть цель для длительной задачи",
116
+ "description.permission": "Переключить пресет разрешений (режим песочницы и политики подтверждений)",
117
+ "description.plan": "Войти или выйти из режима планирования",
118
+ "notice.attachmentsUnsupported": "/{command} не принимает вложения; сначала удалите их"
106
119
  },
107
120
  "session-log-download": {
108
121
  "dialog.close": "Закрыть",
@@ -216,7 +216,51 @@
216
216
  "web.contentTruncated": "Содержимое обрезано",
217
217
  "web.http": "HTTP",
218
218
  "web.noResults": "Ничего не найдено",
219
- "web.sourcesTruncated": "Список источников обрезан"
219
+ "web.sourcesTruncated": "Список источников обрезан",
220
+ "action.copy": "Копировать сообщение",
221
+ "action.fork": "Ответвить диалог с этой реплики",
222
+ "action.quote": "Цитировать",
223
+ "action.retry": "Повторить генерацию",
224
+ "action.delete": "Удалить сообщение",
225
+ "action.edit": "Редактировать сообщение",
226
+ "status.thinking": "Рассуждение…",
227
+ "status.generating": "Генерация ответа…",
228
+ "status.toolRunning": "Вызов инструмента: {name}",
229
+ "status.interrupted": "Генерация прервана пользователем",
230
+ "tokenUsage": "Токены: {used} / {limit}",
231
+ "contextWindow": "Окно контекста: {percent}%",
232
+ "costEstimate": "Оценочная стоимость: ${cost}",
233
+ "timeElapsed": "Затрачено времени: {time}",
234
+ "modelChanged": "Модель переключена на {model}",
235
+ "presetApplied": "Применён пресет: {preset}",
236
+ "sandboxActive": "Песочница: {mode}",
237
+ "export.title": "Экспорт диалога",
238
+ "export.markdown": "Экспорт в Markdown",
239
+ "export.json": "Экспорт в JSON",
240
+ "turn.number": "Реплика #{n}",
241
+ "turn.details": "Сведения о реплике",
242
+ "attachment.dropBlocked": "В настоящий момент добавление файлов и изображений заблокировано",
243
+ "attachment.dropDesc": "Лимит изображений: до {count} шт., до {size} каждое",
244
+ "attachment.dropTitle": "Перетащите файлы или изображения сюда для добавления",
245
+ "attachment.pending": "Ожидающие вложения",
246
+ "attachment.scrollLeft": "Прокрутить вложения влево",
247
+ "attachment.scrollRight": "Прокрутить вложения вправо",
248
+ "command.attachmentsUnsupported": "/{command} не принимает вложения; сначала удалите их",
249
+ "file.attach": "Прикрепить файл",
250
+ "file.label": "Файл",
251
+ "file.notStaged": "Загрузка файла не завершена; прикрепите заново и повторите попытку",
252
+ "file.pending": "Ожидающие файлы",
253
+ "file.remove": "Удалить файл {name}",
254
+ "file.retry": "Повторить загрузку {name}",
255
+ "file.sessionUnavailable": "Сессия недоступна; загрузка файлов невозможна",
256
+ "file.stillUploading": "Файлы всё ещё загружаются; отправьте сообщение после завершения загрузки",
257
+ "file.uploadFailed": "Сбой загрузки; нажмите для повтора",
258
+ "file.uploading": "Загрузка…",
259
+ "input.send.queue": "Поставить в очередь",
260
+ "input.send.steer": "Направить сообщение",
261
+ "queue.file": "Файл в очереди: {name}",
262
+ "queue.sending": "Отправка…",
263
+ "tool.title.readImage": "Просмотр изображения"
220
264
  },
221
265
  "chat": {
222
266
  "chat.deepDiving": "Глубокий анализ…",
@@ -234,19 +278,19 @@
234
278
  "command.failed": "Команда завершилась с ошибкой",
235
279
  "command.running": "Выполняется…",
236
280
  "command.title": "Команда",
237
- "details.close": "Закрыть сведения",
238
- "details.empty": "Нажмите на строку инструмента в потоке сообщений для просмотра сведений",
281
+ "details.close": "Закрыть детали",
282
+ "details.empty": "Нет дополнительных сведений",
239
283
  "details.input": "Входные данные",
240
- "details.notInWindow": "Этот вызов находится за пределами текущего окна",
241
- "details.output": "Вывод",
284
+ "details.notInWindow": "Реплика за пределами текущего окна контекста",
285
+ "details.output": "Результат",
242
286
  "details.running": "Выполняется…",
243
- "details.title": "Сведения",
287
+ "details.title": "Детали выполнения",
244
288
  "duration.compactMinutes": "{minutes}м {seconds}с",
245
289
  "duration.compactSeconds": "{seconds}с",
246
290
  "duration.milliseconds": "{milliseconds}мс",
247
291
  "duration.minutes": "{minutes}м {seconds}с",
248
292
  "duration.seconds": "{seconds}с",
249
- "fileOpen.folderTitle": "Не удалось открыть папку",
293
+ "fileOpen.folderTitle": "Открыть папку",
250
294
  "fileOpen.folderUnknown": "Не удалось открыть эту папку",
251
295
  "fileOpen.title": "Не удалось открыть файл",
252
296
  "fileOpen.unknown": "Не удалось открыть этот файл",
@@ -328,7 +372,14 @@
328
372
  "stats.tokensPerSecond": "{throughput} ток./с",
329
373
  "stats.toolCall": "Инструмент {duration}",
330
374
  "stats.ttftAverage": "TTFT ср. {duration}",
331
- "view.chat": "Чат"
375
+ "view.chat": "Чат",
376
+ "message.systemPromptUpdate": "Обновление системного промпта",
377
+ "stats.dialog.llmTime": "Время LLM",
378
+ "stats.dialog.speed": "Токенов в секунду (TPS)",
379
+ "stats.dialog.title": "Статистика сессии",
380
+ "stats.dialog.toolTime": "Время инструментов",
381
+ "stats.dialog.ttft": "Среднее время до первого токена (TTFT)",
382
+ "stats.dialog.usageTitle": "Использование токенов"
332
383
  },
333
384
  "trajectory": {
334
385
  "block.label": "Блок #{index} {type}",
@@ -771,7 +822,8 @@
771
822
  "usage.sessionCumulative": "Session cumulative",
772
823
  "usage.thisRequest": "This request",
773
824
  "usage.tokens": "Токены",
774
- "view.trajectory": "Trajectory"
825
+ "view.trajectory": "Trajectory",
826
+ "layout.fileAttachments": "Файлы ×{count}"
775
827
  },
776
828
  "slash.menu": {
777
829
  "crumbs.aria": "Навигация по папкам",