@the-i18n-kit/cli 4.9.0 → 5.0.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.
Files changed (87) hide show
  1. package/README.md +31 -298
  2. package/dist/{_shared-Dv1EW2gy.js → _shared-DFyy3sh8.js} +24 -12
  3. package/dist/_shared-DFyy3sh8.js.map +1 -0
  4. package/dist/{add-BvXrJkqZ.js → add-C-qs8iBa.js} +5 -5
  5. package/dist/{add-BvXrJkqZ.js.map → add-C-qs8iBa.js.map} +1 -1
  6. package/dist/bin.js +6 -1
  7. package/dist/bin.js.map +1 -1
  8. package/dist/{check-9dcNfHtC.js → check-BI1orjXb.js} +11 -19
  9. package/dist/check-BI1orjXb.js.map +1 -0
  10. package/dist/cli-DsFuVJNP.js +74 -0
  11. package/dist/cli-DsFuVJNP.js.map +1 -0
  12. package/dist/config/framework/stubs/{next-intl-routing-DEwC7Bbk.d.ts → next-intl-routing-Cwaa3f4R.d.ts} +1 -1
  13. package/dist/config/framework/stubs/{next-intl-routing-DEwC7Bbk.d.ts.map → next-intl-routing-Cwaa3f4R.d.ts.map} +1 -1
  14. package/dist/config/framework/stubs/{unplugin-vue-i18n-D9yaDzRk.d.ts → unplugin-vue-i18n-1ud7-Ly8.d.ts} +1 -1
  15. package/dist/config/framework/stubs/{unplugin-vue-i18n-D9yaDzRk.d.ts.map → unplugin-vue-i18n-1ud7-Ly8.d.ts.map} +1 -1
  16. package/dist/define-config-4ubtfvUy.js.map +1 -1
  17. package/dist/{define-config-Dhg13592.d.ts → define-config-BaZ1dZph.d.ts} +11 -4
  18. package/dist/define-config-BaZ1dZph.d.ts.map +1 -0
  19. package/dist/{define-config-lQrIpPSK.d.ts → define-config-CLgKN-7N.d.ts} +1 -1
  20. package/dist/define-config.d.ts +1 -1
  21. package/dist/{detect-Dr2skxeL.js → detect-7cVfsNJP.js} +5 -5
  22. package/dist/{detect-Dr2skxeL.js.map → detect-7cVfsNJP.js.map} +1 -1
  23. package/dist/{empty-p0wct8Hk.js → empty-DB8Hl_8u.js} +5 -5
  24. package/dist/{empty-p0wct8Hk.js.map → empty-DB8Hl_8u.js.map} +1 -1
  25. package/dist/find-duplicates-CmEqBfby.js +42 -0
  26. package/dist/find-duplicates-CmEqBfby.js.map +1 -0
  27. package/dist/{get-BskDC2wT.js → get-BE21Iy6N.js} +5 -5
  28. package/dist/{get-BskDC2wT.js.map → get-BE21Iy6N.js.map} +1 -1
  29. package/dist/{index-BtSA6-iT.d.ts → index-DCG3dOdC.d.ts} +351 -3
  30. package/dist/index-DCG3dOdC.d.ts.map +1 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +5 -4
  33. package/dist/{init-D7r6iX3q.js → init-BtzmhsOM.js} +5 -5
  34. package/dist/{init-D7r6iX3q.js.map → init-BtzmhsOM.js.map} +1 -1
  35. package/dist/{list-dirs-BHk38JXZ.js → list-dirs-DUQbwvlE.js} +5 -5
  36. package/dist/{list-dirs-BHk38JXZ.js.map → list-dirs-DUQbwvlE.js.map} +1 -1
  37. package/dist/{missing-0gSZKTgt.js → missing-CcYrgd54.js} +5 -5
  38. package/dist/{missing-0gSZKTgt.js.map → missing-CcYrgd54.js.map} +1 -1
  39. package/dist/move-B2px6Ck-.js +50 -0
  40. package/dist/move-B2px6Ck-.js.map +1 -0
  41. package/dist/{operations-BCJd7in3.js → operations-CTo44gPu.js} +1332 -241
  42. package/dist/operations-CTo44gPu.js.map +1 -0
  43. package/dist/php-reader-3Fgw80zK.js +64 -0
  44. package/dist/php-reader-3Fgw80zK.js.map +1 -0
  45. package/dist/php-reader-CpnaPSpZ.js +2 -0
  46. package/dist/{providers-BAwPjeCy.js → providers-CHE2ffi6.js} +53 -27
  47. package/dist/providers-CHE2ffi6.js.map +1 -0
  48. package/dist/{remove-DR_mRiMA.js → remove-DTbqM4bR.js} +5 -5
  49. package/dist/{remove-DR_mRiMA.js.map → remove-DTbqM4bR.js.map} +1 -1
  50. package/dist/{remove-orphans-CcA3XQHJ.js → remove-orphans-Dfzu7hRz.js} +5 -5
  51. package/dist/{remove-orphans-CcA3XQHJ.js.map → remove-orphans-Dfzu7hRz.js.map} +1 -1
  52. package/dist/{rename-CWLfThC1.js → rename-C_rFKkU4.js} +5 -5
  53. package/dist/{rename-CWLfThC1.js.map → rename-C_rFKkU4.js.map} +1 -1
  54. package/dist/rename-notice-BV8HNX3O.js +25 -0
  55. package/dist/rename-notice-BV8HNX3O.js.map +1 -0
  56. package/dist/rename-notice-Cx1vuTRz.js +2 -0
  57. package/dist/{scaffold-BahXKbo9.js → scaffold-CUimlNqR.js} +5 -5
  58. package/dist/{scaffold-BahXKbo9.js.map → scaffold-CUimlNqR.js.map} +1 -1
  59. package/dist/{scan-CTxs3kl7.js → scan-BkOIYhjZ.js} +5 -5
  60. package/dist/{scan-CTxs3kl7.js.map → scan-BkOIYhjZ.js.map} +1 -1
  61. package/dist/{search-BonEgmSt.js → search-0io1BcsY.js} +5 -5
  62. package/dist/{search-BonEgmSt.js.map → search-0io1BcsY.js.map} +1 -1
  63. package/dist/{status-C_5Khw14.js → status-Bp-SzjTN.js} +5 -5
  64. package/dist/{status-C_5Khw14.js.map → status-Bp-SzjTN.js.map} +1 -1
  65. package/dist/{translate-CnXvBi5O.js → translate-cCb2Z57w.js} +6 -6
  66. package/dist/{translate-CnXvBi5O.js.map → translate-cCb2Z57w.js.map} +1 -1
  67. package/dist/{translate-key-DwO9XZMc.js → translate-key-DHLWEmrA.js} +5 -5
  68. package/dist/{translate-key-DwO9XZMc.js.map → translate-key-DHLWEmrA.js.map} +1 -1
  69. package/dist/{update-HR8MvPlI.js → update-DV4ijRVB.js} +5 -5
  70. package/dist/{update-HR8MvPlI.js.map → update-DV4ijRVB.js.map} +1 -1
  71. package/dist/{write-CAD9xQDy.js → write-Czzm_Nb0.js} +5 -5
  72. package/dist/{write-CAD9xQDy.js.map → write-Czzm_Nb0.js.map} +1 -1
  73. package/package.json +15 -5
  74. package/dist/_shared-Dv1EW2gy.js.map +0 -1
  75. package/dist/check-9dcNfHtC.js.map +0 -1
  76. package/dist/cli-CiX4llY1.js +0 -82
  77. package/dist/cli-CiX4llY1.js.map +0 -1
  78. package/dist/define-config-Dhg13592.d.ts.map +0 -1
  79. package/dist/find-duplicates-DuI8niF7.js +0 -31
  80. package/dist/find-duplicates-DuI8niF7.js.map +0 -1
  81. package/dist/index-BtSA6-iT.d.ts.map +0 -1
  82. package/dist/operations-BCJd7in3.js.map +0 -1
  83. package/dist/php-reader-DcgOAhTw.js +0 -2
  84. package/dist/php-reader-DuZ0Hyl_.js +0 -32
  85. package/dist/php-reader-DuZ0Hyl_.js.map +0 -1
  86. package/dist/providers-BAwPjeCy.js.map +0 -1
  87. /package/dist/{bin-Cs-dvpEC.d.ts → bin-DrKRgnr9.d.ts} +0 -0
package/README.md CHANGED
@@ -1,224 +1,55 @@
1
- # the-i18n-cli
1
+ # @the-i18n-kit/cli
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/the-i18n-cli?style=flat&colorA=18181b&colorB=4fc08d)](https://npmjs.com/package/the-i18n-cli)
3
+ [![npm version](https://img.shields.io/npm/v/@the-i18n-kit/cli?style=flat&colorA=18181b&colorB=4fc08d)](https://npmjs.com/package/@the-i18n-kit/cli)
4
4
  [![License](https://img.shields.io/npm/l/the-i18n-cli?style=flat&colorA=18181b&colorB=4fc08d)](https://github.com/fabkho/the-i18n-kit/blob/main/LICENSE)
5
5
 
6
- CLI and core library for managing i18n translation files — supports Nuxt, Laravel, Vue, React/Next.js, and any project with JSON or PHP locale files.
6
+ Find missing translation keys, remove dead ones, and rename across every locale
7
+ and layer at once. Supports Nuxt, Laravel, Vue, React/Next.js, and any project
8
+ with JSON or PHP locale files.
7
9
 
8
- Read, write, search, rename, and remove translation keys across all locales and layers from your terminal. Auto-detects your framework, discovers monorepo structures, and handles the file I/O.
10
+ Part of [the-i18n-kit](https://github.com/fabkho/the-i18n-kit).
9
11
 
10
- Part of [the-i18n-kit](https://github.com/fabkho/the-i18n-kit) monorepo. For MCP server usage, see [the-i18n-mcp](https://www.npmjs.com/package/the-i18n-mcp).
12
+ ### 📖 [Documentation](https://fabkho.github.io/the-i18n-kit/)
11
13
 
12
14
  ## Install
13
15
 
14
16
  ```bash
15
- # Global install
16
- npm install -g the-i18n-cli
17
-
18
- # Or use directly with npx
19
- npx the-i18n-cli --help
20
- ```
21
-
22
- ## Quick Start
23
-
24
- ```bash
25
- the-i18n-cli init # Create .i18n-mcp.json from framework detection
26
- the-i18n-cli missing # Find missing translations
27
- the-i18n-cli search --query "save" # Search keys and values
28
- the-i18n-cli write --layer root --translations '{"common.btn.ok": {"en": "OK", "de": "OK"}}'
29
- the-i18n-cli translate-key --layer root --key common.btn.save --sourceLocale en-US --sourceValue "Save"
30
- the-i18n-cli translate --layer root --provider openai --model gpt-4o-mini # Auto-translate missing keys
31
- the-i18n-cli remove-orphans # Find orphan keys (dry-run by default)
32
- the-i18n-cli check # Find used-but-undefined keys (non-zero exit — CI gate)
33
- ```
34
-
35
- ## Commands
36
-
37
- | Command | Description |
38
- |---------|-------------|
39
- | `init` | Create a schema-valid `.i18n-mcp.json` from framework detection. Non-interactive; refuses to overwrite without `--force` |
40
- | `get` | Read translation values for specific keys |
41
- | `write` | Write translation keys (`add` / `update` / `upsert` mode, default: `upsert`) |
42
- | `add` | Add new translation keys (skips keys that already exist) |
43
- | `update` | Update existing keys (skips keys that do not exist) |
44
- | `missing` | Find keys missing in target locales |
45
- | `status` | Translation coverage per locale and per layer, with one overall percentage |
46
- | `search` | Search keys and values |
47
- | `remove` | Remove keys from all locale files in a layer |
48
- | `rename` | Rename/move a key across all locale files |
49
- | `translate` | Find missing translations and translate them via LLM (see Translation Modes). Also available as `translate-missing`, matching the MCP tool name |
50
- | `translate-key` | Translate one source key into target locales; can overwrite stale values |
51
- | `remove-orphans` | Find and remove keys not referenced in source code (dry-run by default) |
52
- | `check` | Find keys referenced in code but defined in no consumed locale layer — the inverse of `remove-orphans`. Exits non-zero when any are found, so it can gate CI. Dynamically built keys are reported as uncertain, never as hard findings |
53
- | `find-duplicates` | Find keys defined in both a shared layer and a consuming child layer (with divergence detection) |
54
- | `scan` | Find where translation keys are referenced in source, with file and line — use before renaming or removing a key |
55
- | `scaffold` | Create empty locale files for new languages |
56
-
57
- Run `the-i18n-cli <command> --help` for per-command options.
58
-
59
- ### Common Flags
60
-
61
- | Flag | Description |
62
- |------|-------------|
63
- | `-d, --projectDir <dir>` | Project directory (default: cwd) |
64
- | `--json` | Output as JSON (default when piped) |
65
- | `--dryRun` | Preview changes without writing |
66
- | `--output-file <path>` | `missing` / `remove-orphans` / `check`: write the full report to a file, return only a summary |
67
-
68
- ### Exit Codes and CI Gates
69
-
70
- | Code | Meaning |
71
- |------|---------|
72
- | `0` | The run succeeded and no gate tripped |
73
- | `1` | The run itself failed — bad API key, unreadable project, a translate run that translated nothing |
74
- | `2` | The run succeeded but a gate tripped: findings exist and the tool worked |
75
-
76
- Separating `1` from `2` lets a pipeline tell a broken setup apart from a project that simply has untranslated keys.
77
-
78
- Gates are opt-in flags, so every invocation without one keeps the exit code it has today:
79
-
80
- | Flag | Command | Trips when |
81
- |------|---------|-----------|
82
- | `--fail-on-missing` | `missing` | Any key is missing in a target locale |
83
- | `--fail-on-orphans` | `remove-orphans` | Any orphan key is found (dry-run still applies) |
84
- | `--fail-on-failed` | `translate` | Any key failed to translate |
85
- | `--fail-under <n>` | `status` | Overall completion is below `n` percent |
86
-
87
- ```bash
88
- the-i18n-cli missing --fail-on-missing # exit 2 blocks the merge
89
- the-i18n-cli remove-orphans --fail-on-orphans # dry-run, but non-zero on findings
90
- the-i18n-cli translate --fail-on-failed # exit 2 when the run lost keys
91
- the-i18n-cli status --fail-under 90 # ratchet coverage like test coverage
92
- ```
93
-
94
- `translate` needs its own gate because exit `1` is reserved for a run that
95
- translated *nothing*. A run that writes 795 keys and loses 141 is a success by
96
- that measure: it exits `0`, and the partial result is committed like any other.
97
- The failed keys are still missing, so re-running retries them — the gate is for
98
- noticing that you need to.
99
-
100
- Gates compose on one invocation, and a failed run outranks a tripped gate — exit `1` wins over exit `2`. When a gate trips, the JSON result gains a `gatesTripped` array naming it and reporting the observed value against the threshold; nothing else in the result changes, so existing consumers keep working:
101
-
102
- ```json
103
- {
104
- "summary": { "totalMissingKeys": 12 },
105
- "gatesTripped": [
106
- { "name": "fail-on-missing", "counter": "totalMissingKeys", "direction": "above", "threshold": 0, "observed": 12 }
107
- ]
108
- }
17
+ npm install -g @the-i18n-kit/cli
109
18
  ```
110
19
 
111
- `check` is the exception: it gates unconditionally with exit `1`, because a key that renders raw in production is a defect rather than a threshold.
112
-
113
- ## Getting Started on a New Project
114
-
115
- ```bash
116
- the-i18n-cli init # writes .i18n-mcp.json
117
- the-i18n-cli init --dry-run # report what it would write, touch nothing
118
- the-i18n-cli init --force # overwrite an existing config
119
- ```
120
-
121
- `init` detects the framework and writes **only what that framework cannot tell the tool itself**. On a Nuxt, Laravel, Vue or React project the locales, layers and default locale are resolved from the framework config on every run, so `init` deliberately does not copy them into `.i18n-mcp.json` — a generated copy is a second source of truth that drifts silently the day the framework config changes. What you get is the authoring context no adapter can derive:
122
-
123
- ```json
124
- {
125
- "$schema": "https://raw.githubusercontent.com/fabkho/the-i18n-kit/main/packages/mcp/schema.json",
126
- "context": "",
127
- "glossary": {},
128
- "translationPrompt": "",
129
- "localeNotes": {}
130
- }
131
- ```
132
-
133
- A project with no recognised framework is the exception: the generic adapter cannot resolve anything without `localeDirs` and `defaultLocale`, so `init` probes the common locale directory layouts and writes what it finds. The reference locale is guessed as the **fullest** locale file rather than the alphabetically first, since a source locale is the one everything else is translated from.
134
-
135
- The result reports which adapter matched and with what confidence, so you can tell why Nuxt was chosen over generic. `init` is non-interactive, so it runs unattended in a devcontainer or CI bootstrap, and it refuses to overwrite an existing config unless you pass `--force`. Even then it preserves any `localeDirs`, `defaultLocale` and `locales` already in the file — `--force` refreshes the scaffolding, it does not discard your locale wiring.
136
-
137
- ## Coverage
138
-
139
- ```bash
140
- the-i18n-cli status # per locale and per layer, plus one overall number
141
- the-i18n-cli status --layer root # one layer
142
- the-i18n-cli status --fail-under 90 # CI gate: exit 2 below the threshold
143
- ```
144
-
145
- One call replaces calling `missing` per layer and counting keys yourself:
146
-
147
- ```json
148
- {
149
- "summary": {
150
- "totalKeys": 8, "translatedKeys": 3, "missingKeys": 4, "emptyKeys": 1,
151
- "completionPercent": 37.5,
152
- "protectedLocales": ["de-formal"], "localesChecked": 2
153
- }
154
- }
155
- ```
156
-
157
- **Empty strings count as untranslated.** A scaffolded key nobody filled renders as a blank, so counting it as complete would let a locale read as done while showing gaps in the UI — consistent with how `missing` already treats them. They are reported separately from `missing` so you can tell a scaffold-and-forget locale from an untouched one.
158
-
159
- **Protected locales are reported but excluded from the overall figure**, in both the project and per-layer numbers. They are maintained by hand, so counting their gaps as project debt makes a healthy project read as failing and moves a number nobody can act on.
160
-
161
- The full per-locale and per-layer arrays grow with the project, so pass `--output-file` to write them to disk and get back only the summary.
162
-
163
- ## Referring to Locales
164
-
165
- Anywhere a locale is named — `--ref`, `--targets`, `protectedLocales`, the keys of a `write_translations` payload — you may use the locale's **code**, its **language tag**, or its **file name** (extension included). Resolution takes them in that order.
166
-
167
- **Prefer the code.** Codes are unique by construction; language tags are not. A project can declare an informal and a formal German that both carry `language: "de-DE"`, in which case `de-DE` is ambiguous — it resolves to the first match, and that depends on the order of your framework's locale array. The precedence rule guarantees only that a locale's own code is never shadowed by a *different* locale's language tag.
168
-
169
- A ref that matches nothing is reported rather than silently dropped:
20
+ The binary is `the-i18n-cli`, whatever the package is called.
170
21
 
171
- ```json
172
- {
173
- "written": ["common.save"],
174
- "filesWritten": 2,
175
- "unresolvedLocales": [
176
- { "ref": "de-DE-formal", "keys": ["common.save"], "suggestion": "Did you mean \"de-formal\" or \"de-DE\" or \"de-DE-formal.json\"?" }
177
- ]
178
- }
179
- ```
180
-
181
- `unresolvedLocales` is the only reliable signal that a write did less than you asked: the key still appears in `written` because the other locales succeeded, and `filesWritten` is short by one. A ref matching several locales is reported the same way under `ambiguousLocales`, naming every candidate and the one that was used. Both fields are absent when every ref resolves uniquely, so a clean write is byte-for-byte what it always was.
182
-
183
- Note that a locale's `file` must be given with its extension (`de-DE-formal.json`); the bare stem is not a valid ref.
184
-
185
- ## Translation Modes
186
-
187
- `translate` and `translate-key` run in one of two modes — every result reports which one ran (`mode: "provider" | "agent" | "dry-run"`).
188
-
189
- **Provider mode** — pass `--provider` (`openai`, `anthropic`, or `google`) and `--model`; the API key comes from `--apiKey` or the provider's env var (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY`). The CLI calls the LLM directly and writes validated results:
190
-
191
- ```bash
192
- the-i18n-cli translate --layer root --provider google --model gemini-2.5-flash
193
- the-i18n-cli translate --layer root --targets de-DE,fr-FR --batchSize 25 --provider openai --model gpt-4o-mini
194
- ```
195
-
196
- To reach an endpoint that speaks the same protocol — a gateway, a self-hosted model server, a proxy — add `--baseUrl`, or set `I18N_BASE_URL`, or `providerBaseUrl` in `.i18n-mcp.json`, in that order of precedence:
22
+ ## Quick Start
197
23
 
198
24
  ```bash
199
- the-i18n-cli translate --layer root --provider openai --model llama3 \
200
- --baseUrl http://localhost:11434/v1 --apiKey unused
25
+ the-i18n-cli init # write a config from what it detects
26
+ the-i18n-cli status # coverage per locale and per layer
27
+ the-i18n-cli missing # what is not translated yet
28
+ the-i18n-cli check # keys used in code but defined nowhere
29
+ the-i18n-cli remove-orphans # keys defined but unused (previews by default)
201
30
  ```
202
31
 
203
- An API key is still required even when the endpoint ignores it — pass any placeholder for a local server. This overrides the endpoint only, so providers that also change the request shape or auth header (Azure OpenAI among them) are not reachable this way.
204
-
205
- `google` has no endpoint override, so a base URL with it is rejected as a configuration error rather than silently ignored.
32
+ [Cold start guide](https://fabkho.github.io/the-i18n-kit/getting-started/cold-start) ·
33
+ [every command and flag](https://fabkho.github.io/the-i18n-kit/reference/cli) ·
34
+ [the library API](https://fabkho.github.io/the-i18n-kit/reference/programmatic-api)
206
35
 
207
- **Agent mode** — no `--provider` given. Nothing is translated: keys are reported as `skipped` with reason `no-provider`, and the result explains how to enable provider mode. (In the MCP server, agent mode instead returns fallback contexts for the host agent — see [the-i18n-mcp](https://www.npmjs.com/package/the-i18n-mcp).)
36
+ ## Documentation
208
37
 
209
- ### Result contract
38
+ | | |
39
+ |---|---|
40
+ | [Commands](https://fabkho.github.io/the-i18n-kit/reference/cli) | Generated from the command definitions |
41
+ | [Configuration](https://fabkho.github.io/the-i18n-kit/configuration/where-config-lives) | Where it lives, precedence, [every field](https://fabkho.github.io/the-i18n-kit/configuration/reference) |
42
+ | [Frameworks](https://fabkho.github.io/the-i18n-kit/frameworks/detection) | How detection works, and what each adapter reads |
43
+ | [Monorepos and layers](https://fabkho.github.io/the-i18n-kit/monorepos/layers) | Why usage in one app does not protect a key in another |
44
+ | [Referring to locales](https://fabkho.github.io/the-i18n-kit/configuration/locale-refs) | Codes, language tags, and why the code is the one to use |
45
+ | [CI/CD](https://fabkho.github.io/the-i18n-kit/ci-cd/github-actions) | The Action and the GitLab template |
210
46
 
211
- Translate results account for every key:
47
+ | [Translation modes](https://fabkho.github.io/the-i18n-kit/concepts/translation-modes) | Provider and agent mode, the result contract, what is validated before writing |
212
48
 
213
- - `translated` keys written
214
- - `wouldTranslate` `--dryRun` only: keys that would be translated
215
- - `failed` — with a reason: `provider-error`, `omitted-by-model`, `truncated`, `placeholder-mismatch`, `plural-mismatch`, `write-error`
216
- - `skipped` — with a reason: `no-provider`, `already-translated`, `protected-locale`
217
- - Invariant: `missing = translated + wouldTranslate + failed + skipped`
49
+ The section below has not moved to the site yet. It is the deepest material
50
+ here, and it is being rewritten against the new extraction architecture — see
51
+ [#358](https://github.com/fabkho/the-i18n-kit/issues/358).
218
52
 
219
- Translations are validated before writing: placeholder parity per vue-i18n plural variant (`{placeholders}`, `@:linked.refs`; `:params` for PHP) and plural variant-count parity with the source. Failing values are rejected into `failed` instead of written.
220
-
221
- Locales listed in `protectedLocales` (see Project Config) are excluded from default translate targets and reported as `skipped` with reason `protected-locale`; naming one explicitly in `--targets` overrides the protection with a warning.
222
53
 
223
54
  ## How Orphan Detection Works
224
55
 
@@ -265,104 +96,6 @@ Prefixes assembled in another scope defeat this even when they are literal — a
265
96
 
266
97
  `remove-orphans` is dry-run by default; run removals as a reviewed MR and treat the report's `uncertainKeys`/`dynamicKeys` sections as the audit trail.
267
98
 
268
- ## Supported Frameworks
269
-
270
- | Framework | Locale Format | Auto-Detection | Locale Directories Probed |
271
- |-----------|--------------|----------------|---------------------------|
272
- | **Nuxt** (v3+) | JSON | `nuxt.config.ts` with `@nuxtjs/i18n` | `i18n/locales/` per app and per layer; honours each layer's `langDir` (default `locales`) |
273
- | **Laravel** (9+) | PHP arrays or JSON | `artisan`, `composer.json`, `lang/` | `lang/` or `resources/lang/` — PHP subdirectories (`lang/en/*.php`) or flat JSON (`lang/en.json`) |
274
- | **Vue** (SPA, v3) | JSON | `vue` in dependencies without Nuxt; `vue-i18n` raises confidence | `src/locales`, `locales`, `src/i18n/locales`, `i18n/locales`, `src/plugins/i18n/locales`, `src/i18n` — or a `localeDir`/`messages` path read out of `src/i18n/index.{ts,js}`, `src/plugins/i18n.{ts,js}`, `src/i18n.{ts,js}`, `i18n.{ts,js}` |
275
- | **React / Next.js** | JSON | `next`, or `react` + `react-dom`, without Vue/Nuxt; `next-intl`, `next-translate`, `next-i18next`, `react-i18next` or `react-intl` raises confidence | `messages`, `public/locales`, `locales`, `src/i18n`, `src/locales`, `i18n` — namespaced (`messages/en/common.json`) or flat (`locales/en.json`). A `next.config.{ts,js,mjs}` using `createNextIntlPlugin` or `next-translate` pins the directory directly |
276
- | **Generic** | JSON or PHP | `localeDirs` + `defaultLocale` in `.i18n-mcp.json` | Exactly the paths listed in `localeDirs` |
277
-
278
- Detection is confidence-scored: the highest-scoring adapter wins, and a `.i18n-mcp.json` carrying both `localeDirs` and `defaultLocale` outscores framework inference. Set `"framework": "vue"` (or any adapter name) to force one adapter.
279
-
280
- ### Read, not guessed
281
-
282
- Where a project already declares its locales, the adapter reads that file rather than inferring from directory order:
283
-
284
- | Setup | Read from | Gives |
285
- |---|---|---|
286
- | next-intl | `src/i18n/routing.ts` — `defineRouting({ ... })` | `locales`, `defaultLocale` |
287
- | next-translate | `i18n.js` | `locales`, `defaultLocale` |
288
- | Next.js Pages Router | `next.config.{ts,js,mjs}` — `i18n: { ... }` | `locales`, `defaultLocale` |
289
- | Vue + `@intlify/unplugin-vue-i18n` | `vite.config.{ts,js}` — the plugin's `include` | locale directories |
290
-
291
- These files are executed, so a `next.config.js` wrapped in `withNextIntl(...)` or `withSentryConfig(...)` may fail without the right environment. That is never fatal: the CLI warns and falls back to the directory probing above, exactly as it behaved before it could read them.
292
-
293
- A value you declare yourself always wins over both. To pin the reference locale regardless of what any framework config says:
294
-
295
- ```ts
296
- import { defineI18nKitConfig } from 'the-i18n-cli/config'
297
-
298
- export default defineI18nKitConfig({
299
- defaultLocale: 'en',
300
- localeDirs: ['src/locales'],
301
- })
302
- ```
303
-
304
- ## Programmatic API
305
-
306
- The CLI also exports all operations as a library for use in other tools:
307
-
308
- ```ts
309
- import { detectConfig, getMissingTranslations, addTranslations, translateKey } from 'the-i18n-cli'
310
-
311
- const config = await detectConfig('/path/to/project')
312
- const missing = await getMissingTranslations({ projectDir: '/path/to/project' })
313
-
314
- await translateKey({
315
- projectDir: '/path/to/project',
316
- layer: 'root',
317
- key: 'common.actions.save',
318
- sourceLocale: 'en-US',
319
- sourceValue: 'Save',
320
- targetLocales: 'all',
321
- overwrite: true,
322
- })
323
- ```
324
-
325
- ## Project Config
326
-
327
- Project-specific context goes in `i18n-kit.config.ts` at your project root, where the editor checks it:
328
-
329
- ```ts
330
- import { defineI18nKitConfig } from 'the-i18n-cli/config'
331
-
332
- export default defineI18nKitConfig({
333
- context: 'B2B SaaS booking platform',
334
- glossary: {
335
- Booking: "Core concept. Dutch: 'Boeking'.",
336
- },
337
- protectedLocales: ['en-us', 'de-formal'],
338
- })
339
- ```
340
-
341
- Read directly, with no build step — a `protectedLocales` entry that only takes effect once something has been built is a locale that quietly goes unprotected in a fresh checkout. `.ts`, `.mts`, `.js`, `.mjs` and `.cjs` all work, and the file is looked up from the working directory upwards, the way `eslint` and `tsconfig` resolve theirs.
342
-
343
- `.i18n-mcp.json` accepts the same keys and remains supported:
344
-
345
- ```json
346
- {
347
- "$schema": "node_modules/the-i18n-mcp/schema.json",
348
- "context": "B2B SaaS booking platform",
349
- "glossary": {
350
- "Booking": "Core concept. Dutch: 'Boeking'.",
351
- "Resource": "A bookable entity (room, desk, person)"
352
- },
353
- "translationPrompt": "Professional but approachable tone. Keep translations concise.",
354
- "localeNotes": {
355
- "de": "Informal German (du)",
356
- "de-formal": "Formal German (Sie)"
357
- },
358
- "protectedLocales": ["en-us", "de-formal"]
359
- }
360
- ```
361
-
362
- Use one or the other. Both may be present, but declaring the same key in both is an error naming both files rather than a silent precedence rule.
363
-
364
- See the [full config reference](https://github.com/fabkho/the-i18n-kit#project-config) for all options. `samplingPreferences` is deprecated and ignored (accepted for backward compatibility) — configure a provider instead.
365
-
366
99
  ## License
367
100
 
368
101
  [MIT](https://github.com/fabkho/the-i18n-kit/blob/main/LICENSE)
@@ -1,11 +1,11 @@
1
1
  import { n as writeResult } from "./stdout-guard-sJHFWVem.js";
2
2
  import { i as toErrorMessage } from "./errors-coI1dhw1.js";
3
- import { a as resolveProviderBaseUrl, i as createTranslateFn, l as log, s as loadProjectConfig, t as BASE_URL_ENV } from "./providers-BAwPjeCy.js";
3
+ import { a as resolveProviderBaseUrl, i as createTranslateFn, l as log, s as loadProjectConfig, t as BASE_URL_ENV } from "./providers-CHE2ffi6.js";
4
4
  import { defineCommand } from "citty";
5
5
  //#region src/commands/_shared.ts
6
6
  /** Factory for commands that call an operation and output its result. */
7
7
  function createCommand(opts) {
8
- return defineCommand({
8
+ return withGates(defineCommand({
9
9
  meta: {
10
10
  name: opts.name,
11
11
  description: opts.description
@@ -17,8 +17,7 @@ function createCommand(opts) {
17
17
  async run({ args }) {
18
18
  try {
19
19
  const result = await opts.run(args);
20
- const runFailed = isTotalFailure(result) || (opts.failWhen?.(result) ?? false);
21
- const decision = resolveExitCode(result, requestedGates(opts.gates ?? [], args), runFailed);
20
+ const decision = resolveExitCode(result, requestedGates(opts.gates ?? [], args), isTotalFailure(result));
22
21
  outputResult(withGateReport(result, decision.tripped), args);
23
22
  if (decision.code !== 0) process.exitCode = decision.code;
24
23
  } catch (error) {
@@ -26,7 +25,10 @@ function createCommand(opts) {
26
25
  process.exitCode = 1;
27
26
  }
28
27
  }
29
- });
28
+ }), opts.gates ?? []);
29
+ }
30
+ function withGates(def, gates) {
31
+ return Object.assign(def, { gates });
30
32
  }
31
33
  /**
32
34
  * Pure decision from an operation result plus the gates the caller requested
@@ -72,19 +74,29 @@ function trips(gate, observed) {
72
74
  function requestedGates(specs, args) {
73
75
  const requested = [];
74
76
  for (const spec of specs) {
75
- const raw = args[spec.flag];
76
- if (raw === void 0 || raw === null || raw === false || raw === "") continue;
77
- const threshold = spec.threshold ?? Number(raw);
78
- if (!Number.isFinite(threshold)) continue;
79
- requested.push({
77
+ const resolved = spec.flag === void 0 ? {
78
+ name: spec.name,
79
+ threshold: spec.threshold
80
+ } : {
80
81
  name: kebabCase(spec.flag),
82
+ threshold: thresholdFromFlag(spec, args)
83
+ };
84
+ if (resolved.threshold === void 0 || !Number.isFinite(resolved.threshold)) continue;
85
+ requested.push({
86
+ name: resolved.name,
81
87
  counter: spec.counter,
82
88
  direction: spec.direction ?? "above",
83
- threshold
89
+ threshold: resolved.threshold
84
90
  });
85
91
  }
86
92
  return requested;
87
93
  }
94
+ /** The gate's threshold, or undefined when this invocation did not ask for it. */
95
+ function thresholdFromFlag(spec, args) {
96
+ const raw = args[spec.flag];
97
+ if (raw === void 0 || raw === null || raw === false || raw === "") return void 0;
98
+ return spec.threshold ?? Number(raw);
99
+ }
88
100
  /**
89
101
  * Name a tripped gate by the flag that requested it, so the JSON says
90
102
  * "fail-on-missing" — what the user typed — rather than "failOnMissing".
@@ -245,4 +257,4 @@ function parseJsonArg(value, argName) {
245
257
  //#endregion
246
258
  export { resolveProviderTranslateFn as a, providerArgs as i, emitErrorResult as n, splitList as o, parseJsonArg as r, createCommand as t };
247
259
 
248
- //# sourceMappingURL=_shared-Dv1EW2gy.js.map
260
+ //# sourceMappingURL=_shared-DFyy3sh8.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_shared-DFyy3sh8.js","names":[],"sources":["../src/commands/_shared.ts"],"sourcesContent":["import { defineCommand } from 'citty'\nimport type { CommandDef } from 'citty'\nimport { log } from '../utils/logger.js'\nimport { toErrorMessage } from '../utils/errors.js'\nimport { writeResult } from '../utils/stdout-guard.js'\nimport { createTranslateFn, resolveProviderBaseUrl, BASE_URL_ENV } from '../llm/providers.js'\nimport type { LlmProvider } from '../llm/providers.js'\nimport type { TranslateFn } from '../core/types.js'\nimport { loadProjectConfig } from '../config/project-config.js'\n\n/** Factory for commands that call an operation and output its result. */\nexport function createCommand(opts: {\n name: string\n description: string\n args?: Record<string, unknown>\n /**\n * CI gates this command evaluates. The factory reads the requesting flag off\n * args and evaluates every gate uniformly, so no command grows bespoke exit\n * logic. Declaring a flagged gate does not add its flag — pair each spec\n * with an entry in `args`. A spec without a flag is always evaluated.\n */\n gates?: GateSpec[]\n /**\n * Receives citty's parsed args. `any` is deliberate: each command declares\n * its own `args` shape and citty does not thread that type through to the\n * handler, so narrowing here only moves the cast into all nineteen command\n * modules. The shape is validated by citty against the `args` above.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- see above\n run: (args: any) => Promise<unknown>\n}): CommandDef {\n // Retained on the definition, not only consumed here: the generated CLI\n // reference has to state which commands fail a build on findings, and a gate\n // with no flag leaves no trace in the arg descriptions to infer it from.\n return withGates(defineCommand({\n meta: { name: opts.name, description: opts.description },\n args: { ...sharedArgs, ...(opts.args ?? {}) },\n async run({ args }) {\n try {\n const result = await opts.run(args)\n const decision = resolveExitCode(result, requestedGates(opts.gates ?? [], args), isTotalFailure(result))\n outputResult(withGateReport(result, decision.tripped), args)\n // Assign only on a non-zero decision: a clean run must leave the exit\n // code exactly as it found it, as it did before gates existed.\n if (decision.code !== EXIT_SUCCESS) process.exitCode = decision.code\n } catch (error) {\n emitErrorResult(error, args)\n process.exitCode = EXIT_RUN_FAILED\n }\n },\n }) as CommandDef, opts.gates ?? [])\n}\n\n/**\n * A command definition carrying the gates its factory evaluates. Read\n * structurally by the reference generator rather than by importing this type,\n * which would make the docs build depend on the CLI's internals.\n */\ninterface GatedCommandDef extends CommandDef {\n gates: GateSpec[]\n}\n\nfunction withGates(def: CommandDef, gates: GateSpec[]): GatedCommandDef {\n return Object.assign(def, { gates })\n}\n\n/** The run succeeded and no gate tripped. */\nexport const EXIT_SUCCESS = 0\n/** The run itself failed — a bad API key, an unreadable project, a total translate failure. */\nexport const EXIT_RUN_FAILED = 1\n/** The run succeeded but a requested gate tripped — findings exist, the tool worked. */\nexport const EXIT_GATE_TRIPPED = 2\n\n/**\n * A CI gate a command evaluates. `counter` is the field of `result.summary`\n * carrying the observed value.\n *\n * The two shapes are a union rather than one type with optional halves so the\n * invariants hold at compile time: a flagged gate always has a flag to read\n * its name and threshold from, and a flagless one always carries both itself.\n * Stated as options, `{ counter, threshold }` type-checks and then has no name\n * to report the gate under.\n */\nexport type GateSpec = FlaggedGateSpec | AlwaysOnGateSpec\n\ninterface GateSpecBase {\n counter: string\n /** 'above' trips when observed > threshold (default); 'below' when observed < threshold. */\n direction?: 'above' | 'below'\n}\n\n/**\n * Requested by a flag, and evaluated only when that flag is passed. Omitting\n * `threshold` takes it from the flag's own value, so a boolean flag pairs with\n * `threshold: 0` and a numeric one (`--fail-under 90`) omits it.\n */\nexport interface FlaggedGateSpec extends GateSpecBase {\n flag: string\n name?: never\n threshold?: number\n}\n\n/**\n * Always evaluated, for findings that are a defect rather than a threshold — a\n * key that renders raw in production is not something you opt into caring\n * about. It still reports as a gate: the run succeeded, and what it found is\n * what you are being told about.\n */\nexport interface AlwaysOnGateSpec extends GateSpecBase {\n flag?: never\n name: string\n threshold: number\n}\n\n/** A gate the caller asked for, with its threshold already resolved. */\nexport interface RequestedGate {\n name: string\n counter: string\n direction: 'above' | 'below'\n threshold: number\n}\n\n/** A gate that tripped, as reported in the result. */\nexport interface TrippedGate extends RequestedGate {\n observed: number\n}\n\nexport interface ExitDecision {\n code: typeof EXIT_SUCCESS | typeof EXIT_RUN_FAILED | typeof EXIT_GATE_TRIPPED\n tripped: TrippedGate[]\n}\n\n/**\n * Pure decision from an operation result plus the gates the caller requested\n * to an exit code. A failed run outranks a tripped gate — exit 1 wins over\n * exit 2 — and gates are not even consulted in that case, because counters\n * from a run that fell over say nothing about the project.\n */\nexport function resolveExitCode(\n result: unknown,\n gates: RequestedGate[],\n runFailed = false,\n): ExitDecision {\n if (runFailed) return { code: EXIT_RUN_FAILED, tripped: [] }\n\n const tripped: TrippedGate[] = []\n for (const gate of gates) {\n const observed = observedValue(result, gate.counter)\n if (observed === undefined || !trips(gate, observed)) continue\n tripped.push({ ...gate, observed })\n }\n\n return {\n code: tripped.length > 0 ? EXIT_GATE_TRIPPED : EXIT_SUCCESS,\n tripped,\n }\n}\n\n/**\n * Read a gate's counter off result.summary. Works on inline results and on\n * the { reportFile, summary } shape alike, since both carry the summary.\n * A missing or non-numeric counter yields undefined and never trips a gate.\n */\nfunction observedValue(result: unknown, counter: string): number | undefined {\n if (result === null || typeof result !== 'object') return undefined\n const summary = (result as Record<string, unknown>).summary\n if (summary === null || typeof summary !== 'object') return undefined\n const value = (summary as Record<string, unknown>)[counter]\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined\n}\n\nfunction trips(gate: RequestedGate, observed: number): boolean {\n return gate.direction === 'below' ? observed < gate.threshold : observed > gate.threshold\n}\n\n/** Filter the declared gates down to the ones this invocation asked for. */\nfunction requestedGates(specs: GateSpec[], args: Record<string, unknown>): RequestedGate[] {\n const requested: RequestedGate[] = []\n for (const spec of specs) {\n const resolved = spec.flag === undefined\n ? { name: spec.name, threshold: spec.threshold }\n : { name: kebabCase(spec.flag), threshold: thresholdFromFlag(spec, args) }\n if (resolved.threshold === undefined || !Number.isFinite(resolved.threshold)) continue\n requested.push({\n name: resolved.name,\n counter: spec.counter,\n direction: spec.direction ?? 'above',\n threshold: resolved.threshold,\n })\n }\n return requested\n}\n\n/** The gate's threshold, or undefined when this invocation did not ask for it. */\nfunction thresholdFromFlag(spec: FlaggedGateSpec, args: Record<string, unknown>): number | undefined {\n const raw = args[spec.flag]\n if (raw === undefined || raw === null || raw === false || raw === '') return undefined\n return spec.threshold ?? Number(raw)\n}\n\n/**\n * Name a tripped gate by the flag that requested it, so the JSON says\n * \"fail-on-missing\" — what the user typed — rather than \"failOnMissing\".\n */\nfunction kebabCase(flag: string): string {\n return flag.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)\n}\n\n/**\n * Attach the gate report without disturbing the rest of the result: consumers\n * parsing today's shape keep working, and a run where nothing tripped is\n * byte-for-byte what it was before gates existed.\n */\nfunction withGateReport(result: unknown, tripped: TrippedGate[]): unknown {\n if (tripped.length === 0) return result\n if (result === null || typeof result !== 'object' || Array.isArray(result)) return result\n return { ...(result as Record<string, unknown>), gatesTripped: tripped }\n}\n\n/**\n * True when a run completed but achieved nothing: failures present and zero\n * successes. Covers translate-missing-style results (summary.totalFailed /\n * summary.totalTranslated) and translate-key-style results (top-level\n * failed[] / translated[]). Results without those fields are never a\n * total failure, so unrelated commands are unaffected.\n */\nexport function isTotalFailure(result: unknown): boolean {\n if (result === null || typeof result !== 'object') return false\n const r = result as Record<string, unknown>\n\n const summary = r.summary\n if (summary !== null && typeof summary === 'object') {\n const s = summary as Record<string, unknown>\n if (typeof s.totalFailed === 'number' && typeof s.totalTranslated === 'number') {\n return s.totalFailed > 0 && s.totalTranslated === 0\n }\n }\n\n if (Array.isArray(r.failed) && Array.isArray(r.translated)) {\n return r.failed.length > 0 && r.translated.length === 0\n }\n\n return false\n}\n\nconst sharedArgs = {\n projectDir: {\n type: 'string' as const,\n alias: 'd',\n description: 'Project directory (default: cwd)',\n },\n json: {\n type: 'boolean' as const,\n description: 'Output as JSON (default for non-TTY)',\n default: false,\n },\n}\n\n/** Output result — stdout always carries the result, machine-parseable when piped/--json */\nfunction outputResult(data: unknown, args: { json?: boolean }): void {\n const jsonMode = args.json || !process.stdout.isTTY\n if (\n !jsonMode &&\n data !== null &&\n typeof data === 'object' &&\n 'reportFile' in (data as Record<string, unknown>)\n ) {\n const { reportFile, ...rest } = data as Record<string, unknown>\n log.info(`Wrote report to: ${reportFile}`)\n writeResult(JSON.stringify(rest, null, 2) + '\\n')\n return\n }\n // JSON mode emits the full result (including reportFile when present) as pure JSON\n writeResult(JSON.stringify(data, null, 2) + '\\n')\n}\n\n/**\n * Failure output. In JSON mode stdout must still carry parseable JSON —\n * consumers pipe it into jq, and zero bytes is a parse error — so the\n * structured error object IS the result on stdout; the human-readable\n * message goes to stderr in every mode. Exit code stays non-zero (callers\n * set it).\n */\nexport function emitErrorResult(error: unknown, args: { json?: boolean }): void {\n log.error(toErrorMessage(error))\n const jsonMode = args.json || !process.stdout.isTTY\n if (!jsonMode) return\n const payload = { error: { code: errorCode(error), message: toErrorMessage(error) } }\n writeResult(JSON.stringify(payload, null, 2) + '\\n')\n}\n\n/** ToolError/FileIOError carry codes; Node errors expose e.g. ENOENT. */\nfunction errorCode(error: unknown): string {\n if (error instanceof Error) {\n const code = (error as { code?: unknown }).code\n if (typeof code === 'string') return code\n if (error.name === 'ConfigError') return 'CONFIG_ERROR'\n }\n return 'UNKNOWN_ERROR'\n}\n\n/** Provider selection flags shared by the translate commands. */\nexport const providerArgs = {\n provider: { type: 'string' as const, description: 'LLM provider: \"openai\", \"anthropic\", or \"google\". Required for automatic translation.', valueHint: 'openai|anthropic|google' },\n model: { type: 'string' as const, description: 'Model name (required when --provider is set)' },\n apiKey: { type: 'string' as const, description: 'API key (falls back to OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY env).' },\n baseUrl: { type: 'string' as const, description: `Provider base URL for gateways, self-hosted models and proxies speaking the provider's protocol. Falls back to ${BASE_URL_ENV}, then providerBaseUrl in .i18n-mcp.json. Not supported by \"google\".` },\n}\n\n/**\n * Build a TranslateFn from the provider flags, or undefined when no provider\n * was given (agent mode). The base URL resolves flag > env > project config;\n * the config file is only read when neither of the first two is set, so a\n * fully-flagged invocation never depends on config discovery.\n */\nexport async function resolveProviderTranslateFn(args: {\n provider?: string\n model?: string\n apiKey?: string\n baseUrl?: string\n projectDir?: string\n}): Promise<TranslateFn | undefined> {\n if (!args.provider) return undefined\n if (!args.model) {\n throw new Error('--model is required when --provider is set')\n }\n\n let baseUrl = resolveProviderBaseUrl({ flag: args.baseUrl, env: process.env[BASE_URL_ENV] })\n if (!baseUrl) {\n const projectConfig = await loadProjectConfig(args.projectDir ?? process.cwd())\n baseUrl = resolveProviderBaseUrl({ config: projectConfig?.providerBaseUrl })\n }\n if (baseUrl) log.debug(`Provider base URL: ${redactBaseUrl(baseUrl)}`)\n\n return createTranslateFn({\n provider: args.provider as LlmProvider,\n model: args.model,\n apiKey: args.apiKey,\n baseUrl,\n })\n}\n\n/**\n * Strip anything secret-shaped from a base URL before it reaches a log.\n * Gateways are routinely addressed as https://user:pass@host or with the key\n * in a query parameter, so keep only origin and path.\n */\nexport function redactBaseUrl(raw: string): string {\n let url: URL\n try {\n url = new URL(raw)\n } catch {\n return '<unparseable base URL>'\n }\n const credentials = url.username || url.password ? '<redacted>@' : ''\n const query = url.search || url.hash ? ' (query redacted)' : ''\n return `${url.protocol}//${credentials}${url.host}${url.pathname}${query}`\n}\n\n/** Split a comma-separated string into a trimmed array, or return undefined */\nexport function splitList(val: string | undefined): string[] | undefined {\n if (!val) return undefined\n return val.split(',').map(s => s.trim()).filter(Boolean)\n}\n\n/** Parse a JSON string with a user-friendly error */\nexport function parseJsonArg<T = Record<string, Record<string, string>>>(\n value: string,\n argName: string,\n): T {\n try {\n return JSON.parse(value) as T\n } catch (err) {\n const detail = err instanceof SyntaxError ? err.message : String(err)\n throw new Error(`Invalid JSON in --${argName}: ${detail}`)\n }\n}\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import "./stdout-guard-sJHFWVem.js";
2
- import { c as addTranslations } from "./operations-BCJd7in3.js";
3
- import "./providers-BAwPjeCy.js";
4
- import "./php-reader-DuZ0Hyl_.js";
5
- import { r as parseJsonArg, t as createCommand } from "./_shared-Dv1EW2gy.js";
2
+ import { c as addTranslations } from "./operations-CTo44gPu.js";
3
+ import "./providers-CHE2ffi6.js";
4
+ import "./php-reader-3Fgw80zK.js";
5
+ import { r as parseJsonArg, t as createCommand } from "./_shared-DFyy3sh8.js";
6
6
  //#region src/commands/add.ts
7
7
  var add_default = createCommand({
8
8
  name: "add",
@@ -37,4 +37,4 @@ var add_default = createCommand({
37
37
  //#endregion
38
38
  export { add_default as default };
39
39
 
40
- //# sourceMappingURL=add-BvXrJkqZ.js.map
40
+ //# sourceMappingURL=add-C-qs8iBa.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"add-BvXrJkqZ.js","names":[],"sources":["../src/commands/add.ts"],"sourcesContent":["import { createCommand, parseJsonArg } from './_shared.js'\nimport { addTranslations } from '../core/operations.js'\n\nexport default createCommand({\n name: 'add',\n description: 'Add new translation keys (skips keys that already exist)',\n args: {\n layer: { type: 'string', description: 'Layer name', required: true },\n translations: { type: 'string', description: 'JSON: { \"key\": { \"en\": \"val\", \"de\": \"val\" } }', required: true },\n dryRun: { type: 'boolean', description: 'Preview changes without writing', default: false },\n },\n async run(args) {\n const translations = parseJsonArg<Record<string, Record<string, string>>>(args.translations, 'translations')\n return addTranslations({ layer: args.layer, translations, dryRun: args.dryRun, projectDir: args.projectDir })\n },\n})\n"],"mappings":";;;;;;AAGA,IAAA,cAAe,cAAc;CAC3B,MAAM;CACN,aAAa;CACb,MAAM;EACJ,OAAO;GAAE,MAAM;GAAU,aAAa;GAAc,UAAU;GAAM;EACpE,cAAc;GAAE,MAAM;GAAU,aAAa;GAAiD,UAAU;GAAM;EAC9G,QAAQ;GAAE,MAAM;GAAW,aAAa;GAAmC,SAAS;GAAO;EAC5F;CACD,MAAM,IAAI,MAAM;EACd,MAAM,eAAe,aAAqD,KAAK,cAAc,eAAe;AAC5G,SAAO,gBAAgB;GAAE,OAAO,KAAK;GAAO;GAAc,QAAQ,KAAK;GAAQ,YAAY,KAAK;GAAY,CAAC;;CAEhH,CAAC"}
1
+ {"version":3,"file":"add-C-qs8iBa.js","names":[],"sources":["../src/commands/add.ts"],"sourcesContent":["import { createCommand, parseJsonArg } from './_shared.js'\nimport { addTranslations } from '../core/operations.js'\n\nexport default createCommand({\n name: 'add',\n description: 'Add new translation keys (skips keys that already exist)',\n args: {\n layer: { type: 'string', description: 'Layer name', required: true },\n translations: { type: 'string', description: 'JSON: { \"key\": { \"en\": \"val\", \"de\": \"val\" } }', required: true },\n dryRun: { type: 'boolean', description: 'Preview changes without writing', default: false },\n },\n async run(args) {\n const translations = parseJsonArg<Record<string, Record<string, string>>>(args.translations, 'translations')\n return addTranslations({ layer: args.layer, translations, dryRun: args.dryRun, projectDir: args.projectDir })\n },\n})\n"],"mappings":";;;;;;AAGA,IAAA,cAAe,cAAc;CAC3B,MAAM;CACN,aAAa;CACb,MAAM;EACJ,OAAO;GAAE,MAAM;GAAU,aAAa;GAAc,UAAU;GAAM;EACpE,cAAc;GAAE,MAAM;GAAU,aAAa;GAAiD,UAAU;GAAM;EAC9G,QAAQ;GAAE,MAAM;GAAW,aAAa;GAAmC,SAAS;GAAO;EAC5F;CACD,MAAM,IAAI,MAAM;EACd,MAAM,eAAe,aAAqD,KAAK,cAAc,eAAe;AAC5G,SAAO,gBAAgB;GAAE,OAAO,KAAK;GAAO;GAAc,QAAQ,KAAK;GAAQ,YAAY,KAAK;GAAY,CAAC;;CAEhH,CAAC"}
package/dist/bin.js CHANGED
@@ -3,7 +3,12 @@ import { t as guardStdout } from "./stdout-guard-sJHFWVem.js";
3
3
  //#region src/bin.ts
4
4
  const args = process.argv.slice(2);
5
5
  if (!(args.includes("--help") || args.includes("-h") || args.includes("--version") || args.includes("-v"))) guardStdout();
6
- const { runCli } = await import("./cli-CiX4llY1.js");
6
+ const { renameNotice } = await import("./rename-notice-Cx1vuTRz.js");
7
+ const { createRequire } = await import("node:module");
8
+ const { name } = createRequire(import.meta.url)("../package.json");
9
+ const notice = renameNotice(name);
10
+ if (notice) process.stderr.write(`[the-i18n-cli] ${notice}\n`);
11
+ const { runCli } = await import("./cli-DsFuVJNP.js");
7
12
  await runCli();
8
13
  //#endregion
9
14
  export {};
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.js","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n// Install the stdout guard before anything else loads, so third-party logs\n// (e.g. Nuxt modules during config detection) can never pollute the\n// machine-readable output on stdout. Help/version invocations skip the guard:\n// they never load third-party code and their output belongs on stdout\n// (mirrors the flag check in cli.ts, which cannot be imported before the\n// guard decision).\nimport { guardStdout } from './utils/stdout-guard.js'\n\nconst args = process.argv.slice(2)\nconst isHelpOrVersion = args.includes('--help') || args.includes('-h')\n || args.includes('--version') || args.includes('-v')\nif (!isHelpOrVersion) {\n guardStdout()\n}\nconst { runCli } = await import('./cli.js')\nawait runCli()\n"],"mappings":";;;AASA,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAGlC,IAAI,EAFoB,KAAK,SAAS,SAAS,IAAI,KAAK,SAAS,KAAK,IACjE,KAAK,SAAS,YAAY,IAAI,KAAK,SAAS,KAAK,EAEpD,cAAa;AAEf,MAAM,EAAE,WAAW,MAAM,OAAO;AAChC,MAAM,QAAQ"}
1
+ {"version":3,"file":"bin.js","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n// Install the stdout guard before anything else loads, so third-party logs\n// (e.g. Nuxt modules during config detection) can never pollute the\n// machine-readable output on stdout. Help/version invocations skip the guard:\n// they never load third-party code and their output belongs on stdout\n// (mirrors the flag check in cli.ts, which cannot be imported before the\n// guard decision).\nimport { guardStdout } from './utils/stdout-guard.js'\n\nconst args = process.argv.slice(2)\nconst isHelpOrVersion = args.includes('--help') || args.includes('-h')\n || args.includes('--version') || args.includes('-v')\nif (!isHelpOrVersion) {\n guardStdout()\n}\n// Renamed packages announce themselves once per invocation, on stderr so the\n// JSON on stdout is untouched. The package reads its own name, so this is\n// silent when running as @the-i18n-kit/cli (#315).\nconst { renameNotice } = await import('./utils/rename-notice.js')\nconst { createRequire } = await import('node:module')\nconst { name } = createRequire(import.meta.url)('../package.json') as { name: string }\nconst notice = renameNotice(name)\nif (notice) process.stderr.write(`[the-i18n-cli] ${notice}\\n`)\n\nconst { runCli } = await import('./cli.js')\nawait runCli()\n"],"mappings":";;;AASA,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAGlC,IAAI,EAFoB,KAAK,SAAS,SAAS,IAAI,KAAK,SAAS,KAAK,IACjE,KAAK,SAAS,YAAY,IAAI,KAAK,SAAS,KAAK,EAEpD,cAAa;AAKf,MAAM,EAAE,iBAAiB,MAAM,OAAO;AACtC,MAAM,EAAE,kBAAkB,MAAM,OAAO;AACvC,MAAM,EAAE,SAAS,cAAc,OAAO,KAAK,IAAI,CAAC,kBAAkB;AAClE,MAAM,SAAS,aAAa,KAAK;AACjC,IAAI,OAAQ,SAAQ,OAAO,MAAM,kBAAkB,OAAO,IAAI;AAE9D,MAAM,EAAE,WAAW,MAAM,OAAO;AAChC,MAAM,QAAQ"}
@@ -1,24 +1,12 @@
1
1
  import "./stdout-guard-sJHFWVem.js";
2
- import { t as checkUndefinedKeys } from "./operations-BCJd7in3.js";
3
- import "./providers-BAwPjeCy.js";
4
- import "./php-reader-DuZ0Hyl_.js";
5
- import { t as createCommand } from "./_shared-Dv1EW2gy.js";
2
+ import { t as checkUndefinedKeys } from "./operations-CTo44gPu.js";
3
+ import "./providers-CHE2ffi6.js";
4
+ import "./php-reader-3Fgw80zK.js";
5
+ import { t as createCommand } from "./_shared-DFyy3sh8.js";
6
6
  //#region src/commands/check.ts
7
- /**
8
- * CI gate for `check`: any undefined-key finding fails the build.
9
- * Reads summary.undefinedCount, so it works for inline results and for
10
- * the { reportFile, summary } shape alike. Uncertain findings never fail.
11
- */
12
- function hasUndefinedKeys(result) {
13
- if (result === null || typeof result !== "object") return false;
14
- const summary = result.summary;
15
- if (summary === null || typeof summary !== "object") return false;
16
- const count = summary.undefinedCount;
17
- return typeof count === "number" && count > 0;
18
- }
19
7
  var check_default = createCommand({
20
8
  name: "check",
21
- description: "Find keys referenced in code but defined in no consumed locale layer (they render as raw keys); exits non-zero when any are found",
9
+ description: "Find keys referenced in code but defined in no consumed locale layer (they render as raw keys); findings trip an always-on gate and exit 2, distinct from exit 1 for a run that failed",
22
10
  args: {
23
11
  locale: {
24
12
  type: "string",
@@ -33,7 +21,11 @@ var check_default = createCommand({
33
21
  description: "Also write the findings as a GitLab Code Quality (CodeClimate) JSON report to this file path"
34
22
  }
35
23
  },
36
- failWhen: hasUndefinedKeys,
24
+ gates: [{
25
+ name: "undefined-keys",
26
+ counter: "undefinedCount",
27
+ threshold: 0
28
+ }],
37
29
  async run(args) {
38
30
  return checkUndefinedKeys({
39
31
  locale: args.locale,
@@ -46,4 +38,4 @@ var check_default = createCommand({
46
38
  //#endregion
47
39
  export { check_default as default };
48
40
 
49
- //# sourceMappingURL=check-9dcNfHtC.js.map
41
+ //# sourceMappingURL=check-BI1orjXb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-BI1orjXb.js","names":[],"sources":["../src/commands/check.ts"],"sourcesContent":["import { createCommand } from './_shared.js'\nimport { checkUndefinedKeys } from '../core/operations.js'\n\nexport default createCommand({\n name: 'check',\n description: 'Find keys referenced in code but defined in no consumed locale layer (they render as raw keys); findings trip an always-on gate and exit 2, distinct from exit 1 for a run that failed',\n args: {\n locale: { type: 'string', description: 'Reference locale to resolve definitions in (default: project default)' },\n outputFile: { type: 'string', description: 'Write full output to this file path and return only a summary (useful for large outputs)' },\n codequalityOutput: { type: 'string', description: 'Also write the findings as a GitLab Code Quality (CodeClimate) JSON report to this file path' },\n },\n /**\n * Always on, and a gate rather than a run failure. A key that renders raw in\n * production is a defect, so there is no flag to opt into caring about it —\n * but it is still a finding, and reporting it as exit 1 left the caller\n * unable to tell an undefined key from a scan that fell over (#369).\n *\n * Reads summary.undefinedCount, so it trips on inline results and on the\n * { reportFile, summary } shape alike. Uncertain findings never trip it.\n */\n gates: [{ name: 'undefined-keys', counter: 'undefinedCount', threshold: 0 }],\n async run(args) {\n return checkUndefinedKeys({\n locale: args.locale,\n projectDir: args.projectDir,\n outputFile: args.outputFile,\n codequalityOutput: args.codequalityOutput,\n })\n },\n})\n"],"mappings":";;;;;;AAGA,IAAA,gBAAe,cAAc;CAC3B,MAAM;CACN,aAAa;CACb,MAAM;EACJ,QAAQ;GAAE,MAAM;GAAU,aAAa;GAAyE;EAChH,YAAY;GAAE,MAAM;GAAU,aAAa;GAA4F;EACvI,mBAAmB;GAAE,MAAM;GAAU,aAAa;GAAgG;EACnJ;CAUD,OAAO,CAAC;EAAE,MAAM;EAAkB,SAAS;EAAkB,WAAW;EAAG,CAAC;CAC5E,MAAM,IAAI,MAAM;AACd,SAAO,mBAAmB;GACxB,QAAQ,KAAK;GACb,YAAY,KAAK;GACjB,YAAY,KAAK;GACjB,mBAAmB,KAAK;GACzB,CAAC;;CAEL,CAAC"}