@shipi18n/cli 2.8.1 → 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/CHANGELOG.md +6 -0
- package/README.md +25 -1
- package/package.json +2 -2
- package/src/commands/check.js +65 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @shipi18n/cli
|
|
2
2
|
|
|
3
|
+
## 2.9.0
|
|
4
|
+
|
|
5
|
+
- New: `check` now supports **Android `strings.xml`, gettext `.po`/`.pot`, and XLIFF 1.2/2.0** on top
|
|
6
|
+
of JSON, YAML, Flutter `.arb` and Apple `.xcstrings` — auto-detected from the input path (via core
|
|
7
|
+
2.9.0). The structural pass still needs no API key.
|
|
8
|
+
|
|
3
9
|
## 2.8.1
|
|
4
10
|
|
|
5
11
|
- Security (hardening): pulls core 2.8.1 (translator prompt-injection guardrail + recursion depth
|
package/README.md
CHANGED
|
@@ -91,7 +91,8 @@ npx @shipi18n/cli check ./locales --source en
|
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
It detects both common layouts (`locales/en.json` and `locales/en/<ns>.json`), plus Flutter ARB
|
|
94
|
-
directories
|
|
94
|
+
directories, Apple String Catalogs (`shipi18n check Localizable.xcstrings`), Android `res/values-*/`
|
|
95
|
+
`strings.xml` trees, gettext `.po`/`.pot`, and XLIFF (`.xlf`/`.xliff`, 1.2 and 2.0).
|
|
95
96
|
|
|
96
97
|
**What it catches:** missing and orphaned keys · dropped or invented placeholders (`{{name}}`,
|
|
97
98
|
`{count}`, `%s`, `%1$s`, `%@`, `%lld`, `$t(...)`, `%{name}`, HTML tags) · collapsed vue-i18n pipe
|
|
@@ -103,12 +104,35 @@ plurals · empty values · untranslated copy · stale `.xcstrings` states.
|
|
|
103
104
|
| `-r, --reporter <name>` | `human` | `human` \| `json` \| `sarif` \| `junit` |
|
|
104
105
|
| `-o, --output <file>` | stdout | Write the report to a file |
|
|
105
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 |
|
|
106
110
|
| `--fail-on <level>` | `error` | `error` \| `warning` \| `none` |
|
|
107
111
|
| `--min-coverage <pct>` | — | Fail any language below this coverage |
|
|
108
112
|
|
|
109
113
|
Exit codes: `0` pass, `1` findings at the fail level, `2` usage error. Errors may fail CI; warnings
|
|
110
114
|
never do by default — a warning that blocks PRs gets the tool uninstalled.
|
|
111
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
|
+
|
|
112
136
|
### GitHub Actions with PR annotations
|
|
113
137
|
|
|
114
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
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* pipeline can gate on it.
|
|
8
8
|
*
|
|
9
9
|
* Formats: plain JSON locale trees (flat `locales/<lang>.json` or nested
|
|
10
|
-
* `locales/<lang>/<ns>.json`), Flutter ARB directories,
|
|
11
|
-
* Catalogs (`.xcstrings`)
|
|
10
|
+
* `locales/<lang>/<ns>.json`), YAML, Flutter ARB directories, Apple String
|
|
11
|
+
* Catalogs (`.xcstrings`), Android `strings.xml` trees, gettext `.po`/`.pot`,
|
|
12
|
+
* and XLIFF (`.xlf`/`.xliff`, 1.2 & 2.0). Reporters: human, json, sarif, junit.
|
|
12
13
|
*/
|
|
13
14
|
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
14
15
|
import { resolve, dirname, join } from 'node:path'
|
|
@@ -23,6 +24,9 @@ import {
|
|
|
23
24
|
aggregateLanguage,
|
|
24
25
|
SEP,
|
|
25
26
|
verdict,
|
|
27
|
+
applyPolicy,
|
|
28
|
+
buildBaseline,
|
|
29
|
+
parseSeverity,
|
|
26
30
|
} from '@shipi18n/core'
|
|
27
31
|
import { REPORTERS } from '../reporters.js'
|
|
28
32
|
import { locksFor, DEFAULT_LOCKS_PATH } from './lock.js'
|
|
@@ -55,6 +59,9 @@ export function checkCommand(program) {
|
|
|
55
59
|
.option('-o, --output <file>', 'Write the report to a file instead of stdout')
|
|
56
60
|
.option('--json', 'Shorthand for --reporter json')
|
|
57
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')
|
|
58
65
|
.option('--fail-on <level>', 'Exit non-zero on: error | warning | none', 'error')
|
|
59
66
|
.option('--min-coverage <pct>', 'Fail any language below this coverage percentage', parseFloat)
|
|
60
67
|
.option('--glossary <file>', 'Glossary JSON: DNT terms + locked per-language translations (deterministic)')
|
|
@@ -149,6 +156,62 @@ export function checkCommand(program) {
|
|
|
149
156
|
}
|
|
150
157
|
}
|
|
151
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
|
+
|
|
152
215
|
const verdictResult = verdict(result, { failOn: opts.failOn, minCoverage: opts.minCoverage })
|
|
153
216
|
|
|
154
217
|
const name = opts.json ? 'json' : opts.reporter
|