@shipi18n/cli 2.3.0 → 2.5.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 +17 -0
- package/README.md +62 -10
- package/package.json +8 -3
- package/src/commands/check.js +20 -0
- package/src/commands/translate.js +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @shipi18n/cli
|
|
2
2
|
|
|
3
|
+
## 2.5.0
|
|
4
|
+
|
|
5
|
+
- New: `--base-url <url>` on `translate` and `check` — run against any OpenAI-compatible endpoint
|
|
6
|
+
with `-p openai`: Ollama (`http://localhost:11434/v1`, no API key at all), Gemini's compatibility
|
|
7
|
+
endpoint, Groq, LM Studio, vLLM. Makes the API key optional; the judge's published accuracy was
|
|
8
|
+
measured on `claude-haiku-4-5`, so run `evals/semantic/run.mjs` before trusting a different judge.
|
|
9
|
+
- Guard: `--base-url` without `-p openai` is an explicit error rather than a silent provider switch.
|
|
10
|
+
|
|
11
|
+
## 2.4.0
|
|
12
|
+
|
|
13
|
+
- Fix: `check --semantic` explains a `judged 0`. When every translated key has a structural error
|
|
14
|
+
there is nothing for the judge to look at — correct, but it looked broken. The CLI now says how
|
|
15
|
+
many keys it skipped and to fix those first.
|
|
16
|
+
- Fix (via core 2.4.0): the missing-SDK error names the install that works for the `npx` path.
|
|
17
|
+
- Note: the npm description on this page was stale until this release — npm only refreshes it on
|
|
18
|
+
publish, so the registry still led with translation after the project repositioned around QA.
|
|
19
|
+
|
|
3
20
|
## 2.3.0
|
|
4
21
|
|
|
5
22
|
- New: `shipi18n lock [path]` — record hand-edited translations in a readable, commit-friendly
|
package/README.md
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
1
|
# @shipi18n/cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
**Catch broken translations before you ship them** — and translate with your own LLM key when you
|
|
4
|
+
want to. Open-source, no account, no hosted API.
|
|
5
|
+
|
|
6
|
+
## Quickstart — check, no API key
|
|
5
7
|
|
|
6
8
|
```bash
|
|
7
|
-
|
|
9
|
+
npx @shipi18n/cli check ./locales -s en
|
|
8
10
|
```
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
Reports missing and orphaned keys, dropped placeholders, collapsed plurals, empty values,
|
|
13
|
+
untranslated copy and per-language coverage. Deterministic and offline — no LLM, no key, nothing
|
|
14
|
+
leaves your machine. Exit code `1` when there are errors, so it works as a CI gate as-is.
|
|
15
|
+
|
|
16
|
+
Then, for the errors structure cannot see, bring a key. The judge needs a provider SDK next to the
|
|
17
|
+
CLI, so install both:
|
|
11
18
|
|
|
12
19
|
```bash
|
|
20
|
+
npm i -D @shipi18n/cli @anthropic-ai/sdk # or `openai`
|
|
21
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
22
|
+
npx shipi18n check ./locales -s en --semantic
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
An LLM reads each source/translation pair and reports mistranslations, omissions and additions —
|
|
26
|
+
advisory by default, so it never fails your build unless you ask it to.
|
|
27
|
+
|
|
28
|
+
> **Note.** `--semantic` only judges keys that pass the structural checks, so if a tree is full of
|
|
29
|
+
> missing keys and dropped placeholders you will see `judged 0` and no model calls. That is by
|
|
30
|
+
> design — fix the structural errors first, then re-run for meaning.
|
|
31
|
+
|
|
32
|
+
**Measured** on a 228-pair corpus committed before the judge was written: **100% of planted errors
|
|
33
|
+
caught, 7.1% false positives.** Full harness in the repo under `evals/semantic/` — run it against
|
|
34
|
+
your own model.
|
|
35
|
+
|
|
36
|
+
## Translating
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm i -g @shipi18n/cli @anthropic-ai/sdk # or add `openai` for the OpenAI provider
|
|
13
40
|
export ANTHROPIC_API_KEY=sk-ant-...
|
|
14
41
|
shipi18n translate en.json --target es,fr,de
|
|
15
42
|
```
|
|
@@ -29,6 +56,7 @@ Options:
|
|
|
29
56
|
-p, --provider <name> LLM provider: anthropic (default) or openai
|
|
30
57
|
--api-key <key> LLM API key (else ANTHROPIC_API_KEY / OPENAI_API_KEY env)
|
|
31
58
|
--model <model> Override the provider's default model
|
|
59
|
+
--base-url <url> OpenAI-compatible endpoint (Ollama, Gemini compat, ...); needs -p openai
|
|
32
60
|
-i, --incremental Reuse existing output files; only translate new/missing keys
|
|
33
61
|
```
|
|
34
62
|
|
|
@@ -43,6 +71,13 @@ shipi18n translate en.json -p openai -t de --api-key $OPENAI_API_KEY
|
|
|
43
71
|
|
|
44
72
|
# Incremental — only translate keys not already in the target file
|
|
45
73
|
shipi18n translate en.json -t es --incremental
|
|
74
|
+
|
|
75
|
+
# Ollama — fully local, no API key at all (any OpenAI-compatible server works)
|
|
76
|
+
shipi18n translate en.json -p openai --base-url http://localhost:11434/v1 --model llama3.2 -t es
|
|
77
|
+
|
|
78
|
+
# Google Gemini, via its OpenAI-compatible endpoint
|
|
79
|
+
shipi18n translate en.json -p openai --base-url https://generativelanguage.googleapis.com/v1beta/openai/ \
|
|
80
|
+
--model gemini-2.5-flash --api-key $GEMINI_API_KEY -t es
|
|
46
81
|
```
|
|
47
82
|
|
|
48
83
|
## Check — validate translations in CI (no LLM, no key)
|
|
@@ -107,12 +142,22 @@ already failed the structural check are never sent to the judge.
|
|
|
107
142
|
What it flags: `semantic-mistranslation` (says something different), `semantic-omission` (meaning
|
|
108
143
|
dropped), `semantic-addition` (meaning invented).
|
|
109
144
|
|
|
110
|
-
**Measured** (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
145
|
+
**Measured** (committed 228-pair corpus, thresholds fixed before the judge was built, default judge
|
|
146
|
+
`claude-haiku-4-5`, 3 passes). Two independent runs, 2026-08-16 and 2026-08-17:
|
|
147
|
+
|
|
148
|
+
| | 2026-08-16 | 2026-08-17 |
|
|
149
|
+
| --- | --- | --- |
|
|
150
|
+
| planted errors caught | 54/54 (100%) | 54/54 (100%) |
|
|
151
|
+
| per-category recall | 100% | 100% |
|
|
152
|
+
| label accuracy | 100% | 53/54 (98.1%) |
|
|
153
|
+
| false positives on clean pairs | 12/168 (7.1%) | 12/168 (7.1%) |
|
|
154
|
+
| glossary violations | 6/6, 0 false | 6/6, 0 false |
|
|
155
|
+
| cost | ~62k tokens / 141s | ~59k tokens / 156s, 48 calls |
|
|
156
|
+
|
|
157
|
+
The judge is a model, so treat these as a range, not a constant — label accuracy moved between runs
|
|
158
|
+
while catch and false-positive rates held. On a real 478-pair production tree it flagged 3.6% of keys;
|
|
159
|
+
the warm-cache rerun made **zero** model calls. Full harness: `evals/semantic/` in the repo — run it
|
|
160
|
+
against your own model and publish what you get.
|
|
116
161
|
|
|
117
162
|
### Glossary (deterministic — no LLM)
|
|
118
163
|
|
|
@@ -167,6 +212,13 @@ A missing or corrupt lock file is a cold start, not a crash.
|
|
|
167
212
|
Set `ANTHROPIC_API_KEY` (default provider) or use `-p openai` with `OPENAI_API_KEY`. Your keys, your
|
|
168
213
|
models — nothing is sent to a Shipi18n server. Built on [`@shipi18n/core`](https://www.npmjs.com/package/@shipi18n/core).
|
|
169
214
|
|
|
215
|
+
`--base-url` points the OpenAI provider at **any OpenAI-compatible endpoint**: Ollama
|
|
216
|
+
(`http://localhost:11434/v1` — no key needed, fully offline), Gemini's compatibility endpoint, Groq,
|
|
217
|
+
Mistral, LM Studio, vLLM, or a corporate gateway. It works for `translate` and for the
|
|
218
|
+
`check --semantic` judge alike — pass `--model` for the model that server actually hosts. The judge's
|
|
219
|
+
published accuracy numbers were measured on `claude-haiku-4-5`; before trusting a different judge
|
|
220
|
+
model, run the eval harness in the repo (`evals/semantic/run.mjs`) against it.
|
|
221
|
+
|
|
170
222
|
## License
|
|
171
223
|
|
|
172
224
|
Apache-2.0
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipi18n/cli",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "Catch broken translations before you ship: dropped placeholders, missing keys, collapsed plurals and — with your own LLM key — mistranslations the structure checks cannot see. CI-ready (SARIF, JUnit, exit codes). Translates too.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"shipi18n": "./bin/shipi18n.js"
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
|
+
"sarif",
|
|
23
|
+
"ci",
|
|
24
|
+
"lint",
|
|
25
|
+
"translation-quality",
|
|
26
|
+
"i18n-qa",
|
|
22
27
|
"i18n",
|
|
23
28
|
"cli",
|
|
24
29
|
"translation",
|
|
@@ -39,7 +44,7 @@
|
|
|
39
44
|
"chalk": "^5.3.0",
|
|
40
45
|
"commander": "^12.0.0",
|
|
41
46
|
"ora": "^8.0.1",
|
|
42
|
-
"@shipi18n/core": "^2.
|
|
47
|
+
"@shipi18n/core": "^2.5.0"
|
|
43
48
|
},
|
|
44
49
|
"peerDependencies": {
|
|
45
50
|
"@anthropic-ai/sdk": ">=0.30.0",
|
package/src/commands/check.js
CHANGED
|
@@ -73,6 +73,10 @@ export function checkCommand(program) {
|
|
|
73
73
|
.option('-p, --provider <name>', 'LLM provider for --semantic: anthropic | openai', 'anthropic')
|
|
74
74
|
.option('--api-key <key>', 'LLM API key for --semantic (else provider env var)')
|
|
75
75
|
.option('--semantic-model <model>', 'Judge model override')
|
|
76
|
+
.option(
|
|
77
|
+
'--base-url <url>',
|
|
78
|
+
'OpenAI-compatible endpoint for the judge (Ollama, Gemini compat, ...). Needs -p openai; makes the key optional'
|
|
79
|
+
)
|
|
76
80
|
.option('--semantic-passes <n>', 'Judge passes for the majority vote', (v) => parseInt(v, 10), 3)
|
|
77
81
|
.option('--semantic-cache <file>', 'Verdict cache path', '.shipi18n/semantic-cache.json')
|
|
78
82
|
.option('--locks <file>', 'Manual-translation lock file', DEFAULT_LOCKS_PATH)
|
|
@@ -115,10 +119,16 @@ export function checkCommand(program) {
|
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
try {
|
|
122
|
+
if (opts.baseUrl && opts.provider !== 'openai') {
|
|
123
|
+
console.error(chalk.red("--base-url targets OpenAI-compatible endpoints. Add -p openai."))
|
|
124
|
+
process.exitCode = 2
|
|
125
|
+
return
|
|
126
|
+
}
|
|
118
127
|
const judge = await runSemantic(result, {
|
|
119
128
|
provider: opts.provider,
|
|
120
129
|
apiKey: opts.apiKey,
|
|
121
130
|
model: opts.semanticModel,
|
|
131
|
+
baseURL: opts.baseUrl,
|
|
122
132
|
passes: opts.semanticPasses,
|
|
123
133
|
glossary,
|
|
124
134
|
cache,
|
|
@@ -132,6 +142,16 @@ export function checkCommand(program) {
|
|
|
132
142
|
`${judge.calls} model call(s), ${judge.parseFailures} discarded pass(es)`
|
|
133
143
|
)
|
|
134
144
|
)
|
|
145
|
+
// "judged 0" on a badly broken tree is correct but reads as a broken
|
|
146
|
+
// feature: the judge skips keys that already carry a structural error.
|
|
147
|
+
if (judge.judged === 0 && judge.excluded > 0) {
|
|
148
|
+
console.error(
|
|
149
|
+
chalk.yellow(
|
|
150
|
+
`note: nothing was judged — all ${judge.excluded} translated key(s) have structural ` +
|
|
151
|
+
`errors, which the semantic pass skips. Fix those first, then re-run with --semantic.`
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
}
|
|
135
155
|
} catch (err) {
|
|
136
156
|
console.error(chalk.red(`Semantic pass failed: ${err.message}`))
|
|
137
157
|
process.exitCode = 2
|
|
@@ -16,6 +16,10 @@ export function translateCommand(program) {
|
|
|
16
16
|
.option('-p, --provider <name>', 'LLM provider: anthropic | openai', 'anthropic')
|
|
17
17
|
.option('--api-key <key>', 'LLM API key (else read from provider env var)')
|
|
18
18
|
.option('--model <model>', 'Override the provider default model')
|
|
19
|
+
.option(
|
|
20
|
+
'--base-url <url>',
|
|
21
|
+
'OpenAI-compatible endpoint (Ollama, Gemini compat, Groq, a gateway). Needs -p openai; makes the key optional'
|
|
22
|
+
)
|
|
19
23
|
.option('-i, --incremental', 'Only translate new/missing keys (reuse existing output files)')
|
|
20
24
|
.action(async (input, options) => {
|
|
21
25
|
const provider = options.provider
|
|
@@ -23,8 +27,18 @@ export function translateCommand(program) {
|
|
|
23
27
|
console.error(chalk.red(`Unknown provider '${provider}'. Use 'anthropic' or 'openai'.`))
|
|
24
28
|
process.exit(1)
|
|
25
29
|
}
|
|
30
|
+
// --base-url speaks the OpenAI wire protocol, so it only makes sense with
|
|
31
|
+
// the openai adapter. Explicit beats magic: error rather than silently
|
|
32
|
+
// switching providers out from under the default.
|
|
33
|
+
if (options.baseUrl && provider !== 'openai') {
|
|
34
|
+
console.error(chalk.red(`--base-url targets OpenAI-compatible endpoints. Add ${chalk.yellow('-p openai')}.`))
|
|
35
|
+
console.error(chalk.gray('Works with Ollama (http://localhost:11434/v1), Gemini compat, Groq, LM Studio, vLLM…'))
|
|
36
|
+
process.exit(1)
|
|
37
|
+
}
|
|
26
38
|
const apiKey = options.apiKey || process.env[PROVIDER_ENV[provider]]
|
|
27
|
-
|
|
39
|
+
// Local/keyless endpoints (Ollama) need no key; a real provider behind
|
|
40
|
+
// --base-url will reject the request itself if one was required.
|
|
41
|
+
if (!apiKey && !options.baseUrl) {
|
|
28
42
|
console.error(chalk.red(`No API key. Set ${chalk.yellow(PROVIDER_ENV[provider])} or pass --api-key.`))
|
|
29
43
|
console.error(chalk.gray(`This tool uses YOUR own ${provider} key — no account or Shipi18n key needed.`))
|
|
30
44
|
process.exit(1)
|
|
@@ -61,6 +75,7 @@ export function translateCommand(program) {
|
|
|
61
75
|
provider,
|
|
62
76
|
apiKey,
|
|
63
77
|
model: options.model,
|
|
78
|
+
baseURL: options.baseUrl,
|
|
64
79
|
existing,
|
|
65
80
|
})
|
|
66
81
|
writeFileSync(outPath, JSON.stringify(result, null, 2) + '\n', 'utf8')
|