@the-i18n-kit/cli 4.10.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.
- package/README.md +29 -296
- package/dist/{_shared-Dn15c_g9.js → _shared-DFyy3sh8.js} +24 -12
- package/dist/_shared-DFyy3sh8.js.map +1 -0
- package/dist/{add-Dj6hsdl9.js → add-C-qs8iBa.js} +5 -5
- package/dist/{add-Dj6hsdl9.js.map → add-C-qs8iBa.js.map} +1 -1
- package/dist/bin.js +1 -1
- package/dist/{check-BI-CGxpY.js → check-BI1orjXb.js} +11 -19
- package/dist/check-BI1orjXb.js.map +1 -0
- package/dist/cli-DsFuVJNP.js +74 -0
- package/dist/cli-DsFuVJNP.js.map +1 -0
- package/dist/config/framework/stubs/{next-intl-routing-Se7Y8cCb.d.ts → next-intl-routing-Cwaa3f4R.d.ts} +1 -1
- package/dist/config/framework/stubs/{next-intl-routing-Se7Y8cCb.d.ts.map → next-intl-routing-Cwaa3f4R.d.ts.map} +1 -1
- package/dist/config/framework/stubs/{unplugin-vue-i18n-DrR5pbEW.d.ts → unplugin-vue-i18n-1ud7-Ly8.d.ts} +1 -1
- package/dist/config/framework/stubs/{unplugin-vue-i18n-DrR5pbEW.d.ts.map → unplugin-vue-i18n-1ud7-Ly8.d.ts.map} +1 -1
- package/dist/define-config-4ubtfvUy.js.map +1 -1
- package/dist/{define-config-DpbZ4BZ6.d.ts → define-config-BaZ1dZph.d.ts} +11 -4
- package/dist/define-config-BaZ1dZph.d.ts.map +1 -0
- package/dist/{define-config-BSsvRnWR.d.ts → define-config-CLgKN-7N.d.ts} +1 -1
- package/dist/define-config.d.ts +1 -1
- package/dist/{detect-BmvyRD1a.js → detect-7cVfsNJP.js} +5 -5
- package/dist/{detect-BmvyRD1a.js.map → detect-7cVfsNJP.js.map} +1 -1
- package/dist/{empty-Ww6FnmgY.js → empty-DB8Hl_8u.js} +5 -5
- package/dist/{empty-Ww6FnmgY.js.map → empty-DB8Hl_8u.js.map} +1 -1
- package/dist/find-duplicates-CmEqBfby.js +42 -0
- package/dist/find-duplicates-CmEqBfby.js.map +1 -0
- package/dist/{get-oF_xIbVR.js → get-BE21Iy6N.js} +5 -5
- package/dist/{get-oF_xIbVR.js.map → get-BE21Iy6N.js.map} +1 -1
- package/dist/{index-JeU02vfH.d.ts → index-DCG3dOdC.d.ts} +332 -3
- package/dist/index-DCG3dOdC.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -4
- package/dist/{init-D4B1_DXb.js → init-BtzmhsOM.js} +5 -5
- package/dist/{init-D4B1_DXb.js.map → init-BtzmhsOM.js.map} +1 -1
- package/dist/{list-dirs-CgXh-Lg1.js → list-dirs-DUQbwvlE.js} +5 -5
- package/dist/{list-dirs-CgXh-Lg1.js.map → list-dirs-DUQbwvlE.js.map} +1 -1
- package/dist/{missing-BZT6PgW5.js → missing-CcYrgd54.js} +5 -5
- package/dist/{missing-BZT6PgW5.js.map → missing-CcYrgd54.js.map} +1 -1
- package/dist/move-B2px6Ck-.js +50 -0
- package/dist/move-B2px6Ck-.js.map +1 -0
- package/dist/{operations-BCJd7in3.js → operations-CTo44gPu.js} +1332 -241
- package/dist/operations-CTo44gPu.js.map +1 -0
- package/dist/php-reader-3Fgw80zK.js +64 -0
- package/dist/php-reader-3Fgw80zK.js.map +1 -0
- package/dist/php-reader-CpnaPSpZ.js +2 -0
- package/dist/{providers-BAwPjeCy.js → providers-CHE2ffi6.js} +53 -27
- package/dist/providers-CHE2ffi6.js.map +1 -0
- package/dist/{remove-B7m-hi2O.js → remove-DTbqM4bR.js} +5 -5
- package/dist/{remove-B7m-hi2O.js.map → remove-DTbqM4bR.js.map} +1 -1
- package/dist/{remove-orphans-DOTWE7Ji.js → remove-orphans-Dfzu7hRz.js} +5 -5
- package/dist/{remove-orphans-DOTWE7Ji.js.map → remove-orphans-Dfzu7hRz.js.map} +1 -1
- package/dist/{rename-DEJWj5_P.js → rename-C_rFKkU4.js} +5 -5
- package/dist/{rename-DEJWj5_P.js.map → rename-C_rFKkU4.js.map} +1 -1
- package/dist/{scaffold-BH_OX-5z.js → scaffold-CUimlNqR.js} +5 -5
- package/dist/{scaffold-BH_OX-5z.js.map → scaffold-CUimlNqR.js.map} +1 -1
- package/dist/{scan-LeU4xG0G.js → scan-BkOIYhjZ.js} +5 -5
- package/dist/{scan-LeU4xG0G.js.map → scan-BkOIYhjZ.js.map} +1 -1
- package/dist/{search-CHdDywqc.js → search-0io1BcsY.js} +5 -5
- package/dist/{search-CHdDywqc.js.map → search-0io1BcsY.js.map} +1 -1
- package/dist/{status-CwI94d_R.js → status-Bp-SzjTN.js} +5 -5
- package/dist/{status-CwI94d_R.js.map → status-Bp-SzjTN.js.map} +1 -1
- package/dist/{translate-Cf3npNpr.js → translate-cCb2Z57w.js} +6 -6
- package/dist/{translate-Cf3npNpr.js.map → translate-cCb2Z57w.js.map} +1 -1
- package/dist/{translate-key-Bg_8B6c_.js → translate-key-DHLWEmrA.js} +5 -5
- package/dist/{translate-key-Bg_8B6c_.js.map → translate-key-DHLWEmrA.js.map} +1 -1
- package/dist/{update-DNv-dU1U.js → update-DV4ijRVB.js} +5 -5
- package/dist/{update-DNv-dU1U.js.map → update-DV4ijRVB.js.map} +1 -1
- package/dist/{write-DUwMSc75.js → write-Czzm_Nb0.js} +5 -5
- package/dist/{write-DUwMSc75.js.map → write-Czzm_Nb0.js.map} +1 -1
- package/package.json +15 -5
- package/dist/_shared-Dn15c_g9.js.map +0 -1
- package/dist/check-BI-CGxpY.js.map +0 -1
- package/dist/cli-CDhNbaUM.js +0 -82
- package/dist/cli-CDhNbaUM.js.map +0 -1
- package/dist/define-config-DpbZ4BZ6.d.ts.map +0 -1
- package/dist/find-duplicates-BfBBqP7I.js +0 -31
- package/dist/find-duplicates-BfBBqP7I.js.map +0 -1
- package/dist/index-JeU02vfH.d.ts.map +0 -1
- package/dist/operations-BCJd7in3.js.map +0 -1
- package/dist/php-reader-DcgOAhTw.js +0 -2
- package/dist/php-reader-DuZ0Hyl_.js +0 -32
- package/dist/php-reader-DuZ0Hyl_.js.map +0 -1
- package/dist/providers-BAwPjeCy.js.map +0 -1
- /package/dist/{bin-CF8fSc-R.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
3
|
[](https://npmjs.com/package/@the-i18n-kit/cli)
|
|
4
4
|
[](https://github.com/fabkho/the-i18n-kit/blob/main/LICENSE)
|
|
5
5
|
|
|
6
|
-
|
|
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
|
-
|
|
10
|
+
Part of [the-i18n-kit](https://github.com/fabkho/the-i18n-kit).
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
### 📖 [Documentation](https://fabkho.github.io/the-i18n-kit/)
|
|
11
13
|
|
|
12
14
|
## Install
|
|
13
15
|
|
|
14
16
|
```bash
|
|
15
|
-
# Global install
|
|
16
17
|
npm install -g @the-i18n-kit/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
|
-
}
|
|
109
18
|
```
|
|
110
19
|
|
|
111
|
-
|
|
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
|
-
|
|
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
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
36
|
+
## Documentation
|
|
208
37
|
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
-
|
|
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-
|
|
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
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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-
|
|
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-
|
|
3
|
-
import "./providers-
|
|
4
|
-
import "./php-reader-
|
|
5
|
-
import { r as parseJsonArg, t as createCommand } from "./_shared-
|
|
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-
|
|
40
|
+
//# sourceMappingURL=add-C-qs8iBa.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-
|
|
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
|
@@ -8,7 +8,7 @@ const { createRequire } = await import("node:module");
|
|
|
8
8
|
const { name } = createRequire(import.meta.url)("../package.json");
|
|
9
9
|
const notice = renameNotice(name);
|
|
10
10
|
if (notice) process.stderr.write(`[the-i18n-cli] ${notice}\n`);
|
|
11
|
-
const { runCli } = await import("./cli-
|
|
11
|
+
const { runCli } = await import("./cli-DsFuVJNP.js");
|
|
12
12
|
await runCli();
|
|
13
13
|
//#endregion
|
|
14
14
|
export {};
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import "./stdout-guard-sJHFWVem.js";
|
|
2
|
-
import { t as checkUndefinedKeys } from "./operations-
|
|
3
|
-
import "./providers-
|
|
4
|
-
import "./php-reader-
|
|
5
|
-
import { t as createCommand } from "./_shared-
|
|
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);
|
|
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
|
-
|
|
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-
|
|
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"}
|