@the-i18n-kit/cli 6.0.0 → 7.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 (91) hide show
  1. package/README.md +7 -57
  2. package/dist/bin.js +1 -1
  3. package/dist/{_shared-CkMAydRl.js → cli-waEjbUM1.js} +157 -27
  4. package/dist/cli-waEjbUM1.js.map +1 -0
  5. package/dist/config/framework/stubs/{next-intl-routing-lExp4Q3r.d.ts → next-intl-routing-i6Mlxwdt.d.ts} +1 -1
  6. package/dist/config/framework/stubs/{next-intl-routing-lExp4Q3r.d.ts.map → next-intl-routing-i6Mlxwdt.d.ts.map} +1 -1
  7. package/dist/{define-config-DW-rgsU6.d.ts → define-config-ChY3jk6K.d.ts} +26 -4
  8. package/dist/define-config-ChY3jk6K.d.ts.map +1 -0
  9. package/dist/{define-config-BeHvSYBP.d.ts → define-config-e0B0VHq7.d.ts} +1 -1
  10. package/dist/define-config.d.ts +1 -1
  11. package/dist/descriptors-10sgyqEs.js +1225 -0
  12. package/dist/descriptors-10sgyqEs.js.map +1 -0
  13. package/dist/detector-BJTkyhQ0.js +5 -0
  14. package/dist/detector-DtFY4qm3.js +2189 -0
  15. package/dist/detector-DtFY4qm3.js.map +1 -0
  16. package/dist/{index-w-EQZEZF.d.ts → index-CdjJ71Ag.d.ts} +665 -231
  17. package/dist/index-CdjJ71Ag.d.ts.map +1 -0
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +8 -4
  20. package/dist/json-writer-D0b6vEax.js +2 -0
  21. package/dist/json-writer-ek8yEI5R.js +342 -0
  22. package/dist/json-writer-ek8yEI5R.js.map +1 -0
  23. package/dist/{operations-PfRSTepi.js → operations-CNqEBD__.js} +1504 -3284
  24. package/dist/operations-CNqEBD__.js.map +1 -0
  25. package/dist/operations-D7IMu_xY.js +8 -0
  26. package/dist/php-reader-BGx6Ii2d.js +2 -0
  27. package/dist/{php-reader-3Fgw80zK.js → php-reader-DRpcRVzv.js} +6 -3
  28. package/dist/{php-reader-3Fgw80zK.js.map → php-reader-DRpcRVzv.js.map} +1 -1
  29. package/dist/{providers-CAsU_aV1.js → project-config-C3ao4Uii.js} +19 -208
  30. package/dist/project-config-C3ao4Uii.js.map +1 -0
  31. package/dist/providers-BbVPelvp.js +406 -0
  32. package/dist/providers-BbVPelvp.js.map +1 -0
  33. package/dist/report-By1-5JtO.js +37 -0
  34. package/dist/report-By1-5JtO.js.map +1 -0
  35. package/dist/report-CHZgH9Wy.js +2 -0
  36. package/package.json +4 -16
  37. package/dist/_shared-CkMAydRl.js.map +0 -1
  38. package/dist/add-BBoVUNEq.js +0 -40
  39. package/dist/add-BBoVUNEq.js.map +0 -1
  40. package/dist/check-IosaN_EM.js +0 -41
  41. package/dist/check-IosaN_EM.js.map +0 -1
  42. package/dist/cli-CnZNGUGu.js +0 -70
  43. package/dist/cli-CnZNGUGu.js.map +0 -1
  44. package/dist/config/framework/stubs/unplugin-vue-i18n-mH7YdYqA.d.ts +0 -25
  45. package/dist/config/framework/stubs/unplugin-vue-i18n-mH7YdYqA.d.ts.map +0 -1
  46. package/dist/config/framework/stubs/unplugin-vue-i18n.js +0 -26
  47. package/dist/config/framework/stubs/unplugin-vue-i18n.js.map +0 -1
  48. package/dist/define-config-DW-rgsU6.d.ts.map +0 -1
  49. package/dist/detect-3jjcQJs1.js +0 -17
  50. package/dist/detect-3jjcQJs1.js.map +0 -1
  51. package/dist/empty-D3La0enO.js +0 -36
  52. package/dist/empty-D3La0enO.js.map +0 -1
  53. package/dist/find-duplicates-BlQoJDgu.js +0 -42
  54. package/dist/find-duplicates-BlQoJDgu.js.map +0 -1
  55. package/dist/get-Cr5O5zJX.js +0 -41
  56. package/dist/get-Cr5O5zJX.js.map +0 -1
  57. package/dist/index-w-EQZEZF.d.ts.map +0 -1
  58. package/dist/init-BRBMkcI0.js +0 -33
  59. package/dist/init-BRBMkcI0.js.map +0 -1
  60. package/dist/list-dirs-BMuByyuX.js +0 -17
  61. package/dist/list-dirs-BMuByyuX.js.map +0 -1
  62. package/dist/missing-CBOk4ZgD.js +0 -51
  63. package/dist/missing-CBOk4ZgD.js.map +0 -1
  64. package/dist/move-DOdPKnOV.js +0 -50
  65. package/dist/move-DOdPKnOV.js.map +0 -1
  66. package/dist/operations-PfRSTepi.js.map +0 -1
  67. package/dist/php-reader-CpnaPSpZ.js +0 -2
  68. package/dist/providers-CAsU_aV1.js.map +0 -1
  69. package/dist/remove-CVBjJV9h.js +0 -41
  70. package/dist/remove-CVBjJV9h.js.map +0 -1
  71. package/dist/remove-orphans-BaKEISxA.js +0 -57
  72. package/dist/remove-orphans-BaKEISxA.js.map +0 -1
  73. package/dist/rename-CN2ajUeb.js +0 -45
  74. package/dist/rename-CN2ajUeb.js.map +0 -1
  75. package/dist/scaffold-CUKUFuEy.js +0 -39
  76. package/dist/scaffold-CUKUFuEy.js.map +0 -1
  77. package/dist/scan-Bhv0Nmy3.js +0 -31
  78. package/dist/scan-Bhv0Nmy3.js.map +0 -1
  79. package/dist/search-CrkLgz3m.js +0 -49
  80. package/dist/search-CrkLgz3m.js.map +0 -1
  81. package/dist/status-DprS-qDC.js +0 -45
  82. package/dist/status-DprS-qDC.js.map +0 -1
  83. package/dist/translate-Cs6EdbzA.js +0 -92
  84. package/dist/translate-Cs6EdbzA.js.map +0 -1
  85. package/dist/translate-key-BBZpYQjL.js +0 -73
  86. package/dist/translate-key-BBZpYQjL.js.map +0 -1
  87. package/dist/update-BQuGxSSy.js +0 -40
  88. package/dist/update-BQuGxSSy.js.map +0 -1
  89. package/dist/write-B8z6I5Vf.js +0 -47
  90. package/dist/write-B8z6I5Vf.js.map +0 -1
  91. /package/dist/{bin-g05vSfAz.d.ts → bin-NyzIHE2F.d.ts} +0 -0
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Find missing translation keys, remove dead ones, and rename across every locale
7
7
  and layer at once. Supports Nuxt, Laravel, Vue, React/Next.js, and any project
8
- with JSON or PHP locale files.
8
+ with JSON, YAML or PHP locale files.
9
9
 
10
10
  Part of [the-i18n-kit](https://github.com/fabkho/the-i18n-kit).
11
11
 
@@ -26,7 +26,7 @@ the-i18n-cli init # write a config from what it detects
26
26
  the-i18n-cli status # coverage per locale and per layer
27
27
  the-i18n-cli missing # what is not translated yet
28
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)
29
+ the-i18n-cli orphans # keys defined but unused (reports; --remove deletes)
30
30
  ```
31
31
 
32
32
  → [Cold start guide](https://fabkho.github.io/the-i18n-kit/getting-started/cold-start) ·
@@ -38,64 +38,14 @@ the-i18n-cli remove-orphans # keys defined but unused (previews by defa
38
38
  | | |
39
39
  |---|---|
40
40
  | [Commands](https://fabkho.github.io/the-i18n-kit/reference/cli) | Generated from the command definitions |
41
+ | [Agent contract](https://fabkho.github.io/the-i18n-kit/getting-started/agent-contract) | Exit codes, gates, failure reasons, output diversion, env vars |
41
42
  | [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 |
43
+ | [Frameworks](https://fabkho.github.io/the-i18n-kit/frameworks/detection) | What each adapter reads, and how to pin one |
44
+ | [Monorepos and layers](https://fabkho.github.io/the-i18n-kit/monorepos/layers-and-consumer-graph) | Why usage in one app does not protect a key in another |
45
+ | [Orphan detection](https://fabkho.github.io/the-i18n-kit/monorepos/orphan-detection) | What the scanner sees, its blind spots, and the call-site style that keeps a report specific |
46
+ | [Translation modes](https://fabkho.github.io/the-i18n-kit/concepts/translation-modes) | Provider and agent mode, and what is validated before writing |
45
47
  | [CI/CD](https://fabkho.github.io/the-i18n-kit/ci-cd/github-actions) | The Action and the GitLab template |
46
48
 
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 |
48
-
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).
52
-
53
-
54
- ## How Orphan Detection Works
55
-
56
- `remove-orphans` and `check` share a line-based static scanner. Knowing exactly what it can and cannot see is essential before deleting keys.
57
-
58
- **Usage evidence the scanner recognizes:**
59
-
60
- | Class | Example | Effect |
61
- |---|---|---|
62
- | Static keys | `t('a.b.c')`, `$te('a.b')`, `__('a.b')` | exact match |
63
- | Template patterns | `` t(`a.b.${x}`) `` | keys matching `a.b.<one segment>` count as used (`dynamic-matched`) |
64
- | Same-file const prefixes | `const base = 'a.b'` + `` t(`${base}.title`) `` | resolved to the exact key |
65
- | Unresolved variable segments | `` t(`${somePath}.title`) `` | conservatively matches **any** `*.title` key |
66
- | Concat prefixes | `'a.b.' + x`, `x + '.label'` | pattern-matched like templates (single-segment prefixes included) |
67
- | Multiline calls | prefix on a different line than `t(` | caught by bare-string fallbacks (heuristic) |
68
- | Multi-app scoping | key in a shared layer | usage counts only from apps that consume the layer; cross-app usages are reported as `misplacedUsages`, never removed |
69
-
70
- **The scan never removes a key it is unsure about.** Dynamic references are detected, not ignored: a key that *could* be produced by a template pattern, a concatenated prefix or an ambiguous probe is classified as used and left alone. Deletion is reserved for keys with no evidence of use anywhere in a consuming app. On a real 8,000-key project roughly 12% of keys land in the protective buckets — that is the scan working, not failing.
71
-
72
- **Classification buckets** — only `orphanKeys` is ever removed; everything else is protective:
73
-
74
- - `orphanKeys` — no evidence of use in any consuming app: safe to remove
75
- - `dynamic-matched` — a dynamic pattern could produce this key: counted as used
76
- - `uncertainKeys` — evidence is ambiguous (e.g. `$te`-only probes): never removed
77
- - `ignored` — matched by `orphanScan.ignorePatterns`: never scanned
78
- - `misplacedUsages` — used only from non-consuming apps: never removed
79
-
80
- **Known blind spots** (declare these families in `orphanScan.ignorePatterns`):
81
-
82
- - Prefixes stored in **cross-file** constants, class fields, or object properties typed as plain strings (`obj.translationPath`) — the scanner widens these to conservative suffix patterns, but treat such families as declared-dynamic
83
- - Keys composed at runtime from data (API responses, database values, enums built dynamically)
84
- - Keys referenced only outside the scanned source (backend responses, external configs, docs)
85
-
86
- **Recommended code style — anchor dynamic keys, don't avoid them.** Dynamic keys are tracked and are often the right design; what matters is that the *namespace* stays literal at the call site:
87
-
88
- ```ts
89
- t(`${prefix}.title`) // widens to any key ending .title
90
- t(`components.integrations.${type}.title`) // widens to one segment under a known namespace
91
- ```
92
-
93
- Both are counted as used, but the first suppresses every `.title` key in the project — on a large catalog that can be hundreds of keys the scan can no longer audit. Keeping a literal leading segment costs nothing and keeps the report meaningful. Literal key maps (`const KEYS = { draft: 'x.status.draft' } as const`) are the fully-static alternative where the set is closed.
94
-
95
- Prefixes assembled in another scope defeat this even when they are literal — a `computed` returning `'a.b.' + x` reaches the call site as an opaque variable. Inline the namespace instead of the whole key.
96
-
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.
98
-
99
49
  ## License
100
50
 
101
51
  [MIT](https://github.com/fabkho/the-i18n-kit/blob/main/LICENSE)
package/dist/bin.js CHANGED
@@ -3,7 +3,7 @@ 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-CnZNGUGu.js");
6
+ const { runCli } = await import("./cli-waEjbUM1.js");
7
7
  await runCli();
8
8
  //#endregion
9
9
  export {};
@@ -1,9 +1,19 @@
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-CAsU_aV1.js";
4
- import { defineCommand } from "citty";
3
+ import { a as resolveProviderBaseUrl, i as createTranslateFn, t as BASE_URL_ENV } from "./providers-BbVPelvp.js";
4
+ import { i as log, n as loadProjectConfig } from "./project-config-C3ao4Uii.js";
5
+ import { i as divertToReport, t as descriptors } from "./descriptors-10sgyqEs.js";
6
+ import { createRequire } from "node:module";
7
+ import { defineCommand, runCommand, runMain } from "citty";
5
8
  //#region src/commands/_shared.ts
6
- /** Factory for commands that call an operation and output its result. */
9
+ /**
10
+ * Factory for commands that call an operation and output its result.
11
+ *
12
+ * Reached through `commandFromDescriptor`, which is the only production caller:
13
+ * it stays a separate function because output handling, gate evaluation and
14
+ * exit codes are worth testing against a fixed result rather than against a
15
+ * project on disk.
16
+ */
7
17
  function createCommand(opts) {
8
18
  return withGates(defineCommand({
9
19
  meta: {
@@ -18,7 +28,7 @@ function createCommand(opts) {
18
28
  try {
19
29
  const result = await opts.run(args);
20
30
  const decision = resolveExitCode(result, requestedGates(opts.gates ?? [], args), isTotalFailure(result));
21
- outputResult(withGateReport(result, decision.tripped), args);
31
+ outputResult(withGateReport(opts.divert === void 0 ? result : await opts.divert(result, args), decision.tripped), args);
22
32
  if (decision.code !== 0) process.exitCode = decision.code;
23
33
  } catch (error) {
24
34
  emitErrorResult(error, args);
@@ -56,8 +66,9 @@ function resolveExitCode(result, gates, runFailed = false) {
56
66
  };
57
67
  }
58
68
  /**
59
- * Read a gate's counter off result.summary. Works on inline results and on
60
- * the { reportFile, summary } shape alike, since both carry the summary.
69
+ * Read a gate's counter off result.summary. Gates are evaluated before a result
70
+ * is diverted to a file, so this sees the whole result and it still reads the
71
+ * { reportFile, summary } shape, which carries the same summary.
61
72
  * A missing or non-numeric counter yields undefined and never trips a gate.
62
73
  */
63
74
  function observedValue(result, counter) {
@@ -182,26 +193,104 @@ function errorCode(error) {
182
193
  }
183
194
  return "UNKNOWN_ERROR";
184
195
  }
185
- /** Provider selection flags shared by the translate commands. */
186
- const providerArgs = {
187
- provider: {
188
- type: "string",
189
- description: "LLM provider: \"openai\", \"anthropic\", or \"google\". Required for automatic translation.",
190
- valueHint: "openai|anthropic|google"
191
- },
192
- model: {
193
- type: "string",
194
- description: "Model name (required when --provider is set)"
195
- },
196
- apiKey: {
197
- type: "string",
198
- description: "API key (falls back to OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY env)."
199
- },
200
- baseUrl: {
201
- type: "string",
202
- 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".`
196
+ /**
197
+ * The citty command for one descriptor: its flags, its gates, and a run that
198
+ * hands the operation arguments of the types the descriptor declared.
199
+ *
200
+ * Everything a command needs is read off the descriptor, so a command is not a
201
+ * file anyone writes — which is what keeps it from drifting from the tool that
202
+ * runs the same operation.
203
+ */
204
+ function commandFromDescriptor(descriptor) {
205
+ const cli = descriptor.cli;
206
+ if (cli === null) throw new Error(`Operation "${descriptor.id}" declares no CLI command.`);
207
+ return createCommand({
208
+ name: cli.name,
209
+ description: descriptor.description,
210
+ args: cliArgs(descriptor.params),
211
+ gates: descriptor.gates,
212
+ run: async (args) => descriptor.run(operationArgs(descriptor.params, args), {
213
+ surface: "cli",
214
+ translateFn: descriptor.usesTranslateFn === true ? await resolveProviderTranslateFn(args) : void 0
215
+ }),
216
+ divert: async (result, args) => divertToReport(result, descriptor, operationArgs(descriptor.params, args))
217
+ });
218
+ }
219
+ /** The citty `args` for a descriptor's parameters, skipping the CLI-hidden ones. */
220
+ function cliArgs(params) {
221
+ const args = {};
222
+ for (const [name, spec] of Object.entries(params)) {
223
+ if (spec.cli?.hidden === true) continue;
224
+ args[name] = {
225
+ type: spec.type === "boolean" ? "boolean" : "string",
226
+ description: flagDescription(spec),
227
+ ...spec.required === true ? { required: true } : {},
228
+ ...spec.default === void 0 ? {} : { default: spec.default },
229
+ ...spec.cli?.alias === void 0 ? {} : { alias: [...toArray(spec.cli.alias)] },
230
+ ...spec.enum === void 0 ? {} : { valueHint: spec.enum.join("|") }
231
+ };
203
232
  }
204
- };
233
+ return args;
234
+ }
235
+ /**
236
+ * The description a flag prints. The declared one is written for a reader of
237
+ * either surface, so how to spell a list or an object at a shell prompt is
238
+ * added here rather than in every spec.
239
+ */
240
+ function flagDescription(spec) {
241
+ if (spec.type === "string[]") return `${spec.description} Comma-separated${spec.allowAll === true ? ", or \"all\"" : ""}.`;
242
+ if (spec.type === "record") return `${spec.description} Pass it as JSON.`;
243
+ return spec.description;
244
+ }
245
+ /**
246
+ * citty's parsed args as the operation takes them: lists split, numbers and
247
+ * JSON parsed, enums checked. `projectDir` comes along because every operation
248
+ * accepts it without declaring it.
249
+ */
250
+ function operationArgs(params, args) {
251
+ const operation = { projectDir: args.projectDir };
252
+ for (const [name, spec] of Object.entries(params)) {
253
+ if (spec.cli?.hidden === true) continue;
254
+ const value = coerce(name, spec, args[name]);
255
+ if (value !== void 0) operation[name] = value;
256
+ }
257
+ return operation;
258
+ }
259
+ function coerce(name, spec, raw) {
260
+ if (spec.type === "boolean") return typeof raw === "boolean" ? raw : void 0;
261
+ const value = typeof raw === "string" ? raw : void 0;
262
+ switch (spec.type) {
263
+ case "number": return value === void 0 || value === "" ? void 0 : toNumber(name, spec, value);
264
+ case "string[]": return toList(name, spec, value);
265
+ case "record": return value === void 0 ? void 0 : parseJsonArg(value, name);
266
+ default: return toEnumChecked(name, spec, value);
267
+ }
268
+ }
269
+ function toEnumChecked(name, spec, value) {
270
+ if (spec.enum === void 0) return value;
271
+ if (value === void 0 || value === "") return void 0;
272
+ if (!spec.enum.includes(value)) throw new Error(`Invalid --${name} value: "${value}". Must be one of: ${spec.enum.join(", ")}`);
273
+ return value;
274
+ }
275
+ function toNumber(name, spec, raw) {
276
+ const value = Number(raw);
277
+ if (!(Number.isFinite(value) && (spec.integer !== true || Number.isInteger(value) && String(value) === raw.trim()) && (spec.min === void 0 || value >= spec.min))) throw new Error(`Invalid --${name}: "${raw}". Must be ${numberRequirement(spec)}`);
278
+ return value;
279
+ }
280
+ function numberRequirement(spec) {
281
+ if (spec.integer !== true) return spec.min === void 0 ? "a number" : `a number of at least ${spec.min}`;
282
+ if (spec.min === 1) return "a positive integer";
283
+ return spec.min === void 0 ? "an integer" : `an integer of at least ${spec.min}`;
284
+ }
285
+ function toList(name, spec, raw) {
286
+ if (spec.allowAll === true && raw === "all") return "all";
287
+ const list = splitList(raw);
288
+ if ((list === void 0 || list.length === 0) && (spec.required === true || raw !== void 0 && raw !== "")) throw new Error(`No ${name} provided. Pass a comma-separated list via --${name}.`);
289
+ return list;
290
+ }
291
+ function toArray(value) {
292
+ return typeof value === "string" ? [value] : value;
293
+ }
205
294
  /**
206
295
  * Build a TranslateFn from the provider flags, or undefined when no provider
207
296
  * was given (agent mode). The base URL resolves flag > env > project config;
@@ -255,6 +344,47 @@ function parseJsonArg(value, argName) {
255
344
  }
256
345
  }
257
346
  //#endregion
258
- export { resolveProviderTranslateFn as a, providerArgs as i, emitErrorResult as n, splitList as o, parseJsonArg as r, createCommand as t };
347
+ //#region src/commands/index.ts
348
+ function entry(build) {
349
+ let built;
350
+ return { load: async () => built ??= build() };
351
+ }
352
+ function registry() {
353
+ const commands = {};
354
+ for (const descriptor of descriptors) {
355
+ const cli = descriptor.cli;
356
+ if (cli === null) continue;
357
+ commands[cli.name] = entry(() => commandFromDescriptor(descriptor));
358
+ }
359
+ return commands;
360
+ }
361
+ const commands$1 = registry();
362
+ //#endregion
363
+ //#region src/cli.ts
364
+ const commands = Object.fromEntries(Object.entries(commands$1).map(([name, entry]) => [name, entry.load]));
365
+ const { version, description } = createRequire(import.meta.url)("../package.json");
366
+ const main = defineCommand({
367
+ meta: {
368
+ name: "the-i18n-cli",
369
+ version,
370
+ description
371
+ },
372
+ subCommands: commands
373
+ });
374
+ async function runCli() {
375
+ const rawArgs = process.argv.slice(2);
376
+ if (rawArgs.includes("--help") || rawArgs.includes("-h") || rawArgs.includes("--version") || rawArgs.includes("-v")) {
377
+ await runMain(main);
378
+ return;
379
+ }
380
+ try {
381
+ await runCommand(main, { rawArgs });
382
+ } catch (error) {
383
+ emitErrorResult(error, { json: rawArgs.includes("--json") });
384
+ process.exitCode = 1;
385
+ }
386
+ }
387
+ //#endregion
388
+ export { runCli };
259
389
 
260
- //# sourceMappingURL=_shared-CkMAydRl.js.map
390
+ //# sourceMappingURL=cli-waEjbUM1.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-waEjbUM1.js","names":["commands","allCommands"],"sources":["../src/commands/_shared.ts","../src/commands/index.ts","../src/cli.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'\nimport { divertToReport } from '../surface/report.js'\nimport type {\n AnyOperationDescriptor,\n FlaggedGateSpec,\n GateSpec,\n ParamSpec,\n Params,\n} from '../surface/types.js'\n\n// Declared with the descriptors, since a gate is something an operation\n// declares; re-exported here because this is where they are evaluated and\n// where the reference generator reads the exit codes from.\nexport type { GateSpec, FlaggedGateSpec, AlwaysOnGateSpec } from '../surface/types.js'\n\n/**\n * Factory for commands that call an operation and output its result.\n *\n * Reached through `commandFromDescriptor`, which is the only production caller:\n * it stays a separate function because output handling, gate evaluation and\n * exit codes are worth testing against a fixed result rather than against a\n * project on disk.\n */\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: the `args` above are\n * built at runtime from a descriptor's parameters and citty does not thread\n * that type through to the handler. The typed view of these arguments is the\n * descriptor's own `run`, which `commandFromDescriptor` calls with values\n * already coerced to the declared types.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- see above\n run: (args: any) => Promise<unknown>\n /**\n * Applied to the result after the gates have been evaluated. That order is\n * the point: a run that writes its result to a file still exits on the\n * counters that result carried, so `--output-file` and a gate flag combine\n * the way a pipeline expects.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- as above\n divert?: (result: unknown, 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 const output = opts.divert === undefined ? result : await opts.divert(result, args)\n outputResult(withGateReport(output, 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/** 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. Gates are evaluated before a result\n * is diverted to a file, so this sees the whole result — and it still reads the\n * { reportFile, summary } shape, which carries the same 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/**\n * The citty command for one descriptor: its flags, its gates, and a run that\n * hands the operation arguments of the types the descriptor declared.\n *\n * Everything a command needs is read off the descriptor, so a command is not a\n * file anyone writes — which is what keeps it from drifting from the tool that\n * runs the same operation.\n */\nexport function commandFromDescriptor(descriptor: AnyOperationDescriptor): CommandDef {\n const cli = descriptor.cli\n if (cli === null) {\n throw new Error(`Operation \"${descriptor.id}\" declares no CLI command.`)\n }\n\n return createCommand({\n name: cli.name,\n description: descriptor.description,\n args: cliArgs(descriptor.params),\n gates: descriptor.gates,\n run: async args => descriptor.run(operationArgs(descriptor.params, args), {\n surface: 'cli',\n // Resolved from the provider flags rather than passed through: which\n // flags select a backend is the CLI's business, not the operation's.\n translateFn: descriptor.usesTranslateFn === true\n ? await resolveProviderTranslateFn(args)\n : undefined,\n }),\n divert: async (result, args) =>\n divertToReport(result, descriptor, operationArgs(descriptor.params, args)),\n })\n}\n\n/** The citty `args` for a descriptor's parameters, skipping the CLI-hidden ones. */\nfunction cliArgs(params: Params): Record<string, unknown> {\n const args: Record<string, unknown> = {}\n for (const [name, spec] of Object.entries(params)) {\n if (spec.cli?.hidden === true) continue\n args[name] = {\n // citty parses booleans and strings. A list, a number and a JSON object\n // all arrive as one string and are converted in operationArgs.\n type: spec.type === 'boolean' ? 'boolean' : 'string',\n description: flagDescription(spec),\n ...(spec.required === true ? { required: true } : {}),\n ...(spec.default === undefined ? {} : { default: spec.default }),\n ...(spec.cli?.alias === undefined ? {} : { alias: [...toArray(spec.cli.alias)] }),\n ...(spec.enum === undefined ? {} : { valueHint: spec.enum.join('|') }),\n }\n }\n return args\n}\n\n/**\n * The description a flag prints. The declared one is written for a reader of\n * either surface, so how to spell a list or an object at a shell prompt is\n * added here rather than in every spec.\n */\nfunction flagDescription(spec: ParamSpec): string {\n if (spec.type === 'string[]') {\n return `${spec.description} Comma-separated${spec.allowAll === true ? ', or \"all\"' : ''}.`\n }\n if (spec.type === 'record') return `${spec.description} Pass it as JSON.`\n return spec.description\n}\n\n/**\n * citty's parsed args as the operation takes them: lists split, numbers and\n * JSON parsed, enums checked. `projectDir` comes along because every operation\n * accepts it without declaring it.\n */\nfunction operationArgs(params: Params, args: Record<string, unknown>): Record<string, unknown> {\n const operation: Record<string, unknown> = { projectDir: args.projectDir }\n for (const [name, spec] of Object.entries(params)) {\n if (spec.cli?.hidden === true) continue\n const value = coerce(name, spec, args[name])\n if (value !== undefined) operation[name] = value\n }\n return operation\n}\n\nfunction coerce(name: string, spec: ParamSpec, raw: unknown): unknown {\n if (spec.type === 'boolean') return typeof raw === 'boolean' ? raw : undefined\n\n const value = typeof raw === 'string' ? raw : undefined\n\n switch (spec.type) {\n case 'number':\n return value === undefined || value === '' ? undefined : toNumber(name, spec, value)\n case 'string[]':\n return toList(name, spec, value)\n case 'record':\n return value === undefined ? undefined : parseJsonArg(value, name)\n default:\n return toEnumChecked(name, spec, value)\n }\n}\n\nfunction toEnumChecked(name: string, spec: ParamSpec, value: string | undefined): string | undefined {\n if (spec.enum === undefined) return value\n // An empty value is an unset flag, not a value to reject: a shell produces\n // one routinely from an unset variable.\n if (value === undefined || value === '') return undefined\n if (!spec.enum.includes(value)) {\n throw new Error(`Invalid --${name} value: \"${value}\". Must be one of: ${spec.enum.join(', ')}`)\n }\n return value\n}\n\nfunction toNumber(name: string, spec: ParamSpec, raw: string): number {\n const value = Number(raw)\n const wellFormed = Number.isFinite(value)\n && (spec.integer !== true || (Number.isInteger(value) && String(value) === raw.trim()))\n && (spec.min === undefined || value >= spec.min)\n if (!wellFormed) {\n throw new Error(`Invalid --${name}: \"${raw}\". Must be ${numberRequirement(spec)}`)\n }\n return value\n}\n\nfunction numberRequirement(spec: ParamSpec): string {\n if (spec.integer !== true) {\n return spec.min === undefined ? 'a number' : `a number of at least ${spec.min}`\n }\n if (spec.min === 1) return 'a positive integer'\n return spec.min === undefined ? 'an integer' : `an integer of at least ${spec.min}`\n}\n\nfunction toList(name: string, spec: ParamSpec, raw: string | undefined): string[] | 'all' | undefined {\n if (spec.allowAll === true && raw === 'all') return 'all'\n\n const list = splitList(raw)\n // A list flag that was passed and yielded nothing is a mistake, not a request\n // for the default — and a required list with nothing in it has no meaning.\n const empty = list === undefined || list.length === 0\n if (empty && (spec.required === true || (raw !== undefined && raw !== ''))) {\n throw new Error(`No ${name} provided. Pass a comma-separated list via --${name}.`)\n }\n return list\n}\n\nfunction toArray(value: string | readonly string[]): readonly string[] {\n return typeof value === 'string' ? [value] : value\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 */\nfunction 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 */\nfunction 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","import type { CommandDef } from 'citty'\nimport { commandFromDescriptor } from './_shared.js'\nimport { descriptors } from '../surface/descriptors.js'\n\n/**\n * A registered command: nothing but its lazy loader.\n *\n * There is no way to register a command the CLI does not expose. The flag that\n * allowed it kept producing documented commands that printed \"Unknown command\"\n * when anyone tried them, and every operation an MCP tool covers has to be\n * reachable from a terminal or the two surfaces are not the same tool.\n *\n * The commands themselves are built from the operation descriptors, so the\n * registry states no name, no flag and no description of its own. The loader\n * stays because callers hold it: citty resolves subcommands through it, and the\n * reference generator awaits it. It also keeps the definition's identity\n * stable, which is how the generator recognises two names for one command.\n */\nexport interface CommandEntry {\n load: () => Promise<CommandDef>\n}\n\nfunction entry(build: () => CommandDef): CommandEntry {\n let built: CommandDef | undefined\n return { load: async () => (built ??= build()) }\n}\n\nfunction registry(): Record<string, CommandEntry> {\n const commands: Record<string, CommandEntry> = {}\n for (const descriptor of descriptors) {\n const cli = descriptor.cli\n if (cli === null) continue\n commands[cli.name] = entry(() => commandFromDescriptor(descriptor))\n }\n return commands\n}\n\nexport const commands: Record<string, CommandEntry> = registry()\n","import { createRequire } from 'node:module'\nimport { defineCommand, runCommand, runMain } from 'citty'\nimport { commands as allCommands } from './commands/index.js'\nimport type { CommandEntry } from './commands/index.js'\nimport { emitErrorResult } from './commands/_shared.js'\n\n// Every registered command is a subcommand. There is no filter here, and no\n// second list to fall out of step with the registry.\nconst commands = Object.fromEntries(\n Object.entries(allCommands as Record<string, CommandEntry>)\n .map(([name, entry]) => [name, entry.load]),\n)\n\nconst require = createRequire(import.meta.url)\nconst { version, description } = require('../package.json') as { version: string; description: string }\n\nconst main = defineCommand({\n meta: {\n name: 'the-i18n-cli',\n version,\n description,\n },\n subCommands: commands,\n})\n\nexport async function runCli(): Promise<void> {\n const rawArgs = process.argv.slice(2)\n\n // Let citty handle --help and --version natively (pretty-printed usage)\n if (rawArgs.includes('--help') || rawArgs.includes('-h')\n || rawArgs.includes('--version') || rawArgs.includes('-v')) {\n await runMain(main)\n return\n }\n\n // For normal execution, use runCommand so we control error output.\n // Command run() errors are handled inside createCommand; this catch only\n // sees pre-run failures (unknown command, argument parsing) — those must\n // also keep stdout parseable in JSON mode.\n try {\n await runCommand(main, { rawArgs })\n } catch (error: unknown) {\n emitErrorResult(error, { json: rawArgs.includes('--json') })\n process.exitCode = 1\n }\n}\n"],"mappings":"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"}
@@ -16,4 +16,4 @@
16
16
  declare function defineRouting<T>(config: T): T;
17
17
  //#endregion
18
18
  export { defineRouting as default, defineRouting };
19
- //# sourceMappingURL=next-intl-routing-lExp4Q3r.d.ts.map
19
+ //# sourceMappingURL=next-intl-routing-i6Mlxwdt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"next-intl-routing-lExp4Q3r.d.ts","names":[],"sources":["../../../../src/config/framework/stubs/next-intl-routing.ts"],"sourcesContent":[],"mappings":";;AAcA;;;;;;;;;;;;;iBAAgB,yBAAyB,IAAI"}
1
+ {"version":3,"file":"next-intl-routing-i6Mlxwdt.d.ts","names":[],"sources":["../../../../src/config/framework/stubs/next-intl-routing.ts"],"sourcesContent":[],"mappings":";;AAcA;;;;;;;;;;;;;iBAAgB,yBAAyB,IAAI"}
@@ -1,5 +1,5 @@
1
1
  //#region src/adapters/types.d.ts
2
- type LocaleFileFormat = 'json' | 'php-array';
2
+ type LocaleFileFormat = 'json' | 'php-array' | 'yaml';
3
3
  //#endregion
4
4
  //#region src/config/types.d.ts
5
5
  /**
@@ -35,6 +35,12 @@ interface AppInfo {
35
35
  rootDir: string;
36
36
  /** Layer names this app consumes (from _layers) */
37
37
  layers: string[];
38
+ /**
39
+ * Where the consumption edges came from, when it was not the framework
40
+ * adapter: 'workspace' for package.json dependency inference, 'declared'
41
+ * for an `apps` list in the project config. Absent means the adapter.
42
+ */
43
+ source?: 'workspace' | 'declared';
38
44
  }
39
45
  /**
40
46
  * Project-specific configuration from `.i18n-mcp.json`.
@@ -82,6 +88,13 @@ interface ProjectConfig {
82
88
  path: string;
83
89
  layer: string;
84
90
  }>;
91
+ /** Declared consumer graph: which app consumes which layers. Overrides both adapter discovery and workspace inference. */
92
+ apps?: Array<{
93
+ name: string;
94
+ layers: string[];
95
+ }>;
96
+ /** Whether the consumer graph may be inferred from the workspace when the adapter has no app information ('auto', the default) or not ('off'). */
97
+ consumerGraph?: 'auto' | 'off';
85
98
  /** Default locale code (required for generic adapter activation). */
86
99
  defaultLocale?: string;
87
100
  /** Explicit list of locale codes. If set, overrides framework auto-detection (all adapters). Auto-discovered when absent. */
@@ -94,13 +107,22 @@ interface ProjectConfig {
94
107
  * protection (with a warning).
95
108
  */
96
109
  protectedLocales?: string[];
97
- /** Override the auto-detected locale file format. E.g., "json" or "php-array". Useful when both formats exist or auto-detection picks wrong. */
110
+ /** Override the auto-detected locale file format. E.g., "json", "php-array" or "yaml". Useful when several formats exist or auto-detection picks wrong. */
98
111
  localeFileFormat?: LocaleFileFormat;
112
+ /**
113
+ * Write a translation memory to `.i18n-kit.lock.json` at the project root:
114
+ * per layer, key and target locale, a hash of the source text the translation
115
+ * was made from. That is what lets a later run tell a current translation from
116
+ * one whose source has changed since — state alone cannot, because a target
117
+ * value exists either way. Off by default; no file is created until enabled.
118
+ */
119
+ translationMemory?: boolean;
99
120
  /**
100
121
  * Base URL for the LLM provider — gateways, self-hosted model servers and
101
122
  * corporate proxies that speak the provider's own protocol. Overrides the
102
123
  * endpoint only, not the request shape or auth header. Overridden by the
103
- * I18N_BASE_URL env var and by --baseUrl. Not supported by "google".
124
+ * I18N_BASE_URL env var and by --baseUrl. Honoured by every provider, each
125
+ * of which is called over plain HTTP against this base.
104
126
  */
105
127
  providerBaseUrl?: string;
106
128
  }
@@ -146,4 +168,4 @@ type I18nKitConfig = ProjectConfig;
146
168
  declare function defineI18nKitConfig(config: I18nKitConfig): I18nKitConfig;
147
169
  //#endregion
148
170
  export { LocaleDir as a, LocaleDefinition as i, defineI18nKitConfig as n, ProjectConfig as o, I18nConfig as r, LocaleFileFormat as s, I18nKitConfig as t };
149
- //# sourceMappingURL=define-config-DW-rgsU6.d.ts.map
171
+ //# sourceMappingURL=define-config-ChY3jk6K.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-config-ChY3jk6K.d.ts","names":[],"sources":["../src/adapters/types.ts","../src/config/types.ts","../src/define-config.ts"],"sourcesContent":[],"mappings":";AAEY,KAAA,gBAAA,GAAgB,MAAA,GAAA,WAAA,GAAA,MAAA;;;AAA5B;;;UCGiB,gBAAA;EAAA;EAcA,IAAA,EAAA,MAAS;EAWT;EAmBA,QAAA,EAAA,MAAa;EAAA;MAMf,CAAA,EAAA,MAAA;;MAUC,CAAA,EAAA,MAAA;;;;;AAqBP,UAnEQ,SAAA,CAmER;;EAgB4B,IAAA,EAAA,MAAA;EAsBpB;EAAU,KAAA,EAAA,MAAA;;cAUhB,EAAA,MAAA;;SAMO,CAAA,EAAA,MAAA;;AAIV,UAlHS,OAAA,CAkHT;EAAO;;;;EC5HH;EAQI,MAAA,EAAA,MAAA,EAAA;EAAmB;;;;;;;;;;;UDqBlB,aAAA;;;;;;eAMF;;;;;;aAMF;;;;gBAIG;;;;;;aAMH;;;MAAuC;;eAErC;;;;;;;;;;;eAWA;;;;;SAEN;;;;;;;;;;;;;;;;;;;qBAgBY;;;;;;;;;;;;;;;;;;;;;UAsBJ,UAAA;;;;;;;;kBAQC;;WAEP;;cAEG;;;;kBAII;;qBAEG;;QAEb;;;;;;;;;AA1C6B,KClFzB,aAAA,GAAgB,aDkFS;AAsBrC;;;;;;AAkBqB,iBClHL,mBAAA,CDkHK,MAAA,EClHuB,aDkHvB,CAAA,EClHuC,aDkHvC"}
@@ -1,2 +1,2 @@
1
- import { a as LocaleDir, i as LocaleDefinition, n as defineI18nKitConfig, o as ProjectConfig, r as I18nConfig, t as I18nKitConfig } from "./define-config-DW-rgsU6.js";
1
+ import { a as LocaleDir, i as LocaleDefinition, n as defineI18nKitConfig, o as ProjectConfig, r as I18nConfig, t as I18nKitConfig } from "./define-config-ChY3jk6K.js";
2
2
  export { I18nConfig, I18nKitConfig, LocaleDefinition, LocaleDir, ProjectConfig, defineI18nKitConfig };
@@ -1 +1 @@
1
- export * from './define-config-BeHvSYBP.d.ts';
1
+ export * from './define-config-e0B0VHq7.d.ts';