@the-i18n-kit/cli 5.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.
- package/LICENSE +21 -0
- package/README.md +8 -58
- package/dist/bin.js +1 -6
- package/dist/bin.js.map +1 -1
- package/dist/{_shared-DFyy3sh8.js → cli-waEjbUM1.js} +157 -27
- package/dist/cli-waEjbUM1.js.map +1 -0
- package/dist/config/framework/stubs/{next-intl-routing-Cwaa3f4R.d.ts → next-intl-routing-i6Mlxwdt.d.ts} +1 -1
- package/dist/config/framework/stubs/{next-intl-routing-Cwaa3f4R.d.ts.map → next-intl-routing-i6Mlxwdt.d.ts.map} +1 -1
- package/dist/{define-config-BaZ1dZph.d.ts → define-config-ChY3jk6K.d.ts} +26 -4
- package/dist/define-config-ChY3jk6K.d.ts.map +1 -0
- package/dist/{define-config-CLgKN-7N.d.ts → define-config-e0B0VHq7.d.ts} +1 -1
- package/dist/define-config.d.ts +1 -1
- package/dist/descriptors-10sgyqEs.js +1225 -0
- package/dist/descriptors-10sgyqEs.js.map +1 -0
- package/dist/detector-BJTkyhQ0.js +5 -0
- package/dist/detector-DtFY4qm3.js +2189 -0
- package/dist/detector-DtFY4qm3.js.map +1 -0
- package/dist/{index-DCG3dOdC.d.ts → index-CdjJ71Ag.d.ts} +665 -250
- package/dist/index-CdjJ71Ag.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -5
- package/dist/json-writer-D0b6vEax.js +2 -0
- package/dist/json-writer-ek8yEI5R.js +342 -0
- package/dist/json-writer-ek8yEI5R.js.map +1 -0
- package/dist/{operations-CTo44gPu.js → operations-CNqEBD__.js} +1505 -3285
- package/dist/operations-CNqEBD__.js.map +1 -0
- package/dist/operations-D7IMu_xY.js +8 -0
- package/dist/php-reader-BGx6Ii2d.js +2 -0
- package/dist/{php-reader-3Fgw80zK.js → php-reader-DRpcRVzv.js} +6 -3
- package/dist/{php-reader-3Fgw80zK.js.map → php-reader-DRpcRVzv.js.map} +1 -1
- package/dist/{providers-CHE2ffi6.js → project-config-C3ao4Uii.js} +24 -214
- package/dist/project-config-C3ao4Uii.js.map +1 -0
- package/dist/providers-BbVPelvp.js +406 -0
- package/dist/providers-BbVPelvp.js.map +1 -0
- package/dist/report-By1-5JtO.js +37 -0
- package/dist/report-By1-5JtO.js.map +1 -0
- package/dist/report-CHZgH9Wy.js +2 -0
- package/package.json +11 -23
- package/dist/_shared-DFyy3sh8.js.map +0 -1
- package/dist/add-C-qs8iBa.js +0 -40
- package/dist/add-C-qs8iBa.js.map +0 -1
- package/dist/check-BI1orjXb.js +0 -41
- package/dist/check-BI1orjXb.js.map +0 -1
- package/dist/cli-DsFuVJNP.js +0 -74
- package/dist/cli-DsFuVJNP.js.map +0 -1
- package/dist/config/framework/stubs/unplugin-vue-i18n-1ud7-Ly8.d.ts +0 -25
- package/dist/config/framework/stubs/unplugin-vue-i18n-1ud7-Ly8.d.ts.map +0 -1
- package/dist/config/framework/stubs/unplugin-vue-i18n.js +0 -26
- package/dist/config/framework/stubs/unplugin-vue-i18n.js.map +0 -1
- package/dist/define-config-BaZ1dZph.d.ts.map +0 -1
- package/dist/detect-7cVfsNJP.js +0 -17
- package/dist/detect-7cVfsNJP.js.map +0 -1
- package/dist/empty-DB8Hl_8u.js +0 -36
- package/dist/empty-DB8Hl_8u.js.map +0 -1
- package/dist/find-duplicates-CmEqBfby.js +0 -42
- package/dist/find-duplicates-CmEqBfby.js.map +0 -1
- package/dist/get-BE21Iy6N.js +0 -41
- package/dist/get-BE21Iy6N.js.map +0 -1
- package/dist/index-DCG3dOdC.d.ts.map +0 -1
- package/dist/init-BtzmhsOM.js +0 -33
- package/dist/init-BtzmhsOM.js.map +0 -1
- package/dist/list-dirs-DUQbwvlE.js +0 -17
- package/dist/list-dirs-DUQbwvlE.js.map +0 -1
- package/dist/missing-CcYrgd54.js +0 -51
- package/dist/missing-CcYrgd54.js.map +0 -1
- package/dist/move-B2px6Ck-.js +0 -50
- package/dist/move-B2px6Ck-.js.map +0 -1
- package/dist/operations-CTo44gPu.js.map +0 -1
- package/dist/php-reader-CpnaPSpZ.js +0 -2
- package/dist/providers-CHE2ffi6.js.map +0 -1
- package/dist/remove-DTbqM4bR.js +0 -41
- package/dist/remove-DTbqM4bR.js.map +0 -1
- package/dist/remove-orphans-Dfzu7hRz.js +0 -57
- package/dist/remove-orphans-Dfzu7hRz.js.map +0 -1
- package/dist/rename-C_rFKkU4.js +0 -45
- package/dist/rename-C_rFKkU4.js.map +0 -1
- package/dist/rename-notice-BV8HNX3O.js +0 -25
- package/dist/rename-notice-BV8HNX3O.js.map +0 -1
- package/dist/rename-notice-Cx1vuTRz.js +0 -2
- package/dist/scaffold-CUimlNqR.js +0 -39
- package/dist/scaffold-CUimlNqR.js.map +0 -1
- package/dist/scan-BkOIYhjZ.js +0 -31
- package/dist/scan-BkOIYhjZ.js.map +0 -1
- package/dist/search-0io1BcsY.js +0 -49
- package/dist/search-0io1BcsY.js.map +0 -1
- package/dist/status-Bp-SzjTN.js +0 -45
- package/dist/status-Bp-SzjTN.js.map +0 -1
- package/dist/translate-cCb2Z57w.js +0 -92
- package/dist/translate-cCb2Z57w.js.map +0 -1
- package/dist/translate-key-DHLWEmrA.js +0 -73
- package/dist/translate-key-DHLWEmrA.js.map +0 -1
- package/dist/update-DV4ijRVB.js +0 -40
- package/dist/update-DV4ijRVB.js.map +0 -1
- package/dist/write-Czzm_Nb0.js +0 -47
- package/dist/write-Czzm_Nb0.js.map +0 -1
- /package/dist/{bin-DrKRgnr9.d.ts → bin-NyzIHE2F.d.ts} +0 -0
|
@@ -0,0 +1,1225 @@
|
|
|
1
|
+
import { h as resolveReferenceLocale, s as findLocaleImpl, t as BASE_URL_ENV } from "./providers-BbVPelvp.js";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
//#region src/core/codequality.ts
|
|
5
|
+
/**
|
|
6
|
+
* GitLab Code Quality (CodeClimate) mapping — pure result-shape → issue-array
|
|
7
|
+
* transforms, no I/O.
|
|
8
|
+
*
|
|
9
|
+
* Format contract (https://docs.gitlab.com/ci/testing/code_quality/):
|
|
10
|
+
* every issue needs description, check_name, fingerprint, severity, and
|
|
11
|
+
* location.path + location.lines.begin, where path is project-root-relative
|
|
12
|
+
* (no leading `./`) and lines.begin is an integer ≥ 1.
|
|
13
|
+
*
|
|
14
|
+
* Fingerprints deliberately exclude line numbers so unrelated edits that
|
|
15
|
+
* shift a usage do not churn findings as new/resolved in the MR widget.
|
|
16
|
+
*/
|
|
17
|
+
const UNDEFINED_KEY_CHECK = "i18n.undefined-key";
|
|
18
|
+
const ORPHAN_KEY_CHECK = "i18n.orphan-key";
|
|
19
|
+
const MISSING_TRANSLATION_CHECK = "i18n.missing-translation";
|
|
20
|
+
const INCOMPLETE_LOCALE_CHECK = "i18n.incomplete-locale";
|
|
21
|
+
const UNCONSUMED_LAYER_CHECK = "i18n.unconsumed-layer";
|
|
22
|
+
const DUPLICATE_KEY_CHECK = "i18n.duplicate-key";
|
|
23
|
+
/** NUL-joined so no part can bleed into its neighbor (keys/paths never contain NUL). */
|
|
24
|
+
function fingerprintOf(...parts) {
|
|
25
|
+
return createHash("sha256").update(parts.join("\0")).digest("hex");
|
|
26
|
+
}
|
|
27
|
+
/** Normalize to the report's path shape: forward slashes, no leading `./`. */
|
|
28
|
+
function toReportPath(path) {
|
|
29
|
+
const normalized = path.replaceAll("\\", "/");
|
|
30
|
+
return normalized.startsWith("./") ? normalized.slice(2) : normalized;
|
|
31
|
+
}
|
|
32
|
+
/** GitLab rejects non-positive or fractional line numbers. */
|
|
33
|
+
function toLineBegin(line) {
|
|
34
|
+
return Math.max(1, Math.trunc(line));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `check` findings → one issue per usage location. Uncertain findings are
|
|
38
|
+
* not mapped: the widget has no "maybe" state, and a hard-looking finding
|
|
39
|
+
* for an unverifiable key would train consumers to ignore the report.
|
|
40
|
+
*/
|
|
41
|
+
function undefinedKeysToCodeQuality(undefinedKeys) {
|
|
42
|
+
const issues = [];
|
|
43
|
+
for (const finding of undefinedKeys) for (const usage of finding.usages) {
|
|
44
|
+
const path = toReportPath(usage.file);
|
|
45
|
+
issues.push({
|
|
46
|
+
description: `i18n key "${finding.key}" is referenced here but defined in no locale file of the consuming app's layers — it renders as the raw key at runtime.`,
|
|
47
|
+
check_name: UNDEFINED_KEY_CHECK,
|
|
48
|
+
fingerprint: fingerprintOf(UNDEFINED_KEY_CHECK, finding.key, path),
|
|
49
|
+
severity: "major",
|
|
50
|
+
location: {
|
|
51
|
+
path,
|
|
52
|
+
lines: { begin: toLineBegin(usage.line) }
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return issues;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Orphan findings → one issue per key, anchored at the layer's
|
|
60
|
+
* reference-locale file (line 1): orphans live in locale files, not code.
|
|
61
|
+
* The fingerprint is anchor-independent (layer + key), so locale-file
|
|
62
|
+
* renames or layout changes do not churn findings. Uncertain,
|
|
63
|
+
* dynamic-matched, and misplaced keys are not part of `orphansByLayer`
|
|
64
|
+
* and therefore never mapped.
|
|
65
|
+
*/
|
|
66
|
+
function orphanKeysToCodeQuality(orphansByLayer, anchorFileByLayer) {
|
|
67
|
+
const issues = [];
|
|
68
|
+
for (const [layer, keys] of Object.entries(orphansByLayer)) {
|
|
69
|
+
const path = toReportPath(anchorFileByLayer[layer] ?? layer);
|
|
70
|
+
for (const key of keys) issues.push({
|
|
71
|
+
description: `Orphan i18n key "${key}" in layer "${layer}" has no source-code reference — it may be consumed dynamically; verify before deleting.`,
|
|
72
|
+
check_name: ORPHAN_KEY_CHECK,
|
|
73
|
+
fingerprint: fingerprintOf(ORPHAN_KEY_CHECK, layer, key),
|
|
74
|
+
severity: "minor",
|
|
75
|
+
location: {
|
|
76
|
+
path,
|
|
77
|
+
lines: { begin: 1 }
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return issues;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The orphan findings of a result — whatever the run was asked to do with
|
|
85
|
+
* them — mapped and anchored in one call.
|
|
86
|
+
*
|
|
87
|
+
* Takes the result rather than the scan internals, which is what lets the
|
|
88
|
+
* mapping run where the result is handed over instead of inside the operation.
|
|
89
|
+
* A removal run reports what it deleted; either way the keys are the same set
|
|
90
|
+
* the scan found, and nothing else in the result is a finding.
|
|
91
|
+
*/
|
|
92
|
+
function orphanResultToCodeQuality(result, ctx) {
|
|
93
|
+
const orphansByLayer = result.orphanKeys ?? result.removed ?? {};
|
|
94
|
+
const { localeDef } = resolveReferenceLocale(ctx.config, ctx.locale);
|
|
95
|
+
return orphanKeysToCodeQuality(orphansByLayer, referenceLocaleAnchorPaths(ctx.config, Object.keys(orphansByLayer), localeDef, ctx.projectDir));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The locale file of one layer, or the layer name when the config has no path
|
|
99
|
+
* for it. Every issue below anchors at a locale file rather than a call site:
|
|
100
|
+
* these findings are about what a file holds, so there is no line to point at
|
|
101
|
+
* and `lines.begin` is 1 throughout.
|
|
102
|
+
*/
|
|
103
|
+
function anchorPathOf(ctx, layer, localeRef) {
|
|
104
|
+
const locale = findLocaleImpl(ctx.config, localeRef);
|
|
105
|
+
return toReportPath((locale === void 0 ? {} : referenceLocaleAnchorPaths(ctx.config, [layer], locale, ctx.projectDir))[layer] ?? layer);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* `missing` findings → one issue per key per locale, anchored at the locale file
|
|
109
|
+
* of the layer that is short of it.
|
|
110
|
+
*
|
|
111
|
+
* Minor rather than major: a missing key falls back to another locale and
|
|
112
|
+
* renders text, where an undefined key renders its own name at the user.
|
|
113
|
+
*/
|
|
114
|
+
function missingTranslationsToCodeQuality(result, ctx) {
|
|
115
|
+
const issues = [];
|
|
116
|
+
for (const [localeCode, byLayer] of Object.entries(result.missing)) {
|
|
117
|
+
const locale = findLocaleImpl(ctx.config, localeCode);
|
|
118
|
+
const anchors = locale === void 0 ? {} : referenceLocaleAnchorPaths(ctx.config, Object.keys(byLayer), locale, ctx.projectDir);
|
|
119
|
+
for (const [layer, keys] of Object.entries(byLayer)) {
|
|
120
|
+
const path = toReportPath(anchors[layer] ?? layer);
|
|
121
|
+
for (const key of keys) issues.push({
|
|
122
|
+
description: `Missing translation for "${key}" in ${localeCode} — the reference locale defines it, layer "${layer}" does not carry it in this locale.`,
|
|
123
|
+
check_name: MISSING_TRANSLATION_CHECK,
|
|
124
|
+
fingerprint: fingerprintOf(MISSING_TRANSLATION_CHECK, layer, localeCode, key),
|
|
125
|
+
severity: "minor",
|
|
126
|
+
location: {
|
|
127
|
+
path,
|
|
128
|
+
lines: { begin: 1 }
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return issues;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* `status` findings → one issue per locale under the bar, plus one per layer no
|
|
137
|
+
* app consumes.
|
|
138
|
+
*
|
|
139
|
+
* The bar is `failUnder` when the caller named one and 100% otherwise, so the
|
|
140
|
+
* report says what the gate says. A locale is `info`: coverage is a figure that
|
|
141
|
+
* moves with every merge request and is not a defect. An unconsumed layer is
|
|
142
|
+
* `minor` — keys nothing can render, which is a fact about the project rather
|
|
143
|
+
* than about today's translation work.
|
|
144
|
+
*
|
|
145
|
+
* Protected locales are skipped. Their gaps are deliberate and already excluded
|
|
146
|
+
* from the overall figure; reporting them would be reporting a decision.
|
|
147
|
+
*/
|
|
148
|
+
function statusToCodeQuality(result, ctx) {
|
|
149
|
+
const threshold = ctx.failUnder ?? 100;
|
|
150
|
+
const primaryLayer = result.summary.layersScanned[0] ?? "";
|
|
151
|
+
const issues = [];
|
|
152
|
+
for (const locale of result.locales) {
|
|
153
|
+
if (locale.protected === true || locale.completion >= threshold) continue;
|
|
154
|
+
issues.push({
|
|
155
|
+
description: `Locale "${locale.code}" is ${locale.completion}% translated, below the ${threshold}% expected — ${locale.missing} key(s) missing, ${locale.empty} empty.`,
|
|
156
|
+
check_name: INCOMPLETE_LOCALE_CHECK,
|
|
157
|
+
fingerprint: fingerprintOf(INCOMPLETE_LOCALE_CHECK, locale.code),
|
|
158
|
+
severity: "info",
|
|
159
|
+
location: {
|
|
160
|
+
path: anchorPathOf(ctx, primaryLayer, locale.code) || locale.code,
|
|
161
|
+
lines: { begin: 1 }
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const referenceCode = result.summary.referenceLocale.code;
|
|
166
|
+
for (const layer of result.summary.unconsumedLayers) issues.push({
|
|
167
|
+
description: `Layer "${layer}" is consumed by no app — the keys it defines cannot render anywhere. Fold it into a consumed layer or declare the app that uses it.`,
|
|
168
|
+
check_name: UNCONSUMED_LAYER_CHECK,
|
|
169
|
+
fingerprint: fingerprintOf(UNCONSUMED_LAYER_CHECK, layer),
|
|
170
|
+
severity: "minor",
|
|
171
|
+
location: {
|
|
172
|
+
path: anchorPathOf(ctx, layer, referenceCode),
|
|
173
|
+
lines: { begin: 1 }
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return issues;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* `find-duplicates` collisions → one issue per key defined in both a shared and
|
|
180
|
+
* a consuming layer, anchored at the shadowing layer's file: that is the copy
|
|
181
|
+
* whose value wins at runtime and the one a reader has to look at.
|
|
182
|
+
*
|
|
183
|
+
* Value duplicates (`byValue`) are not mapped. Two keys carrying the same text
|
|
184
|
+
* are a consolidation opportunity, not a defect in the file, and the group has
|
|
185
|
+
* no single key or layer to anchor a stable fingerprint on.
|
|
186
|
+
*/
|
|
187
|
+
function duplicateKeysToCodeQuality(result, ctx) {
|
|
188
|
+
const { localeDef } = resolveReferenceLocale(ctx.config, ctx.locale);
|
|
189
|
+
const anchors = referenceLocaleAnchorPaths(ctx.config, [...new Set(result.collisions.map((collision) => collision.childLayer))], localeDef, ctx.projectDir);
|
|
190
|
+
return result.collisions.map((collision) => ({
|
|
191
|
+
description: `i18n key "${collision.key}" is defined in both "${collision.sharedLayer}" and "${collision.childLayer}"` + (collision.divergent ? ", with different values — the shared value never shows. Delete one side; never move the key." : ", with the same value — the shared definition never shows. Delete one side; never move the key."),
|
|
192
|
+
check_name: DUPLICATE_KEY_CHECK,
|
|
193
|
+
fingerprint: fingerprintOf(DUPLICATE_KEY_CHECK, collision.sharedLayer, collision.childLayer, collision.key),
|
|
194
|
+
severity: "minor",
|
|
195
|
+
location: {
|
|
196
|
+
path: toReportPath(anchors[collision.childLayer] ?? collision.childLayer),
|
|
197
|
+
lines: { begin: 1 }
|
|
198
|
+
}
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Project-root-relative reference-locale file path per layer, the anchor for
|
|
203
|
+
* orphan issues. Aliases resolve to their target layer's directory. Flat
|
|
204
|
+
* layouts anchor at the locale file; namespaced layouts (per-locale
|
|
205
|
+
* directories) have no single file and anchor at the locale's directory.
|
|
206
|
+
*/
|
|
207
|
+
function referenceLocaleAnchorPaths(config, layers, locale, projectDir) {
|
|
208
|
+
const anchors = {};
|
|
209
|
+
for (const layer of layers) {
|
|
210
|
+
const localeDir = config.localeDirs.find((d) => d.layer === layer);
|
|
211
|
+
const resolved = localeDir?.aliasOf ? config.localeDirs.find((d) => d.layer === localeDir.aliasOf) ?? localeDir : localeDir;
|
|
212
|
+
if (!resolved) continue;
|
|
213
|
+
anchors[layer] = toReportPath(relative(projectDir, locale.file ? join(resolved.path, locale.file) : join(resolved.path, locale.code)));
|
|
214
|
+
}
|
|
215
|
+
return anchors;
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/surface/report.ts
|
|
219
|
+
/** The parameters a `report` declaration adds, in the order they are offered. */
|
|
220
|
+
const REPORT_PARAM_NAMES = ["outputFile", "codequalityOutput"];
|
|
221
|
+
/**
|
|
222
|
+
* The parameters that request a diversion, built from what the operation
|
|
223
|
+
* declared about its report. One description per concept, filled in with the
|
|
224
|
+
* operation's own example, rather than one hand-written variation per command.
|
|
225
|
+
*/
|
|
226
|
+
function reportParams(spec) {
|
|
227
|
+
return {
|
|
228
|
+
outputFile: {
|
|
229
|
+
type: "string",
|
|
230
|
+
description: `Absolute path to write the full JSON output to. Only a compact summary is returned to the caller, which is what you want for a result too large to read in one piece. Example: "${spec.outputFile.example}"`,
|
|
231
|
+
...spec.outputFile.cli === void 0 ? {} : { cli: spec.outputFile.cli },
|
|
232
|
+
...spec.outputFile.mcp === void 0 ? {} : { mcp: spec.outputFile.mcp }
|
|
233
|
+
},
|
|
234
|
+
...spec.codequality === void 0 ? {} : { codequalityOutput: {
|
|
235
|
+
type: "string",
|
|
236
|
+
description: `Also write the ${spec.codequality.findings} as a GitLab Code Quality (CodeClimate) JSON report to this file path.`,
|
|
237
|
+
mcp: { hidden: true }
|
|
238
|
+
} }
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* An operation's parameters with the report parameters merged in.
|
|
243
|
+
*
|
|
244
|
+
* They sit after what the operation takes and before the flags that only
|
|
245
|
+
* decide an exit code, which is where they were written by hand and where
|
|
246
|
+
* `--help` and the generated reference still show them.
|
|
247
|
+
*/
|
|
248
|
+
function withReportParams(params, gates, spec) {
|
|
249
|
+
const gateFlags = flagsOf(gates);
|
|
250
|
+
const declared = Object.entries(params);
|
|
251
|
+
return Object.fromEntries([
|
|
252
|
+
...declared.filter(([name]) => !gateFlags.has(name)),
|
|
253
|
+
...Object.entries(reportParams(spec)),
|
|
254
|
+
...declared.filter(([name]) => gateFlags.has(name))
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Write the result where the caller asked for it, and hand back the compact
|
|
259
|
+
* stand-in — or hand back the result untouched when nothing asked for a file.
|
|
260
|
+
*
|
|
261
|
+
* Called with the arguments the operation ran with, which is what lets an
|
|
262
|
+
* operation answering several questions write each under its own report name.
|
|
263
|
+
*/
|
|
264
|
+
async function divertToReport(result, descriptor, args) {
|
|
265
|
+
const spec = descriptor.report;
|
|
266
|
+
if (spec === void 0 || result === null || typeof result !== "object") return result;
|
|
267
|
+
const projectDir = typeof args.projectDir === "string" ? args.projectDir : process.cwd();
|
|
268
|
+
const [{ detectI18nConfig }, { resolveOutputFile, resolveReportFilePath, validateReportPath }, { writeCodequalityFile, writeReportFile }] = await Promise.all([
|
|
269
|
+
import("./detector-BJTkyhQ0.js"),
|
|
270
|
+
import("./report-CHZgH9Wy.js"),
|
|
271
|
+
import("./json-writer-D0b6vEax.js")
|
|
272
|
+
]);
|
|
273
|
+
const config = await detectI18nConfig(projectDir);
|
|
274
|
+
const codequalityOutput = pathArg(args.codequalityOutput);
|
|
275
|
+
if (spec.codequality !== void 0 && codequalityOutput !== void 0) {
|
|
276
|
+
const issues = spec.codequality.issues(result, {
|
|
277
|
+
projectDir,
|
|
278
|
+
config,
|
|
279
|
+
args
|
|
280
|
+
});
|
|
281
|
+
if (issues !== void 0) {
|
|
282
|
+
validateReportPath(projectDir, codequalityOutput);
|
|
283
|
+
await writeCodequalityFile(codequalityOutput, issues);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const name = typeof spec.name === "function" ? spec.name(args) : spec.name;
|
|
287
|
+
const reportFile = resolveOutputFile(projectDir, pathArg(args.outputFile)) ?? resolveReportFilePath(config, projectDir, name);
|
|
288
|
+
if (reportFile === void 0) return result;
|
|
289
|
+
await writeReportFile(reportFile, result, {
|
|
290
|
+
tool: name,
|
|
291
|
+
args: requestedArgs(descriptor, args)
|
|
292
|
+
});
|
|
293
|
+
return {
|
|
294
|
+
reportFile,
|
|
295
|
+
summary: spec.summary(result)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* What the report records as the request behind it: what the caller actually
|
|
300
|
+
* asked for. Arguments left at their default say nothing a reader of the file
|
|
301
|
+
* does not already know, the report parameters are how the file was asked for
|
|
302
|
+
* rather than part of the question, and a gate flag decides an exit code and
|
|
303
|
+
* nothing about the result.
|
|
304
|
+
*/
|
|
305
|
+
function requestedArgs(descriptor, args) {
|
|
306
|
+
const excluded = new Set([...REPORT_PARAM_NAMES, ...flagsOf(descriptor.gates)]);
|
|
307
|
+
const requested = {};
|
|
308
|
+
for (const [name, spec] of Object.entries(descriptor.params)) {
|
|
309
|
+
const value = args[name];
|
|
310
|
+
if (excluded.has(name) || value === void 0 || value === spec.default) continue;
|
|
311
|
+
requested[name] = value;
|
|
312
|
+
}
|
|
313
|
+
return requested;
|
|
314
|
+
}
|
|
315
|
+
function flagsOf(gates) {
|
|
316
|
+
const flags = /* @__PURE__ */ new Set();
|
|
317
|
+
for (const gate of gates ?? []) if (gate.flag !== void 0) flags.add(gate.flag);
|
|
318
|
+
return flags;
|
|
319
|
+
}
|
|
320
|
+
/** A path parameter as the surfaces deliver it; anything else is not a path. */
|
|
321
|
+
function pathArg(value) {
|
|
322
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/surface/types.ts
|
|
326
|
+
/**
|
|
327
|
+
* Declare one operation. The `const` type parameter is what keeps `required:
|
|
328
|
+
* true` and `enum: [...]` literal, so `run` receives `layer: string` rather
|
|
329
|
+
* than `string | undefined` for a parameter the surface guarantees.
|
|
330
|
+
*
|
|
331
|
+
* An operation that declares a `report` gains the parameters that request one
|
|
332
|
+
* here, so the two cannot drift apart.
|
|
333
|
+
*/
|
|
334
|
+
function defineOperation(descriptor) {
|
|
335
|
+
const erased = descriptor;
|
|
336
|
+
if (erased.report === void 0) return erased;
|
|
337
|
+
return {
|
|
338
|
+
...erased,
|
|
339
|
+
params: withReportParams(erased.params, erased.gates, erased.report)
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/surface/guidance.ts
|
|
344
|
+
const NO_PROVIDER_CLI = "No provider configured — nothing was translated. Pass --provider and --model (API key via --apiKey or the OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY env vars) to translate automatically.";
|
|
345
|
+
const AGENT_MODE_MCP = "Agent mode — no provider configured on the server. Use the fallbackContexts to translate inline, then call write_translations (mode: \"upsert\") to write the results. To enable provider mode, set I18N_PROVIDER, I18N_MODEL, and the provider API key env on the server process.";
|
|
346
|
+
const AGENT_MODE_KEY_MCP = "Agent mode — no provider configured on the server. Use the fallbackContext to translate inline, then call write_translations (mode: \"upsert\") to write the results. To enable provider mode, set I18N_PROVIDER, I18N_MODEL, and the provider API key env on the server process.";
|
|
347
|
+
/**
|
|
348
|
+
* Locales that lost at least one key, sorted for a stable message.
|
|
349
|
+
* A bare `totalFailed: 141` says nothing about which languages shipped
|
|
350
|
+
* incomplete — this is what makes the count actionable in a CI log.
|
|
351
|
+
* Reads the full and the compact shapes, since either may reach here.
|
|
352
|
+
*/
|
|
353
|
+
function localesWithFailures(result) {
|
|
354
|
+
const perLayer = "layers" in result ? Object.values(result.layers) : [result];
|
|
355
|
+
const locales = /* @__PURE__ */ new Set();
|
|
356
|
+
for (const layer of perLayer) {
|
|
357
|
+
for (const [locale, entry] of Object.entries(layer.results ?? {})) if (entry.failed.length > 0) locales.add(locale);
|
|
358
|
+
for (const entry of layer.summary.byLocale ?? []) if (entry.failed > 0) locales.add(entry.locale);
|
|
359
|
+
}
|
|
360
|
+
return [...locales].sort();
|
|
361
|
+
}
|
|
362
|
+
/** True when any layer of the outcome carries fallback contexts to translate by hand. */
|
|
363
|
+
function hasFallbackContexts(result) {
|
|
364
|
+
return "layers" in result ? Object.values(result.layers).some((layer) => layer.fallbackContexts) : Boolean(result.fallbackContexts);
|
|
365
|
+
}
|
|
366
|
+
function applyTranslateMissingGuidance(result, surface) {
|
|
367
|
+
if (surface === "mcp") {
|
|
368
|
+
if (hasFallbackContexts(result) && result.summary) result.summary.message = AGENT_MODE_MCP;
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (result.summary.mode === "agent") {
|
|
372
|
+
result.summary.message = NO_PROVIDER_CLI;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (result.summary.totalFailed > 0) {
|
|
376
|
+
const affected = localesWithFailures(result);
|
|
377
|
+
result.summary.message = `${result.summary.totalFailed} key(s) failed to translate` + (affected.length > 0 ? ` (locales: ${affected.join(", ")})` : "") + ". Those keys remain missing — re-run to retry them. Pass --fail-on-failed to make this exit 2 in CI.";
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function applyTranslateKeyGuidance(result, surface) {
|
|
381
|
+
if (surface === "mcp") {
|
|
382
|
+
if (result.fallbackContext) result.message = AGENT_MODE_KEY_MCP;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (result.mode === "agent" && result.skipped.some((skip) => skip.reason === "no-provider")) result.message = NO_PROVIDER_CLI;
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region src/surface/descriptors.ts
|
|
389
|
+
/**
|
|
390
|
+
* Every operation the kit exposes, declared once.
|
|
391
|
+
*
|
|
392
|
+
* The CLI builds its fifteen commands from this table and the MCP server builds
|
|
393
|
+
* its fifteen tools from it. Read `./types.ts` first for what a descriptor is
|
|
394
|
+
* allowed to say; this file is the table itself.
|
|
395
|
+
*
|
|
396
|
+
* The core operations are imported per run rather than at module load. This
|
|
397
|
+
* table is read by `--help`, by the tool registrar and by the reference
|
|
398
|
+
* generator, none of which run anything, while the core barrel pulls in the
|
|
399
|
+
* scanner, the parsers and the file writers — a quarter of a megabyte nobody
|
|
400
|
+
* asking for usage text should pay for.
|
|
401
|
+
*/
|
|
402
|
+
const core = () => import("./operations-D7IMu_xY.js");
|
|
403
|
+
const layerFilter = {
|
|
404
|
+
type: "string",
|
|
405
|
+
description: "Layer name to scope this to (e.g., \"root\", \"app-admin\"). If omitted, every layer is included. Call discover to list the layers."
|
|
406
|
+
};
|
|
407
|
+
const layerRequired = {
|
|
408
|
+
type: "string",
|
|
409
|
+
required: true,
|
|
410
|
+
description: "Layer name from discover (e.g., \"root\", \"app-admin\")."
|
|
411
|
+
};
|
|
412
|
+
const referenceLocale = {
|
|
413
|
+
type: "string",
|
|
414
|
+
description: "Locale code used as the source of truth (e.g., \"en\", \"en-US\"). Defaults to the project default locale.",
|
|
415
|
+
cli: { alias: "ref" }
|
|
416
|
+
};
|
|
417
|
+
const readLocale = {
|
|
418
|
+
type: "string",
|
|
419
|
+
description: "Locale code to read from (e.g., \"en\", \"en-US\"). Defaults to the project default locale. Keys are the same across locales, so one is enough."
|
|
420
|
+
};
|
|
421
|
+
const dryRun = (description) => ({
|
|
422
|
+
type: "boolean",
|
|
423
|
+
default: false,
|
|
424
|
+
description
|
|
425
|
+
});
|
|
426
|
+
const scanDirs = {
|
|
427
|
+
type: "string[]",
|
|
428
|
+
description: "Absolute paths of the directories to scan for source usage. Overrides scope-aware scanning: every layer is then checked against these directories alone. Example: [\"/home/user/my-app/apps/admin\"].",
|
|
429
|
+
cli: { hidden: true }
|
|
430
|
+
};
|
|
431
|
+
const excludeDirs = {
|
|
432
|
+
type: "string[]",
|
|
433
|
+
description: "Directory names to skip when scanning source files. Example: [\"storybook\", \"__tests__\", \"node_modules\"].",
|
|
434
|
+
cli: { hidden: true }
|
|
435
|
+
};
|
|
436
|
+
/**
|
|
437
|
+
* Provider selection, shared by the two translating operations.
|
|
438
|
+
*
|
|
439
|
+
* CLI only: the server resolves one backend at startup from I18N_PROVIDER,
|
|
440
|
+
* I18N_MODEL and the provider's API key env, so a request cannot pick another
|
|
441
|
+
* one — and an API key does not belong in a tool call an agent composes.
|
|
442
|
+
*/
|
|
443
|
+
const providerParams = {
|
|
444
|
+
provider: {
|
|
445
|
+
type: "string",
|
|
446
|
+
enum: [
|
|
447
|
+
"openai",
|
|
448
|
+
"anthropic",
|
|
449
|
+
"google"
|
|
450
|
+
],
|
|
451
|
+
description: "LLM provider to translate through. Without one nothing is translated automatically — the result carries the contexts to translate by hand instead.",
|
|
452
|
+
mcp: { hidden: true }
|
|
453
|
+
},
|
|
454
|
+
model: {
|
|
455
|
+
type: "string",
|
|
456
|
+
description: "Model name. Required whenever a provider is set.",
|
|
457
|
+
mcp: { hidden: true }
|
|
458
|
+
},
|
|
459
|
+
apiKey: {
|
|
460
|
+
type: "string",
|
|
461
|
+
description: "API key. Falls back to the OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY environment variables.",
|
|
462
|
+
mcp: { hidden: true }
|
|
463
|
+
},
|
|
464
|
+
baseUrl: {
|
|
465
|
+
type: "string",
|
|
466
|
+
description: `Provider base URL for gateways, self-hosted models and proxies speaking the provider's protocol. Falls back to ${BASE_URL_ENV}, then to providerBaseUrl in .i18n-mcp.json.`,
|
|
467
|
+
mcp: { hidden: true }
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
/**
|
|
471
|
+
* Registry order: it is the order `the-i18n-cli --help` lists commands in and
|
|
472
|
+
* the order both generated reference overviews are written in.
|
|
473
|
+
*/
|
|
474
|
+
const descriptors = [
|
|
475
|
+
defineOperation({
|
|
476
|
+
id: "init",
|
|
477
|
+
cli: { name: "init" },
|
|
478
|
+
mcp: null,
|
|
479
|
+
description: "Create a schema-valid .i18n-mcp.json from framework detection. Non-interactive; refuses to overwrite without force.",
|
|
480
|
+
params: {
|
|
481
|
+
force: {
|
|
482
|
+
type: "boolean",
|
|
483
|
+
default: false,
|
|
484
|
+
description: "Overwrite an existing .i18n-mcp.json."
|
|
485
|
+
},
|
|
486
|
+
dryRun: dryRun("Report the config that would be written without touching disk. Default: false.")
|
|
487
|
+
},
|
|
488
|
+
async run(args) {
|
|
489
|
+
const { initProjectConfig } = await core();
|
|
490
|
+
return initProjectConfig({
|
|
491
|
+
projectDir: args.projectDir,
|
|
492
|
+
force: args.force,
|
|
493
|
+
dryRun: args.dryRun
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}),
|
|
497
|
+
defineOperation({
|
|
498
|
+
id: "discover",
|
|
499
|
+
cli: { name: "discover" },
|
|
500
|
+
mcp: {
|
|
501
|
+
name: "discover",
|
|
502
|
+
title: "Discover i18n Setup"
|
|
503
|
+
},
|
|
504
|
+
description: "Describe the project: detected config, locale directories per layer with file counts and top-level namespaces, the layer graph, and the hand-maintained locales.",
|
|
505
|
+
longDescription: "Call this first to understand the project before reading or writing translations. The result also names the active translation mode (\"provider\" when the server has an env-configured LLM provider, \"agent\" otherwise). layerGraph answers where a new key belongs: a key used by more than one app belongs in a layer those apps share, and layerGraph.shared names those layers.",
|
|
506
|
+
params: {},
|
|
507
|
+
async run(args) {
|
|
508
|
+
const { describeProject } = await core();
|
|
509
|
+
return describeProject({ projectDir: args.projectDir });
|
|
510
|
+
}
|
|
511
|
+
}),
|
|
512
|
+
defineOperation({
|
|
513
|
+
id: "list-namespaces",
|
|
514
|
+
cli: null,
|
|
515
|
+
mcp: {
|
|
516
|
+
name: "list_namespaces",
|
|
517
|
+
title: "List Namespaces"
|
|
518
|
+
},
|
|
519
|
+
description: "List the translation key tree grouped by namespace prefix, with a count per namespace node.",
|
|
520
|
+
longDescription: "Use this to explore the available keys without guessing path prefixes.",
|
|
521
|
+
params: {
|
|
522
|
+
layer: {
|
|
523
|
+
...layerFilter,
|
|
524
|
+
description: "Layer name to filter by (e.g., \"root\", \"app-admin\"). If omitted or \"*\", scans all layers. Call discover to list the layers."
|
|
525
|
+
},
|
|
526
|
+
locale: readLocale
|
|
527
|
+
},
|
|
528
|
+
async run(args) {
|
|
529
|
+
const { listNamespaces } = await core();
|
|
530
|
+
return listNamespaces({
|
|
531
|
+
layer: args.layer,
|
|
532
|
+
locale: args.locale,
|
|
533
|
+
projectDir: args.projectDir
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}),
|
|
537
|
+
defineOperation({
|
|
538
|
+
id: "get",
|
|
539
|
+
cli: { name: "get" },
|
|
540
|
+
mcp: {
|
|
541
|
+
name: "get_translations",
|
|
542
|
+
title: "Get Translations"
|
|
543
|
+
},
|
|
544
|
+
description: "Get translation values for given key paths from a specific locale and layer. Use \"*\" as the locale to read from all locales.",
|
|
545
|
+
params: {
|
|
546
|
+
layer: layerRequired,
|
|
547
|
+
locale: {
|
|
548
|
+
type: "string",
|
|
549
|
+
required: true,
|
|
550
|
+
description: "Locale code, locale file name, or \"*\" to read all locales. Examples: \"en\", \"en-US\", \"en-US.json\", \"*\"."
|
|
551
|
+
},
|
|
552
|
+
keys: {
|
|
553
|
+
type: "string[]",
|
|
554
|
+
required: true,
|
|
555
|
+
description: "Dot-separated key paths to read. Example: [\"common.actions.save\", \"auth.login.title\"]."
|
|
556
|
+
},
|
|
557
|
+
compact: {
|
|
558
|
+
type: "boolean",
|
|
559
|
+
description: "When true and locale is \"*\", returns a summary grouped by key instead of per-locale detail. Default: false.",
|
|
560
|
+
cli: { hidden: true }
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
async run(args) {
|
|
564
|
+
const { getTranslations } = await core();
|
|
565
|
+
return getTranslations({
|
|
566
|
+
layer: args.layer,
|
|
567
|
+
locale: args.locale,
|
|
568
|
+
keys: args.keys,
|
|
569
|
+
compact: args.compact,
|
|
570
|
+
projectDir: args.projectDir
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}),
|
|
574
|
+
defineOperation({
|
|
575
|
+
id: "write",
|
|
576
|
+
cli: { name: "write" },
|
|
577
|
+
mcp: {
|
|
578
|
+
name: "write_translations",
|
|
579
|
+
title: "Write Translations"
|
|
580
|
+
},
|
|
581
|
+
description: "Write translation key-value pairs to a layer. Keys are inserted in alphabetical order.",
|
|
582
|
+
longDescription: "Mode \"upsert\" adds new keys and updates existing ones (default, most common). Mode \"add\" only creates new keys, skipping existing ones. Mode \"update\" only modifies existing keys, skipping missing ones. Use dryRun to preview without writing.",
|
|
583
|
+
params: {
|
|
584
|
+
layer: layerRequired,
|
|
585
|
+
translations: {
|
|
586
|
+
type: "record",
|
|
587
|
+
required: true,
|
|
588
|
+
description: "Map of dot-path keys to locale-value pairs. IMPORTANT: values must be locale maps, NOT plain strings. Locale refs may be a code (\"en-us\"), a language (\"en-US\") or a file (\"en-US.json\"). Wrong: { \"auth.failed\": \"Login failed\" }. Correct: { \"auth.failed\": { \"en-US\": \"Login failed\", \"de-DE\": \"Anmeldung fehlgeschlagen\" } }"
|
|
589
|
+
},
|
|
590
|
+
mode: {
|
|
591
|
+
type: "string",
|
|
592
|
+
enum: [
|
|
593
|
+
"add",
|
|
594
|
+
"update",
|
|
595
|
+
"upsert"
|
|
596
|
+
],
|
|
597
|
+
default: "upsert",
|
|
598
|
+
description: "Write mode. \"upsert\": add-or-update (never fails). \"add\": only new keys. \"update\": only existing keys. Default: \"upsert\"."
|
|
599
|
+
},
|
|
600
|
+
dryRun: dryRun("Return a preview of what would be written without writing any files. Default: false.")
|
|
601
|
+
},
|
|
602
|
+
async run(args) {
|
|
603
|
+
const { writeTranslations } = await core();
|
|
604
|
+
return writeTranslations({
|
|
605
|
+
layer: args.layer,
|
|
606
|
+
translations: args.translations,
|
|
607
|
+
mode: args.mode,
|
|
608
|
+
dryRun: args.dryRun,
|
|
609
|
+
projectDir: args.projectDir
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}),
|
|
613
|
+
defineOperation({
|
|
614
|
+
id: "missing",
|
|
615
|
+
cli: { name: "missing" },
|
|
616
|
+
mcp: {
|
|
617
|
+
name: "get_missing_translations",
|
|
618
|
+
title: "Get Missing Translations"
|
|
619
|
+
},
|
|
620
|
+
description: "Find translation keys that exist in the reference locale but are missing in other locales. Scans a specific layer or all layers.",
|
|
621
|
+
params: {
|
|
622
|
+
layer: layerFilter,
|
|
623
|
+
referenceLocale,
|
|
624
|
+
targetLocales: {
|
|
625
|
+
type: "string[]",
|
|
626
|
+
description: "Locale codes to check for missing keys (e.g., [\"de\", \"fr\", \"es\"]). Defaults to all locales except the reference.",
|
|
627
|
+
cli: { alias: "targets" }
|
|
628
|
+
},
|
|
629
|
+
failOnMissing: {
|
|
630
|
+
type: "boolean",
|
|
631
|
+
default: false,
|
|
632
|
+
description: "Exit 2 when any key is missing (CI gate).",
|
|
633
|
+
mcp: { hidden: true }
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
gates: [{
|
|
637
|
+
flag: "failOnMissing",
|
|
638
|
+
counter: "totalMissingKeys",
|
|
639
|
+
threshold: 0
|
|
640
|
+
}],
|
|
641
|
+
report: {
|
|
642
|
+
name: "get_missing_translations",
|
|
643
|
+
outputFile: { example: "/tmp/missing-translations.json" },
|
|
644
|
+
summary: (result) => result.summary,
|
|
645
|
+
codequality: {
|
|
646
|
+
findings: "missing translations",
|
|
647
|
+
issues: (result, ctx) => missingTranslationsToCodeQuality(result, {
|
|
648
|
+
config: ctx.config,
|
|
649
|
+
projectDir: ctx.projectDir
|
|
650
|
+
})
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
async run(args) {
|
|
654
|
+
const { getMissingTranslations } = await core();
|
|
655
|
+
return getMissingTranslations({
|
|
656
|
+
layer: args.layer,
|
|
657
|
+
referenceLocale: args.referenceLocale,
|
|
658
|
+
targetLocales: args.targetLocales,
|
|
659
|
+
projectDir: args.projectDir
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}),
|
|
663
|
+
defineOperation({
|
|
664
|
+
id: "status",
|
|
665
|
+
cli: { name: "status" },
|
|
666
|
+
mcp: {
|
|
667
|
+
name: "get_translation_status",
|
|
668
|
+
title: "Get Translation Status"
|
|
669
|
+
},
|
|
670
|
+
description: "Translation coverage in one call: per-locale and per-layer counts of total, translated, missing and empty keys, plus an overall completion percentage.",
|
|
671
|
+
longDescription: "Use this instead of calling get_missing_translations per layer and counting keys yourself. Empty-string values count as untranslated; set listEmpty to get the keys behind that count — they exist in the locale file, so they are never reported as missing, and they render as nothing in the UI. Locales listed in protectedLocales are reported but excluded from the overall figure, since they are maintained by hand.",
|
|
672
|
+
params: {
|
|
673
|
+
layer: layerFilter,
|
|
674
|
+
referenceLocale,
|
|
675
|
+
listEmpty: {
|
|
676
|
+
type: "boolean",
|
|
677
|
+
default: false,
|
|
678
|
+
description: "Also list the keys behind summary.emptyKeys under \"empty\" (locale → layer → keys), and keys that are empty in the reference locale itself under \"emptyInReference\" — useful after a scaffold or an interrupted translation run. Default: false, which returns counts only."
|
|
679
|
+
},
|
|
680
|
+
failUnder: {
|
|
681
|
+
type: "number",
|
|
682
|
+
description: "Exit 2 when overall completion is below this percentage (CI gate).",
|
|
683
|
+
mcp: { hidden: true }
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
gates: [{
|
|
687
|
+
flag: "failUnder",
|
|
688
|
+
counter: "completionPercent",
|
|
689
|
+
direction: "below"
|
|
690
|
+
}],
|
|
691
|
+
report: {
|
|
692
|
+
name: "get_translation_status",
|
|
693
|
+
outputFile: { example: "/tmp/translation-status.json" },
|
|
694
|
+
summary: (result) => result.summary,
|
|
695
|
+
codequality: {
|
|
696
|
+
findings: "incomplete locales and unconsumed layers",
|
|
697
|
+
issues: (result, ctx) => statusToCodeQuality(result, {
|
|
698
|
+
config: ctx.config,
|
|
699
|
+
projectDir: ctx.projectDir,
|
|
700
|
+
failUnder: ctx.args.failUnder
|
|
701
|
+
})
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
async run(args) {
|
|
705
|
+
const { getTranslationStatus } = await core();
|
|
706
|
+
return getTranslationStatus({
|
|
707
|
+
layer: args.layer,
|
|
708
|
+
referenceLocale: args.referenceLocale,
|
|
709
|
+
listEmpty: args.listEmpty,
|
|
710
|
+
projectDir: args.projectDir
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}),
|
|
714
|
+
defineOperation({
|
|
715
|
+
id: "search",
|
|
716
|
+
cli: { name: "search" },
|
|
717
|
+
mcp: {
|
|
718
|
+
name: "search_translations",
|
|
719
|
+
title: "Search Translations"
|
|
720
|
+
},
|
|
721
|
+
description: "Search translation files by key path or value. A case-insensitive substring match — not fuzzy, not a regular expression.",
|
|
722
|
+
longDescription: "Useful for finding an existing translation before adding a duplicate of it.",
|
|
723
|
+
params: {
|
|
724
|
+
query: {
|
|
725
|
+
type: "string",
|
|
726
|
+
required: true,
|
|
727
|
+
description: "Substring to search for, matched against keys and/or values. Case-insensitive. Example: \"save\" matches the key \"common.actions.save\" and the value \"Save changes\"."
|
|
728
|
+
},
|
|
729
|
+
searchIn: {
|
|
730
|
+
type: "string",
|
|
731
|
+
enum: [
|
|
732
|
+
"keys",
|
|
733
|
+
"values",
|
|
734
|
+
"both"
|
|
735
|
+
],
|
|
736
|
+
default: "both",
|
|
737
|
+
description: "Whether to search translation keys, values, or both. Default: \"both\".",
|
|
738
|
+
cli: { alias: "in" }
|
|
739
|
+
},
|
|
740
|
+
layer: {
|
|
741
|
+
...layerFilter,
|
|
742
|
+
description: "Layer name to search in (e.g., \"root\", \"app-admin\"), or \"*\" for all layers. If omitted, searches every layer."
|
|
743
|
+
},
|
|
744
|
+
locale: {
|
|
745
|
+
type: "string",
|
|
746
|
+
description: "Locale code to search in (e.g., \"en\", \"de\"). If omitted, searches every locale."
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
report: {
|
|
750
|
+
name: "search_translations",
|
|
751
|
+
outputFile: {
|
|
752
|
+
example: "/tmp/search-results.json",
|
|
753
|
+
cli: { hidden: true }
|
|
754
|
+
},
|
|
755
|
+
summary: (result) => ({ totalMatches: result.totalMatches })
|
|
756
|
+
},
|
|
757
|
+
async run(args) {
|
|
758
|
+
const { searchTranslations } = await core();
|
|
759
|
+
return searchTranslations({
|
|
760
|
+
query: args.query,
|
|
761
|
+
searchIn: args.searchIn,
|
|
762
|
+
layer: args.layer,
|
|
763
|
+
locale: args.locale,
|
|
764
|
+
projectDir: args.projectDir
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}),
|
|
768
|
+
defineOperation({
|
|
769
|
+
id: "remove",
|
|
770
|
+
cli: { name: "remove" },
|
|
771
|
+
mcp: {
|
|
772
|
+
name: "remove_translations",
|
|
773
|
+
title: "Remove Translations"
|
|
774
|
+
},
|
|
775
|
+
description: "Remove one or more translation keys from ALL locale files in the given layer.",
|
|
776
|
+
longDescription: "Use dryRun to preview the changes before applying them.",
|
|
777
|
+
params: {
|
|
778
|
+
layer: {
|
|
779
|
+
...layerRequired,
|
|
780
|
+
description: "Layer name from discover (e.g., \"root\", \"app-admin\"). The keys are removed from ALL locale files in this layer."
|
|
781
|
+
},
|
|
782
|
+
keys: {
|
|
783
|
+
type: "string[]",
|
|
784
|
+
required: true,
|
|
785
|
+
description: "Dot-separated key paths to remove from every locale file in the layer. Example: [\"common.actions.delete\", \"auth.errors.expired\"]."
|
|
786
|
+
},
|
|
787
|
+
dryRun: dryRun("Return a preview of what would be removed without writing any files. Default: false.")
|
|
788
|
+
},
|
|
789
|
+
async run(args) {
|
|
790
|
+
const { removeTranslations } = await core();
|
|
791
|
+
return removeTranslations({
|
|
792
|
+
layer: args.layer,
|
|
793
|
+
keys: args.keys,
|
|
794
|
+
dryRun: args.dryRun,
|
|
795
|
+
projectDir: args.projectDir
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}),
|
|
799
|
+
defineOperation({
|
|
800
|
+
id: "move",
|
|
801
|
+
cli: { name: "move" },
|
|
802
|
+
mcp: {
|
|
803
|
+
name: "move_translation_key",
|
|
804
|
+
title: "Move or Rename a Translation Key"
|
|
805
|
+
},
|
|
806
|
+
description: "Move a translation key to another layer, to another key path, or both, carrying every locale that defines it.",
|
|
807
|
+
longDescription: "Pass toLayer to promote an app-layer key to a shared layer once a second app needs it (or to demote a shared key that turned out to be app-specific); call discover first, layerGraph.shared names the layers more than one app consumes. Pass newKey alone to rename the key in place across every locale file of its layer. Writes nothing at all if the destination already holds the key with a different value in any locale; if it holds the same value, that locale is deduplicated instead. Use dryRun to preview the plan.",
|
|
808
|
+
params: {
|
|
809
|
+
layer: {
|
|
810
|
+
...layerRequired,
|
|
811
|
+
description: "Layer the key lives in today, from discover. Example: \"app-admin\"."
|
|
812
|
+
},
|
|
813
|
+
key: {
|
|
814
|
+
type: "string",
|
|
815
|
+
required: true,
|
|
816
|
+
description: "Dot-separated key path to move. Example: \"calendar.views.save\"."
|
|
817
|
+
},
|
|
818
|
+
toLayer: {
|
|
819
|
+
type: "string",
|
|
820
|
+
description: "Layer to move it to, from discover. Example: \"root\". Omit (or repeat layer) to rename the key within its current layer, which then requires newKey."
|
|
821
|
+
},
|
|
822
|
+
newKey: {
|
|
823
|
+
type: "string",
|
|
824
|
+
description: "Key path to give it. Example: \"common.actions.save\". Omit to keep the current path, which then requires toLayer."
|
|
825
|
+
},
|
|
826
|
+
dryRun: dryRun("Return the plan without writing any files. Default: false.")
|
|
827
|
+
},
|
|
828
|
+
async run(args) {
|
|
829
|
+
const { moveTranslationKey } = await core();
|
|
830
|
+
return moveTranslationKey({
|
|
831
|
+
layer: args.layer,
|
|
832
|
+
key: args.key,
|
|
833
|
+
toLayer: args.toLayer,
|
|
834
|
+
newKey: args.newKey,
|
|
835
|
+
dryRun: args.dryRun,
|
|
836
|
+
projectDir: args.projectDir
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}),
|
|
840
|
+
defineOperation({
|
|
841
|
+
id: "translate",
|
|
842
|
+
cli: { name: "translate" },
|
|
843
|
+
mcp: {
|
|
844
|
+
name: "translate_missing",
|
|
845
|
+
title: "Translate Missing",
|
|
846
|
+
annotations: {
|
|
847
|
+
title: "Translate Missing Translations",
|
|
848
|
+
readOnlyHint: false
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
description: "Find the keys missing in the target locales and translate them. Without a translation backend nothing is written: the result carries per-locale fallback contexts to translate by hand instead.",
|
|
852
|
+
longDescription: "Two modes: in provider mode (the server env-configured with I18N_PROVIDER, I18N_MODEL and an API key) it calls the LLM provider directly and writes the results; in agent mode it returns those fallbackContexts — translate them inline and persist via write_translations. Check the discover output for the active mode. Uses the project config (glossary, translation prompt, locale notes, examples) where there is one. Translates all locales concurrently, so pass every target locale at once.",
|
|
853
|
+
params: {
|
|
854
|
+
layer: {
|
|
855
|
+
...layerFilter,
|
|
856
|
+
description: "Layer to translate (e.g., \"root\", \"app-admin\"). Omit to translate every locale-backed layer in one call — the recommended default for layered projects, which returns a result per layer plus an aggregated summary."
|
|
857
|
+
},
|
|
858
|
+
referenceLocale: {
|
|
859
|
+
...referenceLocale,
|
|
860
|
+
description: "Locale code used as the translation source (e.g., \"en\", \"en-US\"). Defaults to the project default locale."
|
|
861
|
+
},
|
|
862
|
+
targetLocales: {
|
|
863
|
+
type: "string[]",
|
|
864
|
+
description: "Locale codes to translate into (e.g., [\"de\", \"fr\", \"sv\"]). Defaults to all locales except the reference.",
|
|
865
|
+
cli: { alias: "targets" }
|
|
866
|
+
},
|
|
867
|
+
keys: {
|
|
868
|
+
type: "string[]",
|
|
869
|
+
description: "Dot-path keys to translate (e.g., [\"auth.login.title\", \"common.save\"]). If omitted, translates every missing key in the layer."
|
|
870
|
+
},
|
|
871
|
+
batchSize: {
|
|
872
|
+
type: "number",
|
|
873
|
+
integer: true,
|
|
874
|
+
min: 1,
|
|
875
|
+
description: "Maximum number of keys per provider request. Default: 50. A lower value reduces per-batch risk and increases round trips."
|
|
876
|
+
},
|
|
877
|
+
overwriteStale: {
|
|
878
|
+
type: "boolean",
|
|
879
|
+
default: false,
|
|
880
|
+
description: "Also re-translate keys whose target value was written from source text that has changed since. Requires translationMemory in the project config — without it nothing is known to be stale and this changes nothing. Default: false, which reports those keys under \"stale\" and leaves their values alone."
|
|
881
|
+
},
|
|
882
|
+
dryRun: dryRun("Return which keys would be translated without calling the provider or writing files. Default: false."),
|
|
883
|
+
compact: {
|
|
884
|
+
type: "boolean",
|
|
885
|
+
description: "Return a compact summary (totalTranslated, totalFailed, byLocale) instead of full per-locale results. Default: false.",
|
|
886
|
+
cli: { hidden: true }
|
|
887
|
+
},
|
|
888
|
+
...providerParams,
|
|
889
|
+
failOnFailed: {
|
|
890
|
+
type: "boolean",
|
|
891
|
+
default: false,
|
|
892
|
+
description: "Exit 2 when any key failed to translate (CI gate).",
|
|
893
|
+
mcp: { hidden: true }
|
|
894
|
+
}
|
|
895
|
+
},
|
|
896
|
+
gates: [{
|
|
897
|
+
flag: "failOnFailed",
|
|
898
|
+
counter: "totalFailed",
|
|
899
|
+
threshold: 0
|
|
900
|
+
}],
|
|
901
|
+
usesTranslateFn: true,
|
|
902
|
+
async run(args, ctx) {
|
|
903
|
+
const { translateMissing } = await core();
|
|
904
|
+
const result = await translateMissing({
|
|
905
|
+
layer: args.layer,
|
|
906
|
+
referenceLocale: args.referenceLocale,
|
|
907
|
+
targetLocales: args.targetLocales,
|
|
908
|
+
keys: args.keys,
|
|
909
|
+
batchSize: args.batchSize,
|
|
910
|
+
overwriteStale: args.overwriteStale,
|
|
911
|
+
dryRun: args.dryRun,
|
|
912
|
+
compact: args.compact,
|
|
913
|
+
projectDir: args.projectDir,
|
|
914
|
+
translateFn: ctx.translateFn,
|
|
915
|
+
progressFn: ctx.progressFn,
|
|
916
|
+
onProgressTotal: ctx.onProgressTotal
|
|
917
|
+
});
|
|
918
|
+
applyTranslateMissingGuidance(result, ctx.surface);
|
|
919
|
+
return result;
|
|
920
|
+
}
|
|
921
|
+
}),
|
|
922
|
+
defineOperation({
|
|
923
|
+
id: "translate-key",
|
|
924
|
+
cli: { name: "translate-key" },
|
|
925
|
+
mcp: {
|
|
926
|
+
name: "translate_key",
|
|
927
|
+
title: "Translate Key",
|
|
928
|
+
annotations: {
|
|
929
|
+
title: "Translate Single Key",
|
|
930
|
+
readOnlyHint: false
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
description: "Add or update one source translation key and translate it into the target locales.",
|
|
934
|
+
longDescription: "Unlike translate_missing, this can overwrite an existing but stale target translation. Same two modes as translate_missing: provider mode (the server env-configured) translates directly; agent mode returns a fallbackContext — translate it inline and persist via write_translations.",
|
|
935
|
+
params: {
|
|
936
|
+
layer: {
|
|
937
|
+
...layerRequired,
|
|
938
|
+
description: "Layer holding the key, from discover (e.g., \"root\", \"app-admin\")."
|
|
939
|
+
},
|
|
940
|
+
key: {
|
|
941
|
+
type: "string",
|
|
942
|
+
required: true,
|
|
943
|
+
description: "Dot-separated key path to translate. Example: \"bookingCreator.options.removeSubResource\"."
|
|
944
|
+
},
|
|
945
|
+
sourceLocale: {
|
|
946
|
+
type: "string",
|
|
947
|
+
required: true,
|
|
948
|
+
description: "Source locale ref. May be a code (\"en-us\"), a language (\"en-US\") or a file (\"en-US.json\")."
|
|
949
|
+
},
|
|
950
|
+
sourceValue: {
|
|
951
|
+
type: "string",
|
|
952
|
+
description: "Source value to write before translating. If omitted, the existing source value is read."
|
|
953
|
+
},
|
|
954
|
+
targetLocales: {
|
|
955
|
+
type: "string[]",
|
|
956
|
+
allowAll: true,
|
|
957
|
+
description: "Locales to translate into. Pass \"all\", or omit, for every locale except the source.",
|
|
958
|
+
cli: { alias: "targets" }
|
|
959
|
+
},
|
|
960
|
+
overwrite: {
|
|
961
|
+
type: "boolean",
|
|
962
|
+
default: true,
|
|
963
|
+
description: "Overwrite existing target translations. When false, only missing targets are filled. Default: true."
|
|
964
|
+
},
|
|
965
|
+
dryRun: dryRun("Report the source and target locales without writing files or calling the translation backend. Default: false."),
|
|
966
|
+
includePreview: {
|
|
967
|
+
type: "boolean",
|
|
968
|
+
default: false,
|
|
969
|
+
description: "Include the translated values in the result. Default: false, which keeps the response compact."
|
|
970
|
+
},
|
|
971
|
+
...providerParams
|
|
972
|
+
},
|
|
973
|
+
usesTranslateFn: true,
|
|
974
|
+
async run(args, ctx) {
|
|
975
|
+
const { translateKey } = await core();
|
|
976
|
+
const result = await translateKey({
|
|
977
|
+
layer: args.layer,
|
|
978
|
+
key: args.key,
|
|
979
|
+
sourceLocale: args.sourceLocale,
|
|
980
|
+
sourceValue: args.sourceValue,
|
|
981
|
+
targetLocales: args.targetLocales,
|
|
982
|
+
overwrite: args.overwrite,
|
|
983
|
+
dryRun: args.dryRun,
|
|
984
|
+
includePreview: args.includePreview,
|
|
985
|
+
projectDir: args.projectDir,
|
|
986
|
+
translateFn: ctx.translateFn
|
|
987
|
+
});
|
|
988
|
+
applyTranslateKeyGuidance(result, ctx.surface);
|
|
989
|
+
return result;
|
|
990
|
+
}
|
|
991
|
+
}),
|
|
992
|
+
defineOperation({
|
|
993
|
+
id: "check",
|
|
994
|
+
cli: { name: "check" },
|
|
995
|
+
mcp: {
|
|
996
|
+
name: "find_undefined_keys",
|
|
997
|
+
title: "Find Used-But-Undefined Translation Keys"
|
|
998
|
+
},
|
|
999
|
+
description: "Find keys referenced in source code but defined in NO locale layer the using app consumes — the direction that ships raw keys to production.",
|
|
1000
|
+
longDescription: "The inverse of find_orphan_keys. Scope-aware: each scan unit (app) is checked against the layers it consumes (summary.searchedLayersByApp), so a key defined only in a layer the using app does not consume is still undefined for that app. Known limitation: extraction is line-based and static — dynamically built keys (template literals, concatenation) cannot be verified and are reported as uncertainKeys, never as hard findings. With write, the hard findings are also added to a locale file as empty translations, which is the first half of the fix; uncertain findings are never written.",
|
|
1001
|
+
params: {
|
|
1002
|
+
locale: {
|
|
1003
|
+
...readLocale,
|
|
1004
|
+
description: "Reference locale to resolve key definitions in (e.g., \"en\", \"en-US\"). Defaults to the project default locale."
|
|
1005
|
+
},
|
|
1006
|
+
write: {
|
|
1007
|
+
type: "boolean",
|
|
1008
|
+
default: false,
|
|
1009
|
+
description: "Add every undefined key to a locale file, with an empty string as its value, in the project default locale only. Existing values are never touched, and uncertain findings are never written. The layer is the one the using code resolves against; when that is more than one layer, the run refuses and asks for a layer name. Default: false, which only reports."
|
|
1010
|
+
},
|
|
1011
|
+
layer: {
|
|
1012
|
+
type: "string",
|
|
1013
|
+
description: "Layer to write the undefined keys into (e.g., \"root\", \"app-admin\"). Only read together with write, and only needed when the using code resolves against more than one layer. Call discover to list the layers."
|
|
1014
|
+
},
|
|
1015
|
+
scanDirs,
|
|
1016
|
+
excludeDirs
|
|
1017
|
+
},
|
|
1018
|
+
gates: [{
|
|
1019
|
+
name: "undefined-keys",
|
|
1020
|
+
counter: "undefinedCount",
|
|
1021
|
+
threshold: 0
|
|
1022
|
+
}],
|
|
1023
|
+
report: {
|
|
1024
|
+
name: "find_undefined_keys",
|
|
1025
|
+
outputFile: { example: "/tmp/undefined-keys.json" },
|
|
1026
|
+
summary: (result) => result.summary,
|
|
1027
|
+
codequality: {
|
|
1028
|
+
findings: "findings",
|
|
1029
|
+
issues: (result) => undefinedKeysToCodeQuality(result.undefinedKeys)
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
async run(args) {
|
|
1033
|
+
const { checkUndefinedKeys } = await core();
|
|
1034
|
+
return checkUndefinedKeys({
|
|
1035
|
+
locale: args.locale,
|
|
1036
|
+
write: args.write,
|
|
1037
|
+
layer: args.layer,
|
|
1038
|
+
scanDirs: args.scanDirs,
|
|
1039
|
+
excludeDirs: args.excludeDirs,
|
|
1040
|
+
projectDir: args.projectDir
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
}),
|
|
1044
|
+
defineOperation({
|
|
1045
|
+
id: "orphans",
|
|
1046
|
+
cli: { name: "orphans" },
|
|
1047
|
+
mcp: {
|
|
1048
|
+
name: "find_orphan_keys",
|
|
1049
|
+
title: "Find Orphan Translation Keys"
|
|
1050
|
+
},
|
|
1051
|
+
description: "Report translation keys that no source code references. Nothing is deleted unless remove is set.",
|
|
1052
|
+
longDescription: "Scans a specific layer or all layers, and also detects dynamic key patterns and uncertain matches. Scope-aware: each layer is checked only against the code of the apps that consume it (summary.scanScope shows each layer's effective scope), and keys referenced only from non-consuming apps are reported separately as misplacedUsages rather than as orphans. With remove the orphan keys are deleted from every locale file of their layer — uncertain keys and misplaced usages are never deleted, in any mode.",
|
|
1053
|
+
params: {
|
|
1054
|
+
layer: layerFilter,
|
|
1055
|
+
locale: {
|
|
1056
|
+
...readLocale,
|
|
1057
|
+
description: "Locale code to read the translation keys from (e.g., \"en\", \"en-US\"). Defaults to the project default locale."
|
|
1058
|
+
},
|
|
1059
|
+
remove: {
|
|
1060
|
+
type: "boolean",
|
|
1061
|
+
default: false,
|
|
1062
|
+
description: "Permanently delete the orphan keys from every locale file of their layer. Default: false, which only reports them — run without it first and read the findings. Uncertain keys and misplaced usages are never deleted."
|
|
1063
|
+
},
|
|
1064
|
+
usages: {
|
|
1065
|
+
type: "boolean",
|
|
1066
|
+
default: false,
|
|
1067
|
+
description: "Report where keys are referenced in source (file paths and line numbers) instead of which keys are unreferenced.",
|
|
1068
|
+
mcp: { hidden: true }
|
|
1069
|
+
},
|
|
1070
|
+
keys: {
|
|
1071
|
+
type: "string[]",
|
|
1072
|
+
description: "Keys to report usages for. Only read together with usages; without it, every key is considered.",
|
|
1073
|
+
mcp: { hidden: true }
|
|
1074
|
+
},
|
|
1075
|
+
scanDirs,
|
|
1076
|
+
excludeDirs,
|
|
1077
|
+
failOnOrphans: {
|
|
1078
|
+
type: "boolean",
|
|
1079
|
+
default: false,
|
|
1080
|
+
description: "Exit 2 when any orphan key is found (CI gate).",
|
|
1081
|
+
mcp: { hidden: true }
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
gates: [{
|
|
1085
|
+
flag: "failOnOrphans",
|
|
1086
|
+
counter: "orphanCount",
|
|
1087
|
+
threshold: 0
|
|
1088
|
+
}],
|
|
1089
|
+
report: {
|
|
1090
|
+
name: (args) => args.usages === true ? "scan_code_usage" : args.remove === true ? "remove_orphan_keys" : "find_orphan_keys",
|
|
1091
|
+
outputFile: { example: "/tmp/orphan-keys.json" },
|
|
1092
|
+
summary: (result) => result.summary,
|
|
1093
|
+
codequality: {
|
|
1094
|
+
findings: "orphan findings",
|
|
1095
|
+
issues: (result, ctx) => "usages" in result ? void 0 : orphanResultToCodeQuality(result, {
|
|
1096
|
+
config: ctx.config,
|
|
1097
|
+
projectDir: ctx.projectDir,
|
|
1098
|
+
locale: ctx.args.locale
|
|
1099
|
+
})
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
async run(args, ctx) {
|
|
1103
|
+
const { findOrphanKeys, removeOrphanKeys, scanCodeUsage } = await core();
|
|
1104
|
+
if (args.usages) {
|
|
1105
|
+
if (args.remove) throw new Error("--usages reports where keys are used and never writes. Drop --remove, or drop --usages to delete orphans.");
|
|
1106
|
+
return scanCodeUsage({
|
|
1107
|
+
keys: args.keys,
|
|
1108
|
+
scanDirs: args.scanDirs,
|
|
1109
|
+
excludeDirs: args.excludeDirs,
|
|
1110
|
+
projectDir: args.projectDir
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
if (args.remove) return removeOrphanKeys({
|
|
1114
|
+
layer: args.layer,
|
|
1115
|
+
locale: args.locale,
|
|
1116
|
+
scanDirs: args.scanDirs,
|
|
1117
|
+
excludeDirs: args.excludeDirs,
|
|
1118
|
+
dryRun: false,
|
|
1119
|
+
projectDir: args.projectDir,
|
|
1120
|
+
progressFn: ctx.progressFn,
|
|
1121
|
+
onProgressTotal: ctx.onProgressTotal
|
|
1122
|
+
});
|
|
1123
|
+
return findOrphanKeys({
|
|
1124
|
+
layer: args.layer,
|
|
1125
|
+
locale: args.locale,
|
|
1126
|
+
scanDirs: args.scanDirs,
|
|
1127
|
+
excludeDirs: args.excludeDirs,
|
|
1128
|
+
projectDir: args.projectDir,
|
|
1129
|
+
progressFn: ctx.progressFn,
|
|
1130
|
+
onProgressTotal: ctx.onProgressTotal
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
}),
|
|
1134
|
+
defineOperation({
|
|
1135
|
+
id: "find-duplicates",
|
|
1136
|
+
cli: { name: "find-duplicates" },
|
|
1137
|
+
mcp: {
|
|
1138
|
+
name: "find_duplicate_keys",
|
|
1139
|
+
title: "Find Duplicate Translation Keys Across Layers"
|
|
1140
|
+
},
|
|
1141
|
+
description: "Find translation keys defined in BOTH a shared layer and an app layer that consumes it.",
|
|
1142
|
+
longDescription: "For example the same key in a monorepo root layer and in app-shop. At runtime the app layer's value shadows the shared one, so a collision with divergent values is the dangerous case: the shared value silently never shows. Compares one reference locale and reports each collision with both values and a divergent flag. Fix by deleting one side, never by moving.",
|
|
1143
|
+
params: {
|
|
1144
|
+
locale: {
|
|
1145
|
+
...readLocale,
|
|
1146
|
+
description: "Locale code to compare values in (e.g., \"de\", \"en-US\"). Defaults to the project default locale."
|
|
1147
|
+
},
|
|
1148
|
+
byValue: {
|
|
1149
|
+
type: "boolean",
|
|
1150
|
+
default: false,
|
|
1151
|
+
description: "Also group different keys carrying the same value — e.g. common.actions.save and calendar.views.save both \"Speichern\". Each group says what to do about it: \"reuse\" (a shared layer already has it — delete the app copies and repoint the call sites), \"promote\" (move one to a shared layer) or \"consolidate\" (duplication inside one layer). Default: false."
|
|
1152
|
+
},
|
|
1153
|
+
minValueLength: {
|
|
1154
|
+
type: "number",
|
|
1155
|
+
integer: true,
|
|
1156
|
+
min: 1,
|
|
1157
|
+
description: "Shortest value worth grouping when byValue is set. Default: 4 — below that, values like \"OK\" repeat across unrelated namespaces legitimately."
|
|
1158
|
+
}
|
|
1159
|
+
},
|
|
1160
|
+
report: {
|
|
1161
|
+
name: "find_duplicate_keys",
|
|
1162
|
+
outputFile: { example: "/tmp/duplicate-keys.json" },
|
|
1163
|
+
summary: (result) => result.summary,
|
|
1164
|
+
codequality: {
|
|
1165
|
+
findings: "duplicate keys",
|
|
1166
|
+
issues: (result, ctx) => duplicateKeysToCodeQuality(result, {
|
|
1167
|
+
config: ctx.config,
|
|
1168
|
+
projectDir: ctx.projectDir,
|
|
1169
|
+
locale: ctx.args.locale
|
|
1170
|
+
})
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
async run(args) {
|
|
1174
|
+
const { findDuplicateKeys } = await core();
|
|
1175
|
+
return findDuplicateKeys({
|
|
1176
|
+
locale: args.locale,
|
|
1177
|
+
projectDir: args.projectDir,
|
|
1178
|
+
byValue: args.byValue,
|
|
1179
|
+
minValueLength: args.minValueLength
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}),
|
|
1183
|
+
defineOperation({
|
|
1184
|
+
id: "scaffold",
|
|
1185
|
+
cli: { name: "scaffold" },
|
|
1186
|
+
mcp: {
|
|
1187
|
+
name: "scaffold_locale",
|
|
1188
|
+
title: "Scaffold Locale"
|
|
1189
|
+
},
|
|
1190
|
+
description: "Create empty locale files for new languages, copying the key structure of the default locale with every value set to an empty string.",
|
|
1191
|
+
longDescription: "Supports both JSON (Nuxt) and PHP (Laravel) formats. Does NOT modify the framework configuration — add the locale there first, then call this.",
|
|
1192
|
+
params: {
|
|
1193
|
+
locales: {
|
|
1194
|
+
type: "string[]",
|
|
1195
|
+
description: "Locale codes to scaffold empty files for (e.g., [\"sv\", \"ja\", \"pt-BR\"]). If omitted, auto-detects the locales the config declares but has no files for."
|
|
1196
|
+
},
|
|
1197
|
+
layer: {
|
|
1198
|
+
...layerFilter,
|
|
1199
|
+
description: "Layer to scaffold in (e.g., \"root\", \"app-admin\"). If omitted, scaffolds across every layer."
|
|
1200
|
+
},
|
|
1201
|
+
dryRun: dryRun("Report the files that would be created without writing them. Default: false.")
|
|
1202
|
+
},
|
|
1203
|
+
async run(args) {
|
|
1204
|
+
const { scaffoldLocaleFiles } = await core();
|
|
1205
|
+
return scaffoldLocaleFiles({
|
|
1206
|
+
locales: args.locales,
|
|
1207
|
+
layer: args.layer,
|
|
1208
|
+
dryRun: args.dryRun,
|
|
1209
|
+
projectDir: args.projectDir
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
})
|
|
1213
|
+
];
|
|
1214
|
+
/** The descriptors a surface exposes, in registry order. */
|
|
1215
|
+
function descriptorsFor(surface) {
|
|
1216
|
+
return descriptors.filter((descriptor) => descriptor[surface] !== null);
|
|
1217
|
+
}
|
|
1218
|
+
/** The parameter names a surface exposes for one operation, in declaration order. */
|
|
1219
|
+
function visibleParams(descriptor, surface) {
|
|
1220
|
+
return Object.entries(descriptor.params).filter(([, spec]) => spec[surface]?.hidden !== true).map(([name]) => name);
|
|
1221
|
+
}
|
|
1222
|
+
//#endregion
|
|
1223
|
+
export { divertToReport as i, descriptorsFor as n, visibleParams as r, descriptors as t };
|
|
1224
|
+
|
|
1225
|
+
//# sourceMappingURL=descriptors-10sgyqEs.js.map
|