@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 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.4.0",
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.4.0"
52
+ "@shipi18n/core": "^2.5.0"
48
53
  },
49
54
  "peerDependencies": {
50
55
  "@anthropic-ai/sdk": ">=0.30.0",
@@ -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
- if (!apiKey) {
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: 'https://shipi18n.com/docs/cli/commands',
108
+ helpUri: ruleUrl(t),
98
109
  })),
99
110
  },
100
111
  },