dsh-think-translate 1.2.19 → 1.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.de.md +14 -8
- package/README.es.md +14 -8
- package/README.fr.md +14 -8
- package/README.ja.md +14 -8
- package/README.ko.md +14 -8
- package/README.md +14 -8
- package/README.pt.md +14 -8
- package/README.ru.md +14 -8
- package/README.zh-CN.md +262 -252
- package/docs/official-translation-comparison.md +35 -35
- package/lib/client.js +3627 -3529
- package/lib/credentials.js +54 -54
- package/lib/glossary-presets.js +54 -0
- package/lib/glossary.js +151 -138
- package/lib/index.js +1740 -1739
- package/lib/runtime-state.js +29 -25
- package/lib/text.js +111 -111
- package/package.json +2 -1
package/lib/runtime-state.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { BUILTIN_GLOSSARY } from './glossary-presets.js'
|
|
3
|
+
|
|
4
|
+
const builtinRevision = createHash('sha256').update(JSON.stringify(BUILTIN_GLOSSARY)).digest('hex')
|
|
5
|
+
|
|
6
|
+
/** Revision of translation-affecting settings, without disclosing key values. */
|
|
7
|
+
export function translationNamespace(config, credentialRevision = 0) {
|
|
8
|
+
const providers = Object.keys(config.providers || {}).sort().map(id => {
|
|
9
|
+
const p = config.providers[id]
|
|
10
|
+
return [id, p.type, p.enabled, p.source, p.baseURL, p.model, p.apiKeyEnv, p.apiKeyRef]
|
|
11
|
+
})
|
|
12
|
+
return createHash('sha256').update(JSON.stringify(['protected-v4-independent-glossary', config.chain, config.fallback, providers, config.glossary,
|
|
13
|
+
config.glossary?.builtin === true ? builtinRevision : null, credentialRevision])).digest('hex')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Effective Loader state, not mere installation, determines mutual exclusion. */
|
|
17
|
+
export function officialTranslationState(ctx) {
|
|
18
|
+
const loader = ctx?.get?.('loader')
|
|
19
|
+
if (loader?.entries) {
|
|
20
|
+
const official = new Set(['@deepseek-ai/dsh-experimental-client-ui-cot-translation'])
|
|
21
|
+
for (const entry of loader.entries()) {
|
|
22
|
+
if (official.has(entry.options?.name) && !entry.disabled && ![3, 4, 5].includes(entry.fiber?.state)) {
|
|
23
|
+
return { officialEnabled: true, conflictDetection: 'available' }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { officialEnabled: !!ctx.get('cotTranslation'), conflictDetection: 'available' }
|
|
27
|
+
}
|
|
28
|
+
return { officialEnabled: !!ctx?.get?.('cotTranslation'), conflictDetection: 'unavailable' }
|
|
29
|
+
}
|
package/lib/text.js
CHANGED
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
// Code spans keep their exact source offsets so neither translation nor batching
|
|
2
|
-
// needs placeholders that a provider could translate, remove, or reorder.
|
|
3
|
-
export function protectedTextParts(text) {
|
|
4
|
-
const parts = []
|
|
5
|
-
let proseStart = 0
|
|
6
|
-
let cursor = 0
|
|
7
|
-
function protect(start, end, complete) {
|
|
8
|
-
if (start > proseStart) parts.push({ text: text.slice(proseStart, start), start: proseStart, end: start, code: false, complete: true })
|
|
9
|
-
parts.push({ text: text.slice(start, end), start, end, code: true, complete })
|
|
10
|
-
proseStart = end
|
|
11
|
-
cursor = end
|
|
12
|
-
}
|
|
13
|
-
function lineEnd(start) {
|
|
14
|
-
const newline = text.indexOf('\n', start)
|
|
15
|
-
return newline < 0 ? text.length : newline + 1
|
|
16
|
-
}
|
|
17
|
-
function technicalSpan(start) {
|
|
18
|
-
const before = text[start - 1] || ''
|
|
19
|
-
if (before && /[A-Za-z0-9_\\/]/.test(before)) return 0
|
|
20
|
-
const rest = text.slice(start)
|
|
21
|
-
// Quotes are part of a path token only when its prefix is unambiguous.
|
|
22
|
-
const quoted = rest.match(/^("|'|“)(?:[A-Za-z]:[\\/]|\\\\|~?\/|\.\.?\/)/)
|
|
23
|
-
if (quoted) {
|
|
24
|
-
const close = quoted[1] === '“' ? '”' : quoted[1]
|
|
25
|
-
const end = text.indexOf(close, start + 1)
|
|
26
|
-
return end < 0 ? text.length - start : end + 1 - start
|
|
27
|
-
}
|
|
28
|
-
const token = rest.match(/^(?:https?:\/\/|file:\/\/|[A-Za-z]:[\\/]|\\\\|~?\/|\.\.?\/)[^\s<>"'`,。!?;:、]+/)
|
|
29
|
-
|| rest.match(/^[\w@.-]+\.(?:[cm]?js|jsx|tsx?|jsonl?|ya?ml|toml|ini|env|md|py|sh|ps1|cmd|bat|exe|lock|go|rs|cpp|sql|html|css|txt|zstd|png|jpe?g|svg)(?![\w.])/i)
|
|
30
|
-
if (token) {
|
|
31
|
-
let value = token[0].replace(/[.,;:!?,。!?;:、]+$/, '')
|
|
32
|
-
// Keep balanced URL parentheses, but leave Markdown/prose delimiters out.
|
|
33
|
-
for (const [open, close] of [['(', ')'], ['[', ']'], ['{', '}']]) {
|
|
34
|
-
while (value.endsWith(close) && value.split(close).length > value.split(open).length) value = value.slice(0, -1)
|
|
35
|
-
}
|
|
36
|
-
return value.length
|
|
37
|
-
}
|
|
38
|
-
const command = rest.match(/^(?:(?:npm|pnpm|yarn)\s+(?:run\s+[\w:.-]+|test|ci|install|build|start)|git\s+(?:status|diff|log|fetch|pull|push|show|branch)|(?:python3?|node)\s+[\w./\\:-]+\.(?:py|[cm]?js))(?=$|[\s.,;:!?,。!?;:、)]|--)(?:[ \t]+(?:--?[\w-]+(?:=[\w./\\:@=-]+)?|--|"[^"\r\n]*"|'[^'\r\n]*'))*/)
|
|
39
|
-
return command ? command[0].length : 0
|
|
40
|
-
}
|
|
41
|
-
while (cursor < text.length) {
|
|
42
|
-
if (cursor === 0 || text[cursor - 1] === '\n') {
|
|
43
|
-
const end = lineEnd(cursor)
|
|
44
|
-
const line = text.slice(cursor, end)
|
|
45
|
-
// Treat indented/quoted fences conservatively as code as well.
|
|
46
|
-
const fence = line.match(/^[\t ]*(?:>[\t ]*)*(?:(?:[-+*]|[0-9]+[.)])[\t ]+)?(`{3,}|~{3,})[^\r\n]*(?:\r?\n)?$/)
|
|
47
|
-
if (fence) {
|
|
48
|
-
const close = new RegExp('^[\\t ]*(?:>[\\t ]*)*' + fence[1][0] + '{' + fence[1].length + ',}[\\t ]*(?:\\r?\\n)?$')
|
|
49
|
-
let stop = end
|
|
50
|
-
let complete = false
|
|
51
|
-
while (stop < text.length) {
|
|
52
|
-
const next = lineEnd(stop)
|
|
53
|
-
if (close.test(text.slice(stop, next))) { stop = next; complete = true; break }
|
|
54
|
-
stop = next
|
|
55
|
-
}
|
|
56
|
-
protect(cursor, stop, complete)
|
|
57
|
-
continue
|
|
58
|
-
}
|
|
59
|
-
if (/^(?: {4}|\t)/.test(line)) {
|
|
60
|
-
protect(cursor, end, end < text.length || line.endsWith('\n'))
|
|
61
|
-
continue
|
|
62
|
-
}
|
|
63
|
-
const shellLine = /^(?:\$\s*)?(?:(?:npm|pnpm|yarn)\s+(?:run|test|ci|install|add|remove|uninstall|build|start)\b|git\s+(?:status|diff|log|fetch|pull|push|show|branch|checkout|switch|commit|add)\b|(?:curl|wget)\s+-)/.test(line)
|
|
64
|
-
if (shellLine && !/\b(?:and|then|before|after|because|please|shows|prints|returns|failed|succeeded|should|will|can|is|are)\b/.test(line)) {
|
|
65
|
-
protect(cursor, end, end < text.length || line.endsWith('\n'))
|
|
66
|
-
continue
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (text[cursor] === '`') {
|
|
70
|
-
let slashes = 0
|
|
71
|
-
for (let i = cursor - 1; i >= 0 && text[i] === '\\'; i--) slashes++
|
|
72
|
-
if (slashes % 2) { cursor++; continue }
|
|
73
|
-
let after = cursor + 1
|
|
74
|
-
while (text[after] === '`') after++
|
|
75
|
-
const size = after - cursor
|
|
76
|
-
let next = after
|
|
77
|
-
let stop = text.length
|
|
78
|
-
let complete = false
|
|
79
|
-
while ((next = text.indexOf('`', next)) >= 0) {
|
|
80
|
-
let end = next + 1
|
|
81
|
-
while (text[end] === '`') end++
|
|
82
|
-
if (end - next === size) { stop = end; complete = true; break }
|
|
83
|
-
next = end
|
|
84
|
-
}
|
|
85
|
-
// An unfinished streaming code span must not expose its body as prose.
|
|
86
|
-
protect(cursor, stop, complete)
|
|
87
|
-
continue
|
|
88
|
-
}
|
|
89
|
-
const technicalLength = technicalSpan(cursor)
|
|
90
|
-
if (technicalLength) {
|
|
91
|
-
protect(cursor, cursor + technicalLength, true)
|
|
92
|
-
continue
|
|
93
|
-
}
|
|
94
|
-
cursor++
|
|
95
|
-
}
|
|
96
|
-
if (proseStart < text.length) parts.push({ text: text.slice(proseStart), start: proseStart, end: text.length, code: false, complete: true })
|
|
97
|
-
return parts
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Only prose reaches a provider; separators adjoining code stay byte-for-byte.
|
|
101
|
-
export async function translateProtectedText(text, translate, split = value => [value]) {
|
|
102
|
-
const output = []
|
|
103
|
-
for (const part of protectedTextParts(text)) {
|
|
104
|
-
const match = !part.code && part.text.match(/^(\s*)([\s\S]*?)(\s*)$/)
|
|
105
|
-
if (!match || !/\p{L}/u.test(match[2])) { output.push(part.text); continue }
|
|
106
|
-
const chunks = []
|
|
107
|
-
for (const chunk of split(match[2])) chunks.push((await translate(chunk)).trim())
|
|
108
|
-
output.push(match[1] + chunks.join('\n\n') + match[3])
|
|
109
|
-
}
|
|
110
|
-
return output.join('')
|
|
111
|
-
}
|
|
1
|
+
// Code spans keep their exact source offsets so neither translation nor batching
|
|
2
|
+
// needs placeholders that a provider could translate, remove, or reorder.
|
|
3
|
+
export function protectedTextParts(text) {
|
|
4
|
+
const parts = []
|
|
5
|
+
let proseStart = 0
|
|
6
|
+
let cursor = 0
|
|
7
|
+
function protect(start, end, complete) {
|
|
8
|
+
if (start > proseStart) parts.push({ text: text.slice(proseStart, start), start: proseStart, end: start, code: false, complete: true })
|
|
9
|
+
parts.push({ text: text.slice(start, end), start, end, code: true, complete })
|
|
10
|
+
proseStart = end
|
|
11
|
+
cursor = end
|
|
12
|
+
}
|
|
13
|
+
function lineEnd(start) {
|
|
14
|
+
const newline = text.indexOf('\n', start)
|
|
15
|
+
return newline < 0 ? text.length : newline + 1
|
|
16
|
+
}
|
|
17
|
+
function technicalSpan(start) {
|
|
18
|
+
const before = text[start - 1] || ''
|
|
19
|
+
if (before && /[A-Za-z0-9_\\/]/.test(before)) return 0
|
|
20
|
+
const rest = text.slice(start)
|
|
21
|
+
// Quotes are part of a path token only when its prefix is unambiguous.
|
|
22
|
+
const quoted = rest.match(/^("|'|“)(?:[A-Za-z]:[\\/]|\\\\|~?\/|\.\.?\/)/)
|
|
23
|
+
if (quoted) {
|
|
24
|
+
const close = quoted[1] === '“' ? '”' : quoted[1]
|
|
25
|
+
const end = text.indexOf(close, start + 1)
|
|
26
|
+
return end < 0 ? text.length - start : end + 1 - start
|
|
27
|
+
}
|
|
28
|
+
const token = rest.match(/^(?:https?:\/\/|file:\/\/|[A-Za-z]:[\\/]|\\\\|~?\/|\.\.?\/)[^\s<>"'`,。!?;:、]+/)
|
|
29
|
+
|| rest.match(/^[\w@.-]+\.(?:[cm]?js|jsx|tsx?|jsonl?|ya?ml|toml|ini|env|md|py|sh|ps1|cmd|bat|exe|lock|go|rs|cpp|sql|html|css|txt|zstd|png|jpe?g|svg)(?![\w.])/i)
|
|
30
|
+
if (token) {
|
|
31
|
+
let value = token[0].replace(/[.,;:!?,。!?;:、]+$/, '')
|
|
32
|
+
// Keep balanced URL parentheses, but leave Markdown/prose delimiters out.
|
|
33
|
+
for (const [open, close] of [['(', ')'], ['[', ']'], ['{', '}']]) {
|
|
34
|
+
while (value.endsWith(close) && value.split(close).length > value.split(open).length) value = value.slice(0, -1)
|
|
35
|
+
}
|
|
36
|
+
return value.length
|
|
37
|
+
}
|
|
38
|
+
const command = rest.match(/^(?:(?:npm|pnpm|yarn)\s+(?:run\s+[\w:.-]+|test|ci|install|build|start)|git\s+(?:status|diff|log|fetch|pull|push|show|branch)|(?:python3?|node)\s+[\w./\\:-]+\.(?:py|[cm]?js))(?=$|[\s.,;:!?,。!?;:、)]|--)(?:[ \t]+(?:--?[\w-]+(?:=[\w./\\:@=-]+)?|--|"[^"\r\n]*"|'[^'\r\n]*'))*/)
|
|
39
|
+
return command ? command[0].length : 0
|
|
40
|
+
}
|
|
41
|
+
while (cursor < text.length) {
|
|
42
|
+
if (cursor === 0 || text[cursor - 1] === '\n') {
|
|
43
|
+
const end = lineEnd(cursor)
|
|
44
|
+
const line = text.slice(cursor, end)
|
|
45
|
+
// Treat indented/quoted fences conservatively as code as well.
|
|
46
|
+
const fence = line.match(/^[\t ]*(?:>[\t ]*)*(?:(?:[-+*]|[0-9]+[.)])[\t ]+)?(`{3,}|~{3,})[^\r\n]*(?:\r?\n)?$/)
|
|
47
|
+
if (fence) {
|
|
48
|
+
const close = new RegExp('^[\\t ]*(?:>[\\t ]*)*' + fence[1][0] + '{' + fence[1].length + ',}[\\t ]*(?:\\r?\\n)?$')
|
|
49
|
+
let stop = end
|
|
50
|
+
let complete = false
|
|
51
|
+
while (stop < text.length) {
|
|
52
|
+
const next = lineEnd(stop)
|
|
53
|
+
if (close.test(text.slice(stop, next))) { stop = next; complete = true; break }
|
|
54
|
+
stop = next
|
|
55
|
+
}
|
|
56
|
+
protect(cursor, stop, complete)
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (/^(?: {4}|\t)/.test(line)) {
|
|
60
|
+
protect(cursor, end, end < text.length || line.endsWith('\n'))
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
const shellLine = /^(?:\$\s*)?(?:(?:npm|pnpm|yarn)\s+(?:run|test|ci|install|add|remove|uninstall|build|start)\b|git\s+(?:status|diff|log|fetch|pull|push|show|branch|checkout|switch|commit|add)\b|(?:curl|wget)\s+-)/.test(line)
|
|
64
|
+
if (shellLine && !/\b(?:and|then|before|after|because|please|shows|prints|returns|failed|succeeded|should|will|can|is|are)\b/.test(line)) {
|
|
65
|
+
protect(cursor, end, end < text.length || line.endsWith('\n'))
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (text[cursor] === '`') {
|
|
70
|
+
let slashes = 0
|
|
71
|
+
for (let i = cursor - 1; i >= 0 && text[i] === '\\'; i--) slashes++
|
|
72
|
+
if (slashes % 2) { cursor++; continue }
|
|
73
|
+
let after = cursor + 1
|
|
74
|
+
while (text[after] === '`') after++
|
|
75
|
+
const size = after - cursor
|
|
76
|
+
let next = after
|
|
77
|
+
let stop = text.length
|
|
78
|
+
let complete = false
|
|
79
|
+
while ((next = text.indexOf('`', next)) >= 0) {
|
|
80
|
+
let end = next + 1
|
|
81
|
+
while (text[end] === '`') end++
|
|
82
|
+
if (end - next === size) { stop = end; complete = true; break }
|
|
83
|
+
next = end
|
|
84
|
+
}
|
|
85
|
+
// An unfinished streaming code span must not expose its body as prose.
|
|
86
|
+
protect(cursor, stop, complete)
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
const technicalLength = technicalSpan(cursor)
|
|
90
|
+
if (technicalLength) {
|
|
91
|
+
protect(cursor, cursor + technicalLength, true)
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
cursor++
|
|
95
|
+
}
|
|
96
|
+
if (proseStart < text.length) parts.push({ text: text.slice(proseStart), start: proseStart, end: text.length, code: false, complete: true })
|
|
97
|
+
return parts
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Only prose reaches a provider; separators adjoining code stay byte-for-byte.
|
|
101
|
+
export async function translateProtectedText(text, translate, split = value => [value]) {
|
|
102
|
+
const output = []
|
|
103
|
+
for (const part of protectedTextParts(text)) {
|
|
104
|
+
const match = !part.code && part.text.match(/^(\s*)([\s\S]*?)(\s*)$/)
|
|
105
|
+
if (!match || !/\p{L}/u.test(match[2])) { output.push(part.text); continue }
|
|
106
|
+
const chunks = []
|
|
107
|
+
for (const chunk of split(match[2])) chunks.push((await translate(chunk)).trim())
|
|
108
|
+
output.push(match[1] + chunks.join('\n\n') + match[3])
|
|
109
|
+
}
|
|
110
|
+
return output.join('')
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-think-translate",
|
|
3
3
|
"description": "Display-layer translation for the DSH web app: thinking chain, task cards, and optional answer text, in 9 target languages (local Ollama / any OpenAI-compatible or Anthropic endpoint / Google / Bing, fail-open)",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.21",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"lib/credentials.js",
|
|
21
21
|
"lib/runtime-state.js",
|
|
22
22
|
"lib/glossary.js",
|
|
23
|
+
"lib/glossary-presets.js",
|
|
23
24
|
"cordis.patch.yml",
|
|
24
25
|
"docs/official-translation-comparison.md",
|
|
25
26
|
"docs/first-release-evidence.json",
|