@shipi18n/cli 2.4.0 → 2.5.1
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 +15 -0
- package/package.json +8 -3
- package/src/commands/check.js +10 -0
- package/src/commands/translate.js +16 -1
- package/src/reporters.js +12 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @shipi18n/cli
|
|
2
2
|
|
|
3
|
+
## 2.5.1
|
|
4
|
+
|
|
5
|
+
- New: every check finding now links to its rule page. The human reporter prints
|
|
6
|
+
`<rule> → https://shipi18n.com/docs/rules/<rule>` for each finding type seen, and the SARIF
|
|
7
|
+
reporter sets a per-rule `helpUri` — so GitHub's inline PR annotations link to the page that
|
|
8
|
+
explains the rule, what triggers it, how to fix it, and how to silence it.
|
|
9
|
+
|
|
10
|
+
## 2.5.0
|
|
11
|
+
|
|
12
|
+
- New: `--base-url <url>` on `translate` and `check` — run against any OpenAI-compatible endpoint
|
|
13
|
+
with `-p openai`: Ollama (`http://localhost:11434/v1`, no API key at all), Gemini's compatibility
|
|
14
|
+
endpoint, Groq, LM Studio, vLLM. Makes the API key optional; the judge's published accuracy was
|
|
15
|
+
measured on `claude-haiku-4-5`, so run `evals/semantic/run.mjs` before trusting a different judge.
|
|
16
|
+
- Guard: `--base-url` without `-p openai` is an explicit error rather than a silent provider switch.
|
|
17
|
+
|
|
3
18
|
## 2.4.0
|
|
4
19
|
|
|
5
20
|
- Fix: `check --semantic` explains a `judged 0`. When every translated key has a structural error
|
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ Options:
|
|
|
56
56
|
-p, --provider <name> LLM provider: anthropic (default) or openai
|
|
57
57
|
--api-key <key> LLM API key (else ANTHROPIC_API_KEY / OPENAI_API_KEY env)
|
|
58
58
|
--model <model> Override the provider's default model
|
|
59
|
+
--base-url <url> OpenAI-compatible endpoint (Ollama, Gemini compat, ...); needs -p openai
|
|
59
60
|
-i, --incremental Reuse existing output files; only translate new/missing keys
|
|
60
61
|
```
|
|
61
62
|
|
|
@@ -70,6 +71,13 @@ shipi18n translate en.json -p openai -t de --api-key $OPENAI_API_KEY
|
|
|
70
71
|
|
|
71
72
|
# Incremental — only translate keys not already in the target file
|
|
72
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
|
|
73
81
|
```
|
|
74
82
|
|
|
75
83
|
## Check — validate translations in CI (no LLM, no key)
|
|
@@ -204,6 +212,13 @@ A missing or corrupt lock file is a cold start, not a crash.
|
|
|
204
212
|
Set `ANTHROPIC_API_KEY` (default provider) or use `-p openai` with `OPENAI_API_KEY`. Your keys, your
|
|
205
213
|
models — nothing is sent to a Shipi18n server. Built on [`@shipi18n/core`](https://www.npmjs.com/package/@shipi18n/core).
|
|
206
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
|
+
|
|
207
222
|
## License
|
|
208
223
|
|
|
209
224
|
Apache-2.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipi18n/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
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": {
|
|
@@ -31,7 +31,12 @@
|
|
|
31
31
|
"openai",
|
|
32
32
|
"anthropic",
|
|
33
33
|
"localization",
|
|
34
|
-
"bring-your-own-key"
|
|
34
|
+
"bring-your-own-key",
|
|
35
|
+
"linter",
|
|
36
|
+
"icu",
|
|
37
|
+
"i18next",
|
|
38
|
+
"l10n",
|
|
39
|
+
"check"
|
|
35
40
|
],
|
|
36
41
|
"license": "Apache-2.0",
|
|
37
42
|
"author": "Shipi18n",
|
|
@@ -44,7 +49,7 @@
|
|
|
44
49
|
"chalk": "^5.3.0",
|
|
45
50
|
"commander": "^12.0.0",
|
|
46
51
|
"ora": "^8.0.1",
|
|
47
|
-
"@shipi18n/core": "^2.
|
|
52
|
+
"@shipi18n/core": "^2.5.0"
|
|
48
53
|
},
|
|
49
54
|
"peerDependencies": {
|
|
50
55
|
"@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,
|
|
@@ -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')
|
package/src/reporters.js
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import chalk from 'chalk'
|
|
8
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
|
+
|
|
9
13
|
/* ------------------------------------------------------------------ human */
|
|
10
14
|
|
|
11
15
|
export function humanReport(result, verdictResult) {
|
|
@@ -28,6 +32,13 @@ export function humanReport(result, verdictResult) {
|
|
|
28
32
|
}
|
|
29
33
|
if (all.length > 50) lines.push(chalk.gray(` … and ${all.length - 50} more`))
|
|
30
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
|
+
}
|
|
31
42
|
lines.push('')
|
|
32
43
|
lines.push(
|
|
33
44
|
verdictResult.ok
|
|
@@ -94,7 +105,7 @@ export function sarifReport(result, _verdictResult, { toolVersion = '0.0.0' } =
|
|
|
94
105
|
rules: usedTypes.map((t) => ({
|
|
95
106
|
id: t,
|
|
96
107
|
shortDescription: { text: RULE_META[t] || t },
|
|
97
|
-
helpUri:
|
|
108
|
+
helpUri: ruleUrl(t),
|
|
98
109
|
})),
|
|
99
110
|
},
|
|
100
111
|
},
|