@goodandready/dsh-russian-lang 0.2.8 → 0.2.9
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 +3 -3
- package/lib/client.js +337 -40
- package/package.json +1 -1
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.
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-russian-lang"><img src="https://img.shields.io/badge/npm-v0.2.9-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>
|
|
@@ -169,9 +169,9 @@ russian-lang:
|
|
|
169
169
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
170
170
|
|
|
171
171
|
|
|
172
|
-
## ⚡ Новые возможности v0.2.
|
|
172
|
+
## ⚡ Новые возможности v0.2.9 — Smart Russian UX
|
|
173
173
|
|
|
174
|
-
Версия **v0.2.
|
|
174
|
+
Версия **v0.2.9** расширяет функционал плагина от чистой локализации до полноценной адаптации пользовательского опыта работы с моделями на русском языке:
|
|
175
175
|
|
|
176
176
|
1. **Живая экранная типографика в поле ввода (Live Input Typography)**:
|
|
177
177
|
- Автоматическая подстановка кавычек-ёлочек («...») вместо машинных программистских кавычек (`""`).
|
package/lib/client.js
CHANGED
|
@@ -1019,10 +1019,35 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1019
1019
|
|
|
1020
1020
|
// Отвечаем на real input: input / input_event, слушаем на document.
|
|
1021
1021
|
// Читаем value у поля, где курсор (textarea/input), не трогая contenteditable.
|
|
1022
|
+
function setNativeInputValue(el, value) {
|
|
1023
|
+
if (!el) return
|
|
1024
|
+
const proto = el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype
|
|
1025
|
+
const desc = Object.getOwnPropertyDescriptor(proto, 'value')
|
|
1026
|
+
if (desc && desc.set) {
|
|
1027
|
+
desc.set.call(el, value)
|
|
1028
|
+
} else {
|
|
1029
|
+
el.value = value
|
|
1030
|
+
}
|
|
1031
|
+
if (el._valueTracker) {
|
|
1032
|
+
try { el._valueTracker.setValue(value) } catch (e) {}
|
|
1033
|
+
}
|
|
1034
|
+
el.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1035
|
+
el.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1022
1038
|
let layoutHintEl = null
|
|
1023
|
-
const layoutCurrentInput = () => {
|
|
1039
|
+
const layoutCurrentInput = (ev) => {
|
|
1040
|
+
if (ev && ev.target && (ev.target.tagName === 'TEXTAREA' || (ev.target.tagName === 'INPUT' && (ev.target.type === 'text' || !ev.target.type)))) {
|
|
1041
|
+
return ev.target
|
|
1042
|
+
}
|
|
1024
1043
|
const el = document.activeElement
|
|
1025
|
-
if (el && (el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' &&
|
|
1044
|
+
if (el && (el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && (ev.target ? ev.target.type === 'text' : true)))) return el
|
|
1045
|
+
if (el && typeof el.querySelector === 'function') {
|
|
1046
|
+
const inner = el.querySelector('textarea, input[type="text"]')
|
|
1047
|
+
if (inner) return inner
|
|
1048
|
+
}
|
|
1049
|
+
const focused = document.querySelector('textarea:focus, input[type="text"]:focus')
|
|
1050
|
+
if (focused) return focused
|
|
1026
1051
|
return null
|
|
1027
1052
|
}
|
|
1028
1053
|
const layoutDismiss = () => {
|
|
@@ -1041,8 +1066,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1041
1066
|
layoutHintEl.textContent = label + ': ' + converted
|
|
1042
1067
|
layoutHintEl.addEventListener('mousedown', (ev) => {
|
|
1043
1068
|
ev.preventDefault()
|
|
1044
|
-
inputEl
|
|
1045
|
-
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1069
|
+
setNativeInputValue(inputEl, converted)
|
|
1046
1070
|
learnWords(converted) // #67: запомнить принятые слова
|
|
1047
1071
|
layoutDismiss()
|
|
1048
1072
|
})
|
|
@@ -1078,7 +1102,10 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1078
1102
|
ev.preventDefault()
|
|
1079
1103
|
const v = el.value || ''
|
|
1080
1104
|
const c = layoutFixCandidate(v, 'lat2cyr') || layoutFixCandidate(v, 'cyr2lat')
|
|
1081
|
-
if (c) {
|
|
1105
|
+
if (c) {
|
|
1106
|
+
setNativeInputValue(el, c.converted)
|
|
1107
|
+
learnWords(c.converted)
|
|
1108
|
+
}
|
|
1082
1109
|
})
|
|
1083
1110
|
document.body.appendChild(layoutBadgeEl)
|
|
1084
1111
|
}
|
|
@@ -1090,10 +1117,13 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1090
1117
|
const layoutBadgeHide = () => {
|
|
1091
1118
|
if (layoutBadgeEl) { layoutBadgeEl.remove(); layoutBadgeEl = null }
|
|
1092
1119
|
}
|
|
1093
|
-
|
|
1120
|
+
|
|
1121
|
+
let isFormatting = false
|
|
1122
|
+
const layoutOnInput = (ev) => {
|
|
1123
|
+
if (isFormatting) return
|
|
1094
1124
|
try {
|
|
1095
1125
|
if (runtime.getLocale().active !== 'ru') { layoutDismiss(); layoutBadgeHide(); return }
|
|
1096
|
-
const el = layoutCurrentInput()
|
|
1126
|
+
const el = layoutCurrentInput(ev)
|
|
1097
1127
|
if (!el) { layoutDismiss(); layoutBadgeHide(); return }
|
|
1098
1128
|
layoutBadge(el) // #66: метка раскладки
|
|
1099
1129
|
const value = el.value || ''
|
|
@@ -1105,11 +1135,43 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1105
1135
|
if (typoLive && value && typeof formatInputLive === 'function') {
|
|
1106
1136
|
const formatted = formatInputLive(value)
|
|
1107
1137
|
if (formatted !== value) {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1138
|
+
isFormatting = true
|
|
1139
|
+
try {
|
|
1140
|
+
const sStart = el.selectionStart
|
|
1141
|
+
const sEnd = el.selectionEnd
|
|
1142
|
+
const diff = formatted.length - value.length
|
|
1143
|
+
setNativeInputValue(el, formatted)
|
|
1144
|
+
if (sStart !== null && sEnd !== null) {
|
|
1145
|
+
const nextPos = Math.max(0, sStart + diff)
|
|
1146
|
+
el.setSelectionRange(nextPos, nextPos)
|
|
1147
|
+
}
|
|
1148
|
+
} finally {
|
|
1149
|
+
isFormatting = false
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
} catch (e) { /* ignore */ }
|
|
1154
|
+
|
|
1155
|
+
// #158: Русские алиасы слэш-команд при вводе пробела после команды (например "/цель ")
|
|
1156
|
+
try {
|
|
1157
|
+
const snapVal = scope ? (scope.getSnapshot().value || {}) : {}
|
|
1158
|
+
const allowAliases = snapVal.slashAliases !== false
|
|
1159
|
+
if (allowAliases && value.startsWith('/') && typeof expandSlashAlias === 'function') {
|
|
1160
|
+
const expanded = expandSlashAlias(value)
|
|
1161
|
+
if (expanded !== value) {
|
|
1162
|
+
isFormatting = true
|
|
1163
|
+
try {
|
|
1164
|
+
const sStart = el.selectionStart
|
|
1165
|
+
const sEnd = el.selectionEnd
|
|
1166
|
+
const diff = expanded.length - value.length
|
|
1167
|
+
setNativeInputValue(el, expanded)
|
|
1168
|
+
if (sStart !== null && sEnd !== null) {
|
|
1169
|
+
const nextPos = Math.max(0, sStart + diff)
|
|
1170
|
+
el.setSelectionRange(nextPos, nextPos)
|
|
1171
|
+
}
|
|
1172
|
+
} finally {
|
|
1173
|
+
isFormatting = false
|
|
1174
|
+
}
|
|
1113
1175
|
}
|
|
1114
1176
|
}
|
|
1115
1177
|
} catch (e) { /* ignore */ }
|
|
@@ -1132,41 +1194,76 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1132
1194
|
}
|
|
1133
1195
|
const unsubscribeLayout = runtime.subscribe(layoutOnInput)
|
|
1134
1196
|
document.addEventListener('input', layoutOnInput, true)
|
|
1197
|
+
|
|
1135
1198
|
const layoutOnKeydown = (ev) => {
|
|
1199
|
+
const el = layoutCurrentInput(ev)
|
|
1200
|
+
if (!el) return
|
|
1201
|
+
|
|
1136
1202
|
// Alt+L (клавиша KeyL, Latin 'l' или русская 'д'): ручной конверт текущего инпута
|
|
1137
1203
|
const isL = ev.code === 'KeyL' || ev.key.toLowerCase() === 'l' || ev.key.toLowerCase() === 'д'
|
|
1138
1204
|
if (ev.altKey && !ev.ctrlKey && !ev.metaKey && isL) {
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
el
|
|
1146
|
-
el.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1205
|
+
const value = el.value || ''
|
|
1206
|
+
const c = layoutFixCandidate(value, 'lat2cyr') || layoutFixCandidate(value, 'cyr2lat')
|
|
1207
|
+
if (c) {
|
|
1208
|
+
ev.preventDefault()
|
|
1209
|
+
isFormatting = true
|
|
1210
|
+
try {
|
|
1211
|
+
setNativeInputValue(el, c.converted)
|
|
1147
1212
|
learnWords(c.converted) // #67
|
|
1213
|
+
} finally {
|
|
1214
|
+
isFormatting = false
|
|
1148
1215
|
}
|
|
1149
1216
|
}
|
|
1150
1217
|
}
|
|
1218
|
+
|
|
1151
1219
|
// #158: Alt+T: фонетическая транслитерация (privet <-> привет)
|
|
1152
1220
|
const isT = ev.code === 'KeyT' || ev.key.toLowerCase() === 't' || ev.key.toLowerCase() === 'е'
|
|
1153
1221
|
if (ev.altKey && !ev.ctrlKey && !ev.metaKey && isT) {
|
|
1154
|
-
|
|
1155
|
-
if (el && el.value && typeof phoneticTranslit === 'function') {
|
|
1222
|
+
if (el.value && typeof phoneticTranslit === 'function') {
|
|
1156
1223
|
ev.preventDefault()
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1224
|
+
isFormatting = true
|
|
1225
|
+
try {
|
|
1226
|
+
const sStart = el.selectionStart
|
|
1227
|
+
const sEnd = el.selectionEnd
|
|
1228
|
+
if (sStart !== null && sEnd !== null && sStart !== sEnd) {
|
|
1229
|
+
const sel = el.value.slice(sStart, sEnd)
|
|
1230
|
+
const dir = /[а-яё]/i.test(sel) ? 'cyr2lat' : 'lat2cyr'
|
|
1231
|
+
const converted = phoneticTranslit(sel, dir)
|
|
1232
|
+
const next = el.value.slice(0, sStart) + converted + el.value.slice(sEnd)
|
|
1233
|
+
setNativeInputValue(el, next)
|
|
1234
|
+
el.setSelectionRange(sStart, sStart + converted.length)
|
|
1235
|
+
} else {
|
|
1236
|
+
const dir = /[а-яё]/i.test(el.value) ? 'cyr2lat' : 'lat2cyr'
|
|
1237
|
+
const converted = phoneticTranslit(el.value, dir)
|
|
1238
|
+
setNativeInputValue(el, converted)
|
|
1239
|
+
}
|
|
1240
|
+
} finally {
|
|
1241
|
+
isFormatting = false
|
|
1242
|
+
}
|
|
1160
1243
|
}
|
|
1161
1244
|
}
|
|
1245
|
+
|
|
1162
1246
|
// #158: Разворачивание русских алиасов слэш-команд (/цель -> /goal)
|
|
1163
1247
|
if (ev.key === ' ' || ev.key === 'Enter') {
|
|
1164
|
-
|
|
1165
|
-
if (el && el.value && el.value.startsWith('/') && typeof expandSlashAlias === 'function') {
|
|
1248
|
+
if (el.value && el.value.startsWith('/') && typeof expandSlashAlias === 'function') {
|
|
1166
1249
|
const expanded = expandSlashAlias(el.value)
|
|
1167
1250
|
if (expanded !== el.value) {
|
|
1168
|
-
|
|
1169
|
-
|
|
1251
|
+
if (ev.key === ' ') {
|
|
1252
|
+
ev.preventDefault()
|
|
1253
|
+
isFormatting = true
|
|
1254
|
+
try {
|
|
1255
|
+
setNativeInputValue(el, expanded + ' ')
|
|
1256
|
+
} finally {
|
|
1257
|
+
isFormatting = false
|
|
1258
|
+
}
|
|
1259
|
+
} else if (ev.key === 'Enter') {
|
|
1260
|
+
isFormatting = true
|
|
1261
|
+
try {
|
|
1262
|
+
setNativeInputValue(el, expanded)
|
|
1263
|
+
} finally {
|
|
1264
|
+
isFormatting = false
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1170
1267
|
}
|
|
1171
1268
|
}
|
|
1172
1269
|
}
|
|
@@ -1229,6 +1326,19 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1229
1326
|
}, TranslateTurnAction),
|
|
1230
1327
|
)
|
|
1231
1328
|
} catch (err) { /* ignore if slot not declared */ }
|
|
1329
|
+
|
|
1330
|
+
// #158: Экспорт сессии в Markdown
|
|
1331
|
+
try {
|
|
1332
|
+
ctx.slots.inject('conversation.session.header.utilities', () =>
|
|
1333
|
+
ctx.slots.register({
|
|
1334
|
+
name: 'conversation.session.header.utilities',
|
|
1335
|
+
id: 'dsh-russian-lang-md-export',
|
|
1336
|
+
order: 101,
|
|
1337
|
+
locale: SETTINGS_NS_NAME,
|
|
1338
|
+
inject: () => ({ runtime }),
|
|
1339
|
+
}, ExportMarkdownButton),
|
|
1340
|
+
)
|
|
1341
|
+
} catch (err) { /* ignore if slot not declared */ }
|
|
1232
1342
|
}
|
|
1233
1343
|
|
|
1234
1344
|
// Карточка настроек: React-компонент вне apply.
|
|
@@ -1260,25 +1370,178 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1260
1370
|
}, React.createElement('span', { className: 'rl-lang-text' }, isRu ? 'RU' : 'EN'))
|
|
1261
1371
|
}
|
|
1262
1372
|
|
|
1373
|
+
// #158: Кнопка экспорта в Markdown
|
|
1374
|
+
function ExportMarkdownButton(props) {
|
|
1375
|
+
const [done, setDone] = React.useState(false)
|
|
1376
|
+
const onExport = () => {
|
|
1377
|
+
try {
|
|
1378
|
+
const titleEl = document.querySelector('.dsw-session-title, [data-session-title], header h1, header h2')
|
|
1379
|
+
const title = (titleEl && titleEl.textContent.trim()) || document.title || 'Диалог DSH'
|
|
1380
|
+
const messageNodes = document.querySelectorAll(
|
|
1381
|
+
'[data-turn-id], [data-message-role], .dsw-turn-node, .dsw-chat-message, [data-turn-tail]'
|
|
1382
|
+
)
|
|
1383
|
+
const messages = []
|
|
1384
|
+
const seen = new Set()
|
|
1385
|
+
messageNodes.forEach((node) => {
|
|
1386
|
+
let role = 'assistant'
|
|
1387
|
+
const roleAttr = node.getAttribute('data-message-role')
|
|
1388
|
+
if (roleAttr === 'user' || node.classList.contains('dsw-user-message') || node.querySelector('[data-role="user"]')) {
|
|
1389
|
+
role = 'user'
|
|
1390
|
+
}
|
|
1391
|
+
const prose = node.querySelector('.dsw-prose, [data-block-kind="text"], .dsw-markdown-view, p')
|
|
1392
|
+
let text = (prose ? prose.innerText : node.innerText) || ''
|
|
1393
|
+
text = text.trim()
|
|
1394
|
+
if (!text || seen.has(text)) return
|
|
1395
|
+
seen.add(text)
|
|
1396
|
+
messages.push({ role, content: text })
|
|
1397
|
+
})
|
|
1398
|
+
|
|
1399
|
+
const session = {
|
|
1400
|
+
title,
|
|
1401
|
+
createdAt: new Date().toISOString(),
|
|
1402
|
+
messages
|
|
1403
|
+
}
|
|
1404
|
+
const md = exportSessionToMarkdown(session)
|
|
1405
|
+
const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' })
|
|
1406
|
+
const url = URL.createObjectURL(blob)
|
|
1407
|
+
const a = document.createElement('a')
|
|
1408
|
+
const safeTitle = (title || 'dialog').replace(/[/\\?%*:|"<>]/g, '-').slice(0, 50)
|
|
1409
|
+
a.href = url
|
|
1410
|
+
a.download = safeTitle + '-' + new Date().toISOString().slice(0, 10) + '.md'
|
|
1411
|
+
document.body.appendChild(a)
|
|
1412
|
+
a.click()
|
|
1413
|
+
document.body.removeChild(a)
|
|
1414
|
+
URL.revokeObjectURL(url)
|
|
1415
|
+
setDone(true)
|
|
1416
|
+
setTimeout(() => setDone(false), 2000)
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
console.warn('dsh-russian-lang: export md failed', err)
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
return React.createElement('button', {
|
|
1423
|
+
type: 'button',
|
|
1424
|
+
className: 'rl-lang-chip rl-export-md-btn',
|
|
1425
|
+
title: 'Экспорт диалога в Markdown (.md)',
|
|
1426
|
+
onClick: onExport,
|
|
1427
|
+
}, React.createElement('span', { className: 'rl-lang-text' }, done ? '✓ MD' : '📥 MD'))
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
async function translateTurnContent(text) {
|
|
1431
|
+
if (!text || typeof text !== 'string') return ''
|
|
1432
|
+
const cyr = (text.match(/[\u0430-\u044f\u0451]/gi) || []).length
|
|
1433
|
+
const lat = (text.match(/[a-z]/gi) || []).length
|
|
1434
|
+
if (cyr > lat && cyr > 15) return text
|
|
1435
|
+
|
|
1436
|
+
const parts = text.split(/(```[\s\S]*?```)/g)
|
|
1437
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
1438
|
+
const chunk = parts[i].trim()
|
|
1439
|
+
if (!chunk) continue
|
|
1440
|
+
const paragraphs = chunk.split(/\n\n+/)
|
|
1441
|
+
const translatedParas = []
|
|
1442
|
+
for (const para of paragraphs) {
|
|
1443
|
+
const pTrim = para.trim()
|
|
1444
|
+
if (!pTrim) continue
|
|
1445
|
+
try {
|
|
1446
|
+
const url = 'https://api.mymemory.translated.net/get?q=' + encodeURIComponent(pTrim) + '&langpair=en|ru'
|
|
1447
|
+
const res = await fetch(url)
|
|
1448
|
+
if (res.ok) {
|
|
1449
|
+
const data = await res.json()
|
|
1450
|
+
if (data && data.responseData && data.responseData.translatedText) {
|
|
1451
|
+
translatedParas.push(data.responseData.translatedText)
|
|
1452
|
+
continue
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
try {
|
|
1457
|
+
const gurl = 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=ru&dt=t&q=' + encodeURIComponent(pTrim)
|
|
1458
|
+
const gres = await fetch(gurl)
|
|
1459
|
+
if (gres.ok) {
|
|
1460
|
+
const gdata = await gres.json()
|
|
1461
|
+
if (Array.isArray(gdata) && Array.isArray(gdata[0])) {
|
|
1462
|
+
translatedParas.push(gdata[0].map((it) => it[0]).join(''))
|
|
1463
|
+
continue
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
} catch (e2) {}
|
|
1467
|
+
}
|
|
1468
|
+
translatedParas.push(pTrim)
|
|
1469
|
+
}
|
|
1470
|
+
parts[i] = translatedParas.join('\n\n')
|
|
1471
|
+
}
|
|
1472
|
+
return parts.join('\n\n')
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1263
1475
|
// #158: Кнопка перевода реплики на русский
|
|
1264
1476
|
function TranslateTurnAction(props) {
|
|
1265
1477
|
const t = typeof props.t === 'function' ? props.t : ((k) => k)
|
|
1266
|
-
const [
|
|
1478
|
+
const [loading, setLoading] = React.useState(false)
|
|
1479
|
+
const [open, setOpen] = React.useState(false)
|
|
1480
|
+
|
|
1267
1481
|
return React.createElement('button', {
|
|
1268
1482
|
type: 'button',
|
|
1269
|
-
className: 'rl-action-btn',
|
|
1483
|
+
className: 'rl-action-btn' + (open ? ' rl-action-btn-active' : ''),
|
|
1270
1484
|
title: t('translateTurn'),
|
|
1271
|
-
onClick: (ev) => {
|
|
1485
|
+
onClick: async (ev) => {
|
|
1272
1486
|
ev.stopPropagation()
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1487
|
+
const btn = ev.currentTarget
|
|
1488
|
+
const turn = btn.closest('[data-turn-tail], [data-turn-id], .osXY9a_root, [data-message-role="assistant"], .dsw-turn-node') || btn.parentElement.parentElement
|
|
1489
|
+
if (!turn) return
|
|
1490
|
+
|
|
1491
|
+
let box = turn.querySelector('.rl-turn-translation')
|
|
1492
|
+
if (box) {
|
|
1493
|
+
box.style.display = box.style.display === 'none' ? 'block' : 'none'
|
|
1494
|
+
setOpen(box.style.display !== 'none')
|
|
1495
|
+
return
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
setLoading(true)
|
|
1499
|
+
try {
|
|
1500
|
+
const prose = turn.querySelector('.dsw-prose, [data-block-kind="text"], .dsw-markdown-view, p')
|
|
1501
|
+
const rawText = (prose ? prose.innerText : turn.innerText) || ''
|
|
1502
|
+
const translated = await translateTurnContent(rawText)
|
|
1503
|
+
|
|
1504
|
+
box = document.createElement('div')
|
|
1505
|
+
box.className = 'rl-turn-translation'
|
|
1506
|
+
box.innerHTML = '<div class="rl-trans-head">' +
|
|
1507
|
+
'<span class="rl-trans-title">🌐 Перевод на русский</span>' +
|
|
1508
|
+
'<div class="rl-trans-tools">' +
|
|
1509
|
+
'<button type="button" class="rl-trans-btn rl-btn-copy" title="Скопировать перевод">📋 Копировать</button>' +
|
|
1510
|
+
'<button type="button" class="rl-trans-btn rl-btn-close" title="Закрыть">✕</button>' +
|
|
1511
|
+
'</div>' +
|
|
1512
|
+
'</div>' +
|
|
1513
|
+
'<div class="rl-trans-body"></div>'
|
|
1514
|
+
box.querySelector('.rl-trans-body').textContent = translated
|
|
1515
|
+
|
|
1516
|
+
const copyBtn = box.querySelector('.rl-btn-copy')
|
|
1517
|
+
copyBtn.addEventListener('click', (e) => {
|
|
1518
|
+
e.stopPropagation()
|
|
1519
|
+
try { navigator.clipboard.writeText(translated) } catch (err) {}
|
|
1520
|
+
copyBtn.textContent = '✓ Скопировано'
|
|
1521
|
+
setTimeout(() => { copyBtn.textContent = '📋 Копировать' }, 2000)
|
|
1522
|
+
})
|
|
1523
|
+
|
|
1524
|
+
const closeBtn = box.querySelector('.rl-btn-close')
|
|
1525
|
+
closeBtn.addEventListener('click', (e) => {
|
|
1526
|
+
e.stopPropagation()
|
|
1527
|
+
box.style.display = 'none'
|
|
1528
|
+
setOpen(false)
|
|
1529
|
+
})
|
|
1530
|
+
|
|
1531
|
+
const actionsBar = btn.closest('.dsw-turn-actions, [data-turn-actions]') || btn.parentElement
|
|
1532
|
+
if (actionsBar && actionsBar.parentNode) {
|
|
1533
|
+
actionsBar.parentNode.insertBefore(box, actionsBar)
|
|
1534
|
+
} else {
|
|
1535
|
+
turn.appendChild(box)
|
|
1536
|
+
}
|
|
1537
|
+
setOpen(true)
|
|
1538
|
+
} catch (err) {
|
|
1539
|
+
console.warn('dsh-russian-lang: translate turn failed', err)
|
|
1540
|
+
} finally {
|
|
1541
|
+
setLoading(false)
|
|
1277
1542
|
}
|
|
1278
|
-
setDone(true)
|
|
1279
|
-
setTimeout(() => setDone(false), 2000)
|
|
1280
1543
|
}
|
|
1281
|
-
}, React.createElement('span', null,
|
|
1544
|
+
}, React.createElement('span', null, loading ? '⏳' : (open ? 'RU ✓' : 'RU ↗')))
|
|
1282
1545
|
}
|
|
1283
1546
|
|
|
1284
1547
|
function SettingsCard(props) {
|
|
@@ -1387,8 +1650,14 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1387
1650
|
row(t('enabled'), checkbox(ruActive, onEnabled, false), null),
|
|
1388
1651
|
row(t('typography'), checkbox(typography.enabled !== false && ruActive,
|
|
1389
1652
|
(ev) => setTypo({ enabled: ev.target.checked }), !ruActive), t('typographyDesc')),
|
|
1653
|
+
row(t('liveInput'), checkbox(typography.liveInput !== false && ruActive,
|
|
1654
|
+
(ev) => setTypo({ liveInput: ev.target.checked }), !ruActive), t('liveInputDesc')),
|
|
1390
1655
|
row(t('yo'), checkbox(typography.yo === true,
|
|
1391
1656
|
(ev) => setTypo({ yo: ev.target.checked }), !ruActive), t('yoDesc')),
|
|
1657
|
+
row(t('slashAliases'), checkbox(value.slashAliases !== false && ruActive,
|
|
1658
|
+
(ev) => {
|
|
1659
|
+
try { scope.set('slashAliases', ev.target.checked) } catch (err) {}
|
|
1660
|
+
}, !ruActive), t('slashAliasesDesc')),
|
|
1392
1661
|
row(t('agentPrompt'), checkbox(value.agentPrompt === true,
|
|
1393
1662
|
(ev) => {
|
|
1394
1663
|
try {
|
|
@@ -1400,12 +1669,31 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1400
1669
|
console.warn('dsh-russian-lang: scope.set agentPrompt sync threw', err && err.message || err)
|
|
1401
1670
|
}
|
|
1402
1671
|
}, false), t('agentPromptDesc')),
|
|
1672
|
+
row(t('agentPromptPreset'),
|
|
1673
|
+
React.createElement('select', {
|
|
1674
|
+
className: 'rl-select',
|
|
1675
|
+
value: value.agentPromptPreset || 'technical_expert',
|
|
1676
|
+
disabled: !value.agentPrompt || !ruActive,
|
|
1677
|
+
onChange: (ev) => {
|
|
1678
|
+
try {
|
|
1679
|
+
const val = ev.target.value
|
|
1680
|
+
scope.set('agentPromptPreset', val)
|
|
1681
|
+
} catch (err) { console.warn('dsh-russian-lang: set agentPromptPreset failed', err) }
|
|
1682
|
+
}
|
|
1683
|
+
},
|
|
1684
|
+
React.createElement('option', { value: 'technical_expert' }, t('presetExpert')),
|
|
1685
|
+
React.createElement('option', { value: 'tech_writer' }, t('presetWriter')),
|
|
1686
|
+
React.createElement('option', { value: 'concise' }, t('presetConcise'))
|
|
1687
|
+
),
|
|
1688
|
+
t('agentPromptDesc')
|
|
1689
|
+
),
|
|
1403
1690
|
React.createElement('div', { className: 'rl-note' },
|
|
1404
1691
|
t('overridesCount') + ': ' + overridesCount),
|
|
1405
1692
|
React.createElement('div', { className: 'rl-hint' }, t('altL')),
|
|
1693
|
+
React.createElement('div', { className: 'rl-hint' }, t('altT')),
|
|
1406
1694
|
React.createElement('div', { className: 'rl-actions' },
|
|
1407
1695
|
React.createElement('a', {
|
|
1408
|
-
href: makeIssueUrl({}, '0.2.
|
|
1696
|
+
href: makeIssueUrl({}, '0.2.9'),
|
|
1409
1697
|
target: '_blank',
|
|
1410
1698
|
rel: 'noopener noreferrer',
|
|
1411
1699
|
className: 'rl-link'
|
|
@@ -1438,6 +1726,15 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1438
1726
|
'.rl-lang-chip-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}',
|
|
1439
1727
|
'.rl-action-btn{appearance:none;background:0 0;border:1px solid transparent;border-radius:4px;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:11px;padding:2px 5px;display:inline-flex;align-items:center;transition:all .15s}',
|
|
1440
1728
|
'.rl-action-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3)}',
|
|
1729
|
+
'.rl-action-btn-active{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2)}',
|
|
1730
|
+
'.rl-turn-translation{margin:8px 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;font-size:13px;line-height:1.5}',
|
|
1731
|
+
'.rl-trans-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:4px}',
|
|
1732
|
+
'.rl-trans-title{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:0.5px}',
|
|
1733
|
+
'.rl-trans-tools{display:flex;gap:4px}',
|
|
1734
|
+
'.rl-trans-btn{appearance:none;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);cursor:pointer;padding:2px 7px;border-radius:4px;font-size:11px}',
|
|
1735
|
+
'.rl-trans-btn:hover{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}',
|
|
1736
|
+
'.rl-trans-body{color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;user-select:text}',
|
|
1737
|
+
'.rl-export-md-btn{font-weight:600}',
|
|
1441
1738
|
'.rl-select{height:30px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 8px;font-size:12px;outline:none;margin-top:4px}',
|
|
1442
1739
|
].join('\n')
|
|
1443
1740
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="rl-card"]')) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-russian-lang",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
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",
|