@shipi18n/cli 2.9.0 → 2.10.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/README.md CHANGED
@@ -104,12 +104,35 @@ plurals · empty values · untranslated copy · stale `.xcstrings` states.
104
104
  | `-r, --reporter <name>` | `human` | `human` \| `json` \| `sarif` \| `junit` |
105
105
  | `-o, --output <file>` | stdout | Write the report to a file |
106
106
  | `--ignore-keys <globs>` | — | Silence keys: `'*.copyright,home:mcp.badge'` |
107
+ | `--severity <spec>` | — | Per-rule level override: `'untranslated=off,placeholder-added=error'` |
108
+ | `--baseline <file>` | — | Fail only on findings NOT already in the baseline |
109
+ | `--write-baseline` | — | Snapshot current findings into `--baseline` (default `.shipi18n/baseline.json`) and exit |
107
110
  | `--fail-on <level>` | `error` | `error` \| `warning` \| `none` |
108
111
  | `--min-coverage <pct>` | — | Fail any language below this coverage |
109
112
 
110
113
  Exit codes: `0` pass, `1` findings at the fail level, `2` usage error. Errors may fail CI; warnings
111
114
  never do by default — a warning that blocks PRs gets the tool uninstalled.
112
115
 
116
+ ### Adopting on a messy catalog — baseline & severity
117
+
118
+ A linter that fails on 2,000 pre-existing findings gets uninstalled by lunch. Baseline first, then
119
+ fail only on what's **new** — the Stylelint/RuboCop pattern:
120
+
121
+ ```bash
122
+ # 1. Snapshot today's findings (commit the file).
123
+ npx @shipi18n/cli check ./locales --baseline .shipi18n/baseline.json --write-baseline
124
+
125
+ # 2. CI from now on fails only on NEW findings; the backlog is accepted.
126
+ npx @shipi18n/cli check ./locales --baseline .shipi18n/baseline.json
127
+ ```
128
+
129
+ The baseline keys each finding on `(language, namespace, key path, rule)` — not the message text, so
130
+ rewording a message never invalidates it. A missing baseline file is a cold start (warn + report all),
131
+ not an error. Burn the backlog down by re-running `--write-baseline` whenever it shrinks.
132
+
133
+ `--severity` tunes or silences a rule everywhere: `error`, `warning`, `info` (reported, never fails),
134
+ or `off` (dropped entirely). Example: `--severity 'untranslated=off,empty-value=warning'`.
135
+
113
136
  ### GitHub Actions with PR annotations
114
137
 
115
138
  ```yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipi18n/cli",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
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": {
@@ -49,7 +49,7 @@
49
49
  "chalk": "^5.3.0",
50
50
  "commander": "^12.0.0",
51
51
  "ora": "^8.0.1",
52
- "@shipi18n/core": "^2.9.0"
52
+ "@shipi18n/core": "^2.10.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@anthropic-ai/sdk": ">=0.30.0",
@@ -24,6 +24,9 @@ import {
24
24
  aggregateLanguage,
25
25
  SEP,
26
26
  verdict,
27
+ applyPolicy,
28
+ buildBaseline,
29
+ parseSeverity,
27
30
  } from '@shipi18n/core'
28
31
  import { REPORTERS } from '../reporters.js'
29
32
  import { locksFor, DEFAULT_LOCKS_PATH } from './lock.js'
@@ -56,6 +59,9 @@ export function checkCommand(program) {
56
59
  .option('-o, --output <file>', 'Write the report to a file instead of stdout')
57
60
  .option('--json', 'Shorthand for --reporter json')
58
61
  .option('--ignore-keys <patterns>', "Comma-separated '*' globs of keys to silence (path or ns:path)")
62
+ .option('--severity <spec>', "Per-rule severity overrides, e.g. 'untranslated=off,placeholder-added=error' (error|warning|info|off)")
63
+ .option('--baseline <file>', 'Baseline file: findings already recorded in it do not fail the build (only NEW ones do)')
64
+ .option('--write-baseline', 'Snapshot current findings into --baseline (default .shipi18n/baseline.json) and exit')
59
65
  .option('--fail-on <level>', 'Exit non-zero on: error | warning | none', 'error')
60
66
  .option('--min-coverage <pct>', 'Fail any language below this coverage percentage', parseFloat)
61
67
  .option('--glossary <file>', 'Glossary JSON: DNT terms + locked per-language translations (deterministic)')
@@ -150,6 +156,62 @@ export function checkCommand(program) {
150
156
  }
151
157
  }
152
158
 
159
+ const DEFAULT_BASELINE = '.shipi18n/baseline.json'
160
+
161
+ // --write-baseline: snapshot every current finding and exit 0. Run after
162
+ // the (optional) semantic pass so a baseline can capture judge findings too.
163
+ if (opts.writeBaseline) {
164
+ const file = resolve(opts.baseline || DEFAULT_BASELINE)
165
+ const bl = buildBaseline(result)
166
+ try {
167
+ mkdirSync(dirname(file), { recursive: true })
168
+ writeFileSync(file, JSON.stringify(bl, null, 2) + '\n')
169
+ } catch (err) {
170
+ console.error(chalk.red(`Error: cannot write baseline ${file}: ${err.message}`))
171
+ process.exitCode = 2
172
+ return
173
+ }
174
+ console.error(chalk.gray(`baseline: recorded ${bl.count} finding(s) → ${opts.baseline || DEFAULT_BASELINE}`))
175
+ return
176
+ }
177
+
178
+ let severityMap
179
+ if (opts.severity) {
180
+ try {
181
+ severityMap = parseSeverity(opts.severity)
182
+ } catch (err) {
183
+ console.error(chalk.red(`Error: ${err.message}`))
184
+ process.exitCode = 2
185
+ return
186
+ }
187
+ }
188
+
189
+ let baseline
190
+ if (opts.baseline) {
191
+ const file = resolve(opts.baseline)
192
+ if (existsSync(file)) {
193
+ try {
194
+ baseline = JSON.parse(readFileSync(file, 'utf8'))
195
+ } catch (err) {
196
+ console.error(chalk.red(`Error: cannot read baseline ${opts.baseline}: ${err.message}`))
197
+ process.exitCode = 2
198
+ return
199
+ }
200
+ } else {
201
+ // A missing baseline is a cold start, not an error: nothing is suppressed
202
+ // and the run reports every finding. Hint how to create one.
203
+ console.error(chalk.yellow(`note: baseline ${opts.baseline} not found — reporting all findings. Create it with --write-baseline.`))
204
+ }
205
+ }
206
+
207
+ if (severityMap || baseline) {
208
+ const suppressed = applyPolicy(result, { severity: severityMap, baseline })
209
+ const parts = []
210
+ if (suppressed.suppressedByBaseline) parts.push(`${suppressed.suppressedByBaseline} baselined`)
211
+ if (suppressed.suppressedBySeverity) parts.push(`${suppressed.suppressedBySeverity} silenced (severity=off)`)
212
+ if (parts.length) console.error(chalk.gray(`policy: ${parts.join(', ')}`))
213
+ }
214
+
153
215
  const verdictResult = verdict(result, { failOn: opts.failOn, minCoverage: opts.minCoverage })
154
216
 
155
217
  const name = opts.json ? 'json' : opts.reporter