@shipi18n/core 2.4.0 → 2.6.0
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/CHANGELOG.md +15 -0
- package/README.md +14 -1
- package/package.json +8 -2
- package/src/adapters/index.js +19 -3
- package/src/index.js +4 -0
- package/src/reporters.js +217 -0
- package/src/review.js +2 -1
- package/src/translate.js +3 -2
- package/src/tree.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @shipi18n/core
|
|
2
2
|
|
|
3
|
+
## 2.6.0
|
|
4
|
+
|
|
5
|
+
- New: reporters (`humanReport`, `jsonReport`, `sarifReport`, `junitReport`, `REPORTERS`,
|
|
6
|
+
`RULE_META`) and `verdict` lifted from the CLI into core, so any surface — the GitHub Action
|
|
7
|
+
first — can run a full `check` with exit-code semantics and SARIF output without depending on
|
|
8
|
+
the CLI. The CLI re-exports every name; nothing breaks.
|
|
9
|
+
|
|
10
|
+
## 2.5.0
|
|
11
|
+
|
|
12
|
+
- New: the `openai` adapter accepts `baseURL`, pointing it at any OpenAI-compatible endpoint —
|
|
13
|
+
Ollama (no key needed, fully offline), Gemini's compatibility endpoint, Groq, Mistral, LM Studio,
|
|
14
|
+
vLLM, corporate gateways. `translateJSON`, `reviewTranslations` and `runSemantic` all take and
|
|
15
|
+
thread it through. When a `baseURL` is set and no key is given, a placeholder key is sent instead
|
|
16
|
+
of failing, since servers like Ollama accept anything.
|
|
17
|
+
|
|
3
18
|
## 2.4.0
|
|
4
19
|
|
|
5
20
|
- Fix: `runSemantic` now returns `excluded` — the number of pairs it skipped because the key already
|
package/README.md
CHANGED
|
@@ -36,6 +36,19 @@ console.log(stats) // { translated, reused, placeholderWarnings }
|
|
|
36
36
|
The API key is resolved from `apiKey` or, if omitted, the provider's env var
|
|
37
37
|
(`ANTHROPIC_API_KEY` / `OPENAI_API_KEY`). Your key is used to call **your** LLM directly.
|
|
38
38
|
|
|
39
|
+
The `openai` provider also takes a `baseURL`, which points it at **any OpenAI-compatible
|
|
40
|
+
endpoint** — Ollama (`http://localhost:11434/v1`, no key needed, fully offline), Gemini's
|
|
41
|
+
compatibility endpoint, Groq, Mistral, LM Studio, vLLM, or a corporate gateway:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
await translateJSON({
|
|
45
|
+
content, from: 'en', to: 'es',
|
|
46
|
+
provider: 'openai',
|
|
47
|
+
baseURL: 'http://localhost:11434/v1', // Ollama — no apiKey required
|
|
48
|
+
model: 'llama3.2',
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
39
52
|
## What it does
|
|
40
53
|
|
|
41
54
|
- **Structure-preserving** — flattens/unflattens nested JSON; non-string leaves pass through untouched.
|
|
@@ -95,7 +108,7 @@ Format adapters for mobile catalogs are exported too: `parseArbBundle` (Flutter
|
|
|
95
108
|
|
|
96
109
|
## API
|
|
97
110
|
|
|
98
|
-
- `translateJSON({ content, from, to, provider, apiKey?, model?, existing? })` → `{ result, stats }`
|
|
111
|
+
- `translateJSON({ content, from, to, provider, apiKey?, model?, baseURL?, existing? })` → `{ result, stats }`
|
|
99
112
|
- `translateStrings(texts, { adapter, from, to, batchSize? })` → `string[]`
|
|
100
113
|
- `flatten(obj)` / `unflatten(flat)`
|
|
101
114
|
- `extractPlaceholders(str)` / `validatePlaceholders(source, translation)`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipi18n/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Translation QA for i18n locale files: placeholder and plural validation, key parity, coverage, and an LLM-as-judge semantic review. Also a structure-preserving translation engine — bring your own OpenAI or Anthropic key.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
"openai",
|
|
31
31
|
"anthropic",
|
|
32
32
|
"localization",
|
|
33
|
-
"bring-your-own-key"
|
|
33
|
+
"bring-your-own-key",
|
|
34
|
+
"linter",
|
|
35
|
+
"icu",
|
|
36
|
+
"l10n"
|
|
34
37
|
],
|
|
35
38
|
"license": "Apache-2.0",
|
|
36
39
|
"author": "Shipi18n",
|
|
@@ -54,6 +57,9 @@
|
|
|
54
57
|
"devDependencies": {
|
|
55
58
|
"jest": "^29.7.0"
|
|
56
59
|
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"chalk": "^5.3.0"
|
|
62
|
+
},
|
|
57
63
|
"scripts": {
|
|
58
64
|
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
|
|
59
65
|
"test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch"
|
package/src/adapters/index.js
CHANGED
|
@@ -70,7 +70,13 @@ export function anthropicAdapter(config = {}) {
|
|
|
70
70
|
/**
|
|
71
71
|
* OpenAI adapter. Requires the optional peer dep `openai`.
|
|
72
72
|
* Key resolved from opts.apiKey or the OPENAI_API_KEY env var (SDK default).
|
|
73
|
-
*
|
|
73
|
+
*
|
|
74
|
+
* `baseURL` points the same adapter at any OpenAI-compatible endpoint —
|
|
75
|
+
* Ollama (http://localhost:11434/v1), Gemini's compatibility endpoint, Groq,
|
|
76
|
+
* Mistral, LM Studio, vLLM, a corporate gateway. Servers like Ollama accept
|
|
77
|
+
* any key, but the SDK refuses to construct without one, so when a baseURL is
|
|
78
|
+
* given and no key is, we pass a placeholder instead of failing the run.
|
|
79
|
+
* @param {{ apiKey?: string, model?: string, baseURL?: string }} [config]
|
|
74
80
|
* @returns {LLMAdapter}
|
|
75
81
|
*/
|
|
76
82
|
export function openaiAdapter(config = {}) {
|
|
@@ -79,7 +85,17 @@ export function openaiAdapter(config = {}) {
|
|
|
79
85
|
const getClient = async () => {
|
|
80
86
|
if (!clientPromise) {
|
|
81
87
|
clientPromise = import('openai')
|
|
82
|
-
.then(
|
|
88
|
+
.then(
|
|
89
|
+
({ default: OpenAI }) =>
|
|
90
|
+
new OpenAI({
|
|
91
|
+
...(config.apiKey
|
|
92
|
+
? { apiKey: config.apiKey }
|
|
93
|
+
: config.baseURL
|
|
94
|
+
? { apiKey: 'not-needed' } // local/keyless endpoints; real ones will 401
|
|
95
|
+
: {}), // no baseURL: keep SDK default (OPENAI_API_KEY env)
|
|
96
|
+
...(config.baseURL ? { baseURL: config.baseURL } : {}),
|
|
97
|
+
})
|
|
98
|
+
)
|
|
83
99
|
.catch(() => {
|
|
84
100
|
throw missingSdkError('openai', 'openai')
|
|
85
101
|
})
|
|
@@ -106,7 +122,7 @@ export function openaiAdapter(config = {}) {
|
|
|
106
122
|
/**
|
|
107
123
|
* Resolve a provider name (+ config) to an adapter instance.
|
|
108
124
|
* @param {'anthropic'|'openai'|LLMAdapter} provider
|
|
109
|
-
* @param {{ apiKey?: string, model?: string }} [config]
|
|
125
|
+
* @param {{ apiKey?: string, model?: string, baseURL?: string }} [config]
|
|
110
126
|
* @returns {LLMAdapter}
|
|
111
127
|
*/
|
|
112
128
|
export function resolveAdapter(provider, config = {}) {
|
package/src/index.js
CHANGED
|
@@ -20,3 +20,7 @@ export { parseXcstrings } from './formats/xcstrings.js'
|
|
|
20
20
|
export { reviewTranslations, DEFAULT_JUDGE_MODELS, buildReviewPrompt, parseVerdicts, pairHash } from './review.js'
|
|
21
21
|
export { getLanguageName, LANGUAGE_NAMES } from './languages.js'
|
|
22
22
|
export { anthropicAdapter, openaiAdapter, resolveAdapter } from './adapters/index.js'
|
|
23
|
+
// Reporters + verdict lived in the CLI through 2.5.x; lifted here (core 2.6.0)
|
|
24
|
+
// so the GitHub Action can run `check` without depending on the CLI. The CLI
|
|
25
|
+
// re-exports these names for compatibility.
|
|
26
|
+
export { humanReport, jsonReport, sarifReport, junitReport, REPORTERS, RULE_META, verdict } from './reporters.js'
|
package/src/reporters.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formats for `shipi18n check`.
|
|
3
|
+
*
|
|
4
|
+
* Reporters SERIALIZE a result; they never decide it. Exit codes come from
|
|
5
|
+
* `verdict()` alone, so switching reporter can never change whether CI fails.
|
|
6
|
+
*/
|
|
7
|
+
import chalk from 'chalk'
|
|
8
|
+
|
|
9
|
+
/** Every rule has a documentation page; SARIF helpUri and the human footer
|
|
10
|
+
* point at it. Ids must match RULE_META below and the site's checkRules.js. */
|
|
11
|
+
const ruleUrl = (type) => `https://shipi18n.com/docs/rules/${type}`
|
|
12
|
+
|
|
13
|
+
/* ------------------------------------------------------------------ human */
|
|
14
|
+
|
|
15
|
+
export function humanReport(result, verdictResult) {
|
|
16
|
+
const lines = []
|
|
17
|
+
lines.push('')
|
|
18
|
+
lines.push(
|
|
19
|
+
`🔎 shipi18n check — ${result.layout} layout, source '${result.source}', ${result.languages.length} target language(s)`
|
|
20
|
+
)
|
|
21
|
+
lines.push('')
|
|
22
|
+
for (const l of result.languages) {
|
|
23
|
+
const all = l.namespaces.flatMap((n) => n.findings.map((f) => ({ ...f, ns: n.ns })))
|
|
24
|
+
const mark = l.stats.errors ? chalk.red('✗') : all.length ? chalk.yellow('⚠') : chalk.green('✓')
|
|
25
|
+
lines.push(
|
|
26
|
+
`${mark} ${chalk.bold(l.lang)} coverage ${(l.stats.coverage * 100).toFixed(1)}% ${l.stats.errors} error(s), ${l.stats.warnings} warning(s)`
|
|
27
|
+
)
|
|
28
|
+
for (const f of all.slice(0, 50)) {
|
|
29
|
+
const color = f.severity === 'error' ? chalk.red : chalk.yellow
|
|
30
|
+
const where = result.layout === 'flat' ? f.path : `${f.ns}:${f.path}`
|
|
31
|
+
lines.push(` ${color(f.severity)} ${chalk.cyan(where)} ${f.type} — ${f.message}`)
|
|
32
|
+
}
|
|
33
|
+
if (all.length > 50) lines.push(chalk.gray(` … and ${all.length - 50} more`))
|
|
34
|
+
}
|
|
35
|
+
const seenTypes = [
|
|
36
|
+
...new Set(result.languages.flatMap((l) => l.namespaces.flatMap((n) => n.findings.map((f) => f.type)))),
|
|
37
|
+
].sort()
|
|
38
|
+
if (seenTypes.length > 0) {
|
|
39
|
+
lines.push('')
|
|
40
|
+
for (const t of seenTypes) lines.push(chalk.gray(` ${t} → ${ruleUrl(t)}`))
|
|
41
|
+
}
|
|
42
|
+
lines.push('')
|
|
43
|
+
lines.push(
|
|
44
|
+
verdictResult.ok
|
|
45
|
+
? chalk.green('✓ check passed')
|
|
46
|
+
: chalk.red(`✗ check failed: ${verdictResult.failures.join('; ')}`)
|
|
47
|
+
)
|
|
48
|
+
return lines.join('\n')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* ------------------------------------------------------------------- json */
|
|
52
|
+
|
|
53
|
+
export function jsonReport(result, verdictResult) {
|
|
54
|
+
return JSON.stringify({ ...result, ok: verdictResult.ok, failures: verdictResult.failures }, null, 2)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ------------------------------------------------------------------ sarif */
|
|
58
|
+
|
|
59
|
+
export const RULE_META = {
|
|
60
|
+
'missing-key': 'A key present in the source language is missing from a translation.',
|
|
61
|
+
'orphan-key': 'A key present in a translation does not exist in the source language.',
|
|
62
|
+
'placeholder-missing': 'A placeholder from the source string was dropped in the translation.',
|
|
63
|
+
'placeholder-added': 'The translation contains a placeholder the source does not have.',
|
|
64
|
+
'plural-forms': 'A pipe-separated plural lost one or more of its forms in translation.',
|
|
65
|
+
'empty-value': 'The translation of a non-empty source string is empty.',
|
|
66
|
+
'untranslated': 'The translation is identical to a multi-word source string.',
|
|
67
|
+
'type-mismatch': 'Source and translation values have different JSON types.',
|
|
68
|
+
'invalid-json': 'A locale file could not be parsed as JSON.',
|
|
69
|
+
'missing-file': 'An expected locale file does not exist.',
|
|
70
|
+
'stale-translation': 'The catalog marks this translation as needing review.',
|
|
71
|
+
'glossary-violation': 'A do-not-translate or locked glossary term was not respected.',
|
|
72
|
+
'manual-translation-clobbered': 'A translation locked as hand-edited has been overwritten.',
|
|
73
|
+
'manual-translation-stale': 'The source changed after this translation was locked by hand.',
|
|
74
|
+
'semantic-mistranslation': 'LLM judge (majority vote): the translation states something different from the source.',
|
|
75
|
+
'semantic-omission': 'LLM judge (majority vote): meaningful source content is missing from the translation.',
|
|
76
|
+
'semantic-addition': 'LLM judge (majority vote): the translation contains claims the source does not make.',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** SARIF 2.1.0 — one run, one rule per finding type, one result per finding. */
|
|
80
|
+
export function sarifReport(result, _verdictResult, { toolVersion = '0.0.0' } = {}) {
|
|
81
|
+
const findings = []
|
|
82
|
+
for (const l of result.languages) {
|
|
83
|
+
for (const n of l.namespaces) {
|
|
84
|
+
for (const f of n.findings) findings.push({ lang: l.lang, ns: n.ns, file: n.file, ...f })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Deterministic output: stable ordering makes committed SARIF diffable.
|
|
88
|
+
findings.sort((a, b) =>
|
|
89
|
+
`${a.file}|${a.path}|${a.type}`.localeCompare(`${b.file}|${b.path}|${b.type}`)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const usedTypes = [...new Set(findings.map((f) => f.type))].sort()
|
|
93
|
+
const ruleIndex = Object.fromEntries(usedTypes.map((t, i) => [t, i]))
|
|
94
|
+
|
|
95
|
+
const sarif = {
|
|
96
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
97
|
+
version: '2.1.0',
|
|
98
|
+
runs: [
|
|
99
|
+
{
|
|
100
|
+
tool: {
|
|
101
|
+
driver: {
|
|
102
|
+
name: 'shipi18n-check',
|
|
103
|
+
informationUri: 'https://github.com/Shipi18n/shipi18n',
|
|
104
|
+
version: toolVersion,
|
|
105
|
+
rules: usedTypes.map((t) => ({
|
|
106
|
+
id: t,
|
|
107
|
+
shortDescription: { text: RULE_META[t] || t },
|
|
108
|
+
helpUri: ruleUrl(t),
|
|
109
|
+
})),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
results: findings.map((f) => ({
|
|
113
|
+
ruleId: f.type,
|
|
114
|
+
ruleIndex: ruleIndex[f.type],
|
|
115
|
+
level: f.severity === 'error' ? 'error' : 'warning',
|
|
116
|
+
message: { text: `[${f.lang}] ${f.path}: ${f.message}` },
|
|
117
|
+
locations: [
|
|
118
|
+
{
|
|
119
|
+
physicalLocation: {
|
|
120
|
+
artifactLocation: { uri: (f.file || '').split('\\').join('/') },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
})),
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
}
|
|
128
|
+
return JSON.stringify(sarif, null, 2)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ------------------------------------------------------------------ junit */
|
|
132
|
+
|
|
133
|
+
const xmlEscape = (s) =>
|
|
134
|
+
String(s)
|
|
135
|
+
.replace(/&/g, '&')
|
|
136
|
+
.replace(/</g, '<')
|
|
137
|
+
.replace(/>/g, '>')
|
|
138
|
+
.replace(/"/g, '"')
|
|
139
|
+
.replace(/'/g, ''')
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* One <testsuite> per language, one <testcase> per namespace.
|
|
143
|
+
* Errors become <failure>; warnings go to <system-out> — a warning that fails
|
|
144
|
+
* CI gets the tool uninstalled.
|
|
145
|
+
*/
|
|
146
|
+
export function junitReport(result) {
|
|
147
|
+
const suites = []
|
|
148
|
+
let totalTests = 0
|
|
149
|
+
let totalFailures = 0
|
|
150
|
+
|
|
151
|
+
for (const l of result.languages) {
|
|
152
|
+
const cases = []
|
|
153
|
+
let failures = 0
|
|
154
|
+
for (const n of l.namespaces) {
|
|
155
|
+
totalTests++
|
|
156
|
+
const errors = n.findings.filter((f) => f.severity === 'error')
|
|
157
|
+
const warnings = n.findings.filter((f) => f.severity === 'warning')
|
|
158
|
+
const body = []
|
|
159
|
+
if (errors.length) {
|
|
160
|
+
failures++
|
|
161
|
+
totalFailures++
|
|
162
|
+
// Include the offending strings: "dropped {{name}}" is not actionable
|
|
163
|
+
// without seeing WHICH string dropped it.
|
|
164
|
+
const detail = errors
|
|
165
|
+
.map((f) => {
|
|
166
|
+
const lines = [`${f.path}: ${f.type} — ${f.message}`]
|
|
167
|
+
if (f.source != null) lines.push(` source: ${f.source}`)
|
|
168
|
+
if (f.translation != null) lines.push(` translation: ${f.translation}`)
|
|
169
|
+
return lines.join('\n')
|
|
170
|
+
})
|
|
171
|
+
.join('\n')
|
|
172
|
+
body.push(
|
|
173
|
+
` <failure message="${xmlEscape(`${errors.length} error(s) in ${l.lang}/${n.ns}`)}">${xmlEscape(detail)}</failure>`
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
if (warnings.length) {
|
|
177
|
+
const detail = warnings.map((f) => `${f.path}: ${f.type} — ${f.message}`).join('\n')
|
|
178
|
+
body.push(` <system-out>${xmlEscape(detail)}</system-out>`)
|
|
179
|
+
}
|
|
180
|
+
cases.push(
|
|
181
|
+
body.length
|
|
182
|
+
? ` <testcase classname="${xmlEscape(l.lang)}" name="${xmlEscape(n.ns)}">\n${body.join('\n')}\n </testcase>`
|
|
183
|
+
: ` <testcase classname="${xmlEscape(l.lang)}" name="${xmlEscape(n.ns)}"/>`
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
suites.push(
|
|
187
|
+
` <testsuite name="${xmlEscape(l.lang)}" tests="${l.namespaces.length}" failures="${failures}">\n${cases.join('\n')}\n </testsuite>`
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return [
|
|
192
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
193
|
+
`<testsuites name="shipi18n-check" tests="${totalTests}" failures="${totalFailures}">`,
|
|
194
|
+
...suites,
|
|
195
|
+
'</testsuites>',
|
|
196
|
+
'',
|
|
197
|
+
].join('\n')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const REPORTERS = { human: humanReport, json: jsonReport, sarif: sarifReport, junit: junitReport }
|
|
201
|
+
|
|
202
|
+
/* ---------------------------------------------------------------- verdict */
|
|
203
|
+
|
|
204
|
+
/** Decide the exit code from findings and flags. Reporters never influence this. */
|
|
205
|
+
export function verdict(result, { failOn = 'error', minCoverage } = {}) {
|
|
206
|
+
const failures = []
|
|
207
|
+
if (failOn === 'error' && result.totals.errors > 0) failures.push(`${result.totals.errors} error(s)`)
|
|
208
|
+
if (failOn === 'warning' && result.totals.errors + result.totals.warnings > 0)
|
|
209
|
+
failures.push(`${result.totals.errors} error(s), ${result.totals.warnings} warning(s)`)
|
|
210
|
+
if (minCoverage != null) {
|
|
211
|
+
for (const l of result.languages) {
|
|
212
|
+
if (l.stats.coverage * 100 < minCoverage)
|
|
213
|
+
failures.push(`${l.lang} coverage ${(l.stats.coverage * 100).toFixed(1)}% < ${minCoverage}%`)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { ok: failures.length === 0, failures }
|
|
217
|
+
}
|
package/src/review.js
CHANGED
|
@@ -118,10 +118,11 @@ export async function reviewTranslations({
|
|
|
118
118
|
passes = 3,
|
|
119
119
|
glossary,
|
|
120
120
|
cache,
|
|
121
|
+
baseURL,
|
|
121
122
|
}) {
|
|
122
123
|
const judgeModel =
|
|
123
124
|
model ?? (typeof provider === 'string' ? DEFAULT_JUDGE_MODELS[provider] : undefined)
|
|
124
|
-
const adapter = resolveAdapter(provider, { apiKey, model: judgeModel })
|
|
125
|
+
const adapter = resolveAdapter(provider, { apiKey, model: judgeModel, baseURL })
|
|
125
126
|
|
|
126
127
|
const src = flatten(source)
|
|
127
128
|
const tgt = flatten(target)
|
package/src/translate.js
CHANGED
|
@@ -116,11 +116,12 @@ export async function translateStrings(texts, { adapter, from, to, batchSize = 4
|
|
|
116
116
|
* @param {'anthropic'|'openai'|object} params.provider provider name or a custom adapter
|
|
117
117
|
* @param {string} [params.apiKey] LLM API key (else provider env var)
|
|
118
118
|
* @param {string} [params.model] override the provider's default model
|
|
119
|
+
* @param {string} [params.baseURL] OpenAI-compatible endpoint override (Ollama, Gemini compat, ...)
|
|
119
120
|
* @param {Record<string,any>} [params.existing] prior translation → only re-translate changed/new keys (incremental)
|
|
120
121
|
* @returns {Promise<{ result: object, stats: { translated: number, reused: number, placeholderWarnings: Array }}>}
|
|
121
122
|
*/
|
|
122
|
-
export async function translateJSON({ content, from, to, provider, apiKey, model, existing }) {
|
|
123
|
-
const adapter = resolveAdapter(provider, { apiKey, model })
|
|
123
|
+
export async function translateJSON({ content, from, to, provider, apiKey, model, baseURL, existing }) {
|
|
124
|
+
const adapter = resolveAdapter(provider, { apiKey, model, baseURL })
|
|
124
125
|
const sourceFlat = flatten(content)
|
|
125
126
|
const existingFlat = existing ? flatten(existing) : {}
|
|
126
127
|
|
package/src/tree.js
CHANGED
|
@@ -320,7 +320,7 @@ export function runCheck({ input, source = 'en', ignoreKeys, glossary, locks } =
|
|
|
320
320
|
* @returns aggregated judge stats { judged, cached, flagged, calls, parseFailures, excluded }
|
|
321
321
|
*/
|
|
322
322
|
|
|
323
|
-
export async function runSemantic(result, { provider, apiKey, model, passes, glossary, cache, fail = false }) {
|
|
323
|
+
export async function runSemantic(result, { provider, apiKey, model, baseURL, passes, glossary, cache, fail = false }) {
|
|
324
324
|
const totals = { judged: 0, cached: 0, flagged: 0, calls: 0, parseFailures: 0, excluded: 0 }
|
|
325
325
|
|
|
326
326
|
for (const l of result.languages) {
|
|
@@ -346,7 +346,7 @@ export async function runSemantic(result, { provider, apiKey, model, passes, glo
|
|
|
346
346
|
|
|
347
347
|
const { findings, stats } = await reviewTranslations({
|
|
348
348
|
source: src, target: tgt, from: result.source, to: l.lang,
|
|
349
|
-
provider, apiKey, model, passes, glossary, cache,
|
|
349
|
+
provider, apiKey, model, baseURL, passes, glossary, cache,
|
|
350
350
|
})
|
|
351
351
|
for (const k of Object.keys(stats)) totals[k] = (totals[k] ?? 0) + (stats[k] ?? 0)
|
|
352
352
|
|