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.
@@ -1,25 +1,29 @@
1
- import { createHash } from 'node:crypto'
2
-
3
- /** Revision of translation-affecting settings, without disclosing key values. */
4
- export function translationNamespace(config, credentialRevision = 0) {
5
- const providers = Object.keys(config.providers || {}).sort().map(id => {
6
- const p = config.providers[id]
7
- return [id, p.type, p.enabled, p.source, p.baseURL, p.model, p.apiKeyEnv, p.apiKeyRef]
8
- })
9
- return createHash('sha256').update(JSON.stringify(['protected-v3-glossary', config.chain, config.fallback, providers, config.glossary, credentialRevision])).digest('hex')
10
- }
11
-
12
- /** Effective Loader state, not mere installation, determines mutual exclusion. */
13
- export function officialTranslationState(ctx) {
14
- const loader = ctx?.get?.('loader')
15
- if (loader?.entries) {
16
- const official = new Set(['@deepseek-ai/dsh-experimental-client-ui-cot-translation'])
17
- for (const entry of loader.entries()) {
18
- if (official.has(entry.options?.name) && !entry.disabled && ![3, 4, 5].includes(entry.fiber?.state)) {
19
- return { officialEnabled: true, conflictDetection: 'available' }
20
- }
21
- }
22
- return { officialEnabled: !!ctx.get('cotTranslation'), conflictDetection: 'available' }
23
- }
24
- return { officialEnabled: !!ctx?.get?.('cotTranslation'), conflictDetection: 'unavailable' }
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.19",
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",