@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 +23 -0
- package/package.json +2 -2
- package/src/commands/check.js +62 -0
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.
|
|
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.
|
|
52
|
+
"@shipi18n/core": "^2.10.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@anthropic-ai/sdk": ">=0.30.0",
|
package/src/commands/check.js
CHANGED
|
@@ -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
|