@inline-i18n-multi/cli 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/dist/bin.js +383 -34
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +42 -1
- package/dist/index.js +390 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,68 @@ Find missing translations for specific locales:
|
|
|
30
30
|
npx inline-i18n check --locales en,ko,ja
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
### Validate Translations
|
|
34
|
+
|
|
35
|
+
Check translation consistency across locales:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx inline-i18n validate
|
|
39
|
+
npx inline-i18n validate --locales en ko ja
|
|
40
|
+
npx inline-i18n validate --unused
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Detect Unused Keys (v0.8.0)
|
|
44
|
+
|
|
45
|
+
Find translation keys defined in dictionaries but never referenced in source code:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx inline-i18n validate --unused
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Example output:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Found 2 unused translation key(s):
|
|
55
|
+
|
|
56
|
+
- old.feature.title
|
|
57
|
+
defined in src/locales.ts:5
|
|
58
|
+
- deprecated.banner
|
|
59
|
+
defined in src/locales.ts:12
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Strict Mode (v0.7.0)
|
|
63
|
+
|
|
64
|
+
Enable ICU type consistency checking with `--strict`:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx inline-i18n validate --strict
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Strict mode detects:
|
|
71
|
+
- **Variable name mismatches** — different variable names across locales
|
|
72
|
+
- **ICU type mismatches** — inconsistent ICU types (e.g., `plural` in one locale, `select` in another)
|
|
73
|
+
|
|
74
|
+
Example output:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
ICU type mismatch between translations
|
|
78
|
+
src/Header.tsx:12
|
|
79
|
+
en: {count, plural, one {# item} other {# items}}
|
|
80
|
+
ko: {count, select, male {He} female {She}}
|
|
81
|
+
|
|
82
|
+
Found 1 issue(s)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Generate Types (v0.8.0)
|
|
86
|
+
|
|
87
|
+
Generate TypeScript type definitions from your translation keys for type-safe `t()` calls:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx inline-i18n typegen --output src/i18n.d.ts
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This scans your dictionaries and produces a `.d.ts` file with autocomplete-ready key types.
|
|
94
|
+
|
|
33
95
|
### Generate Report
|
|
34
96
|
|
|
35
97
|
Generate a translation coverage report:
|
|
@@ -45,6 +107,8 @@ npx inline-i18n report
|
|
|
45
107
|
--output, -o Output file path
|
|
46
108
|
--locales, -l Comma-separated list of locales
|
|
47
109
|
--format, -f Output format: json, csv (default: json)
|
|
110
|
+
--strict Enable strict mode (ICU type consistency check)
|
|
111
|
+
--unused Detect unused translation keys (validate command)
|
|
48
112
|
```
|
|
49
113
|
|
|
50
114
|
## Documentation
|
package/dist/bin.js
CHANGED
|
@@ -71,6 +71,16 @@ function extractVariables(text) {
|
|
|
71
71
|
if (!matches) return [];
|
|
72
72
|
return matches.map((m) => m.slice(1, -1));
|
|
73
73
|
}
|
|
74
|
+
var ICU_TYPE_PATTERN = /\{(\w+),\s*(\w+)/g;
|
|
75
|
+
function extractICUTypes(text) {
|
|
76
|
+
const types = [];
|
|
77
|
+
let match;
|
|
78
|
+
ICU_TYPE_PATTERN.lastIndex = 0;
|
|
79
|
+
while ((match = ICU_TYPE_PATTERN.exec(text)) !== null) {
|
|
80
|
+
types.push({ variable: match[1], type: match[2] });
|
|
81
|
+
}
|
|
82
|
+
return types;
|
|
83
|
+
}
|
|
74
84
|
function parseFile(filePath) {
|
|
75
85
|
const entries = [];
|
|
76
86
|
const code = fs.readFileSync(filePath, "utf-8");
|
|
@@ -106,6 +116,11 @@ function parseFile(filePath) {
|
|
|
106
116
|
if (prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.value.type === "StringLiteral") {
|
|
107
117
|
entry.translations[prop.key.name] = prop.value.value;
|
|
108
118
|
entry.variables.push(...extractVariables(prop.value.value));
|
|
119
|
+
const types = extractICUTypes(prop.value.value);
|
|
120
|
+
if (types.length > 0) {
|
|
121
|
+
if (!entry.icuTypes) entry.icuTypes = {};
|
|
122
|
+
entry.icuTypes[prop.key.name] = types;
|
|
123
|
+
}
|
|
109
124
|
}
|
|
110
125
|
}
|
|
111
126
|
} else if (args[0]?.type === "StringLiteral" && args[1]?.type === "StringLiteral") {
|
|
@@ -114,6 +129,16 @@ function parseFile(filePath) {
|
|
|
114
129
|
entry.translations[lang2] = args[1].value;
|
|
115
130
|
entry.variables.push(...extractVariables(args[0].value));
|
|
116
131
|
entry.variables.push(...extractVariables(args[1].value));
|
|
132
|
+
const types1 = extractICUTypes(args[0].value);
|
|
133
|
+
if (types1.length > 0) {
|
|
134
|
+
if (!entry.icuTypes) entry.icuTypes = {};
|
|
135
|
+
entry.icuTypes[lang1] = types1;
|
|
136
|
+
}
|
|
137
|
+
const types2 = extractICUTypes(args[1].value);
|
|
138
|
+
if (types2.length > 0) {
|
|
139
|
+
if (!entry.icuTypes) entry.icuTypes = {};
|
|
140
|
+
entry.icuTypes[lang2] = types2;
|
|
141
|
+
}
|
|
117
142
|
}
|
|
118
143
|
entry.variables = [...new Set(entry.variables)];
|
|
119
144
|
if (Object.keys(entry.translations).length > 0) {
|
|
@@ -123,6 +148,125 @@ function parseFile(filePath) {
|
|
|
123
148
|
});
|
|
124
149
|
return entries;
|
|
125
150
|
}
|
|
151
|
+
function flattenObjectKeys(node, prefix = "") {
|
|
152
|
+
const keys = [];
|
|
153
|
+
if (node.type !== "ObjectExpression" || !node.properties) return keys;
|
|
154
|
+
for (const prop of node.properties) {
|
|
155
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
156
|
+
let propName;
|
|
157
|
+
if (prop.key.type === "Identifier") propName = prop.key.name;
|
|
158
|
+
else if (prop.key.type === "StringLiteral") propName = prop.key.value;
|
|
159
|
+
if (!propName) continue;
|
|
160
|
+
const fullKey = prefix ? `${prefix}.${propName}` : propName;
|
|
161
|
+
if (prop.value.type === "StringLiteral") {
|
|
162
|
+
keys.push(fullKey);
|
|
163
|
+
} else if (prop.value.type === "ObjectExpression") {
|
|
164
|
+
keys.push(...flattenObjectKeys(prop.value, fullKey));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return keys;
|
|
168
|
+
}
|
|
169
|
+
function parseDictionaryKeys(filePath) {
|
|
170
|
+
const entries = [];
|
|
171
|
+
const code = fs.readFileSync(filePath, "utf-8");
|
|
172
|
+
let ast;
|
|
173
|
+
try {
|
|
174
|
+
ast = (0, import_parser.parse)(code, {
|
|
175
|
+
sourceType: "module",
|
|
176
|
+
plugins: ["typescript", "jsx"]
|
|
177
|
+
});
|
|
178
|
+
} catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
(0, import_traverse.default)(ast, {
|
|
182
|
+
CallExpression(nodePath) {
|
|
183
|
+
const { node } = nodePath;
|
|
184
|
+
const { callee } = node;
|
|
185
|
+
if (callee.type !== "Identifier" || callee.name !== "loadDictionaries") return;
|
|
186
|
+
const args = node.arguments;
|
|
187
|
+
const loc = node.loc;
|
|
188
|
+
if (!loc || !args[0] || args[0].type !== "ObjectExpression") return;
|
|
189
|
+
let namespace = "default";
|
|
190
|
+
if (args[1]?.type === "StringLiteral") {
|
|
191
|
+
namespace = args[1].value;
|
|
192
|
+
}
|
|
193
|
+
const dictObj = args[0];
|
|
194
|
+
const allKeys = /* @__PURE__ */ new Set();
|
|
195
|
+
for (const localeProp of dictObj.properties) {
|
|
196
|
+
if (localeProp.type !== "ObjectProperty") continue;
|
|
197
|
+
if (localeProp.value.type !== "ObjectExpression") continue;
|
|
198
|
+
const localeKeys = flattenObjectKeys(localeProp.value);
|
|
199
|
+
for (const key of localeKeys) {
|
|
200
|
+
allKeys.add(key);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (allKeys.size > 0) {
|
|
204
|
+
entries.push({
|
|
205
|
+
file: filePath,
|
|
206
|
+
line: loc.start.line,
|
|
207
|
+
namespace,
|
|
208
|
+
keys: [...allKeys]
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
return entries;
|
|
214
|
+
}
|
|
215
|
+
function parseTCalls(filePath) {
|
|
216
|
+
const entries = [];
|
|
217
|
+
const code = fs.readFileSync(filePath, "utf-8");
|
|
218
|
+
let ast;
|
|
219
|
+
try {
|
|
220
|
+
ast = (0, import_parser.parse)(code, {
|
|
221
|
+
sourceType: "module",
|
|
222
|
+
plugins: ["typescript", "jsx"]
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
(0, import_traverse.default)(ast, {
|
|
228
|
+
CallExpression(nodePath) {
|
|
229
|
+
const { node } = nodePath;
|
|
230
|
+
const { callee } = node;
|
|
231
|
+
if (callee.type !== "Identifier" || callee.name !== "t") return;
|
|
232
|
+
const args = node.arguments;
|
|
233
|
+
const loc = node.loc;
|
|
234
|
+
if (!loc || !args[0] || args[0].type !== "StringLiteral") return;
|
|
235
|
+
entries.push({
|
|
236
|
+
file: filePath,
|
|
237
|
+
line: loc.start.line,
|
|
238
|
+
key: args[0].value
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return entries;
|
|
243
|
+
}
|
|
244
|
+
async function extractProjectDictionaryKeys(options = {}) {
|
|
245
|
+
const {
|
|
246
|
+
cwd = process.cwd(),
|
|
247
|
+
include = ["**/*.{ts,tsx,js,jsx}"],
|
|
248
|
+
exclude = ["**/node_modules/**", "**/dist/**", "**/.next/**"]
|
|
249
|
+
} = options;
|
|
250
|
+
const files = await (0, import_fast_glob.default)(include, { cwd, ignore: exclude, absolute: true });
|
|
251
|
+
const allEntries = [];
|
|
252
|
+
for (const file of files) {
|
|
253
|
+
allEntries.push(...parseDictionaryKeys(file));
|
|
254
|
+
}
|
|
255
|
+
return allEntries;
|
|
256
|
+
}
|
|
257
|
+
async function extractProjectTCalls(options = {}) {
|
|
258
|
+
const {
|
|
259
|
+
cwd = process.cwd(),
|
|
260
|
+
include = ["**/*.{ts,tsx,js,jsx}"],
|
|
261
|
+
exclude = ["**/node_modules/**", "**/dist/**", "**/.next/**"]
|
|
262
|
+
} = options;
|
|
263
|
+
const files = await (0, import_fast_glob.default)(include, { cwd, ignore: exclude, absolute: true });
|
|
264
|
+
const allEntries = [];
|
|
265
|
+
for (const file of files) {
|
|
266
|
+
allEntries.push(...parseTCalls(file));
|
|
267
|
+
}
|
|
268
|
+
return allEntries;
|
|
269
|
+
}
|
|
126
270
|
async function parseProject(options = {}) {
|
|
127
271
|
const {
|
|
128
272
|
cwd = process.cwd(),
|
|
@@ -179,10 +323,131 @@ Searching for: "${query}"
|
|
|
179
323
|
}
|
|
180
324
|
|
|
181
325
|
// src/commands/validate.ts
|
|
326
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
327
|
+
|
|
328
|
+
// src/commands/unused.ts
|
|
182
329
|
var import_chalk2 = __toESM(require("chalk"));
|
|
330
|
+
var PLURAL_SUFFIXES = ["_zero", "_one", "_two", "_few", "_many", "_other"];
|
|
331
|
+
async function unused(options = {}) {
|
|
332
|
+
const { cwd } = options;
|
|
333
|
+
console.log(import_chalk2.default.blue("\nDetecting unused translations...\n"));
|
|
334
|
+
const dictEntries = await extractProjectDictionaryKeys({ cwd });
|
|
335
|
+
const tCalls = await extractProjectTCalls({ cwd });
|
|
336
|
+
const usedKeys = /* @__PURE__ */ new Set();
|
|
337
|
+
for (const call of tCalls) {
|
|
338
|
+
usedKeys.add(call.key);
|
|
339
|
+
if (!call.key.includes(":")) {
|
|
340
|
+
usedKeys.add(call.key);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const unusedKeys = [];
|
|
344
|
+
for (const entry of dictEntries) {
|
|
345
|
+
for (const key of entry.keys) {
|
|
346
|
+
const fullKey = entry.namespace === "default" ? key : `${entry.namespace}:${key}`;
|
|
347
|
+
if (usedKeys.has(fullKey)) continue;
|
|
348
|
+
let isPluralVariant = false;
|
|
349
|
+
for (const suffix of PLURAL_SUFFIXES) {
|
|
350
|
+
if (key.endsWith(suffix)) {
|
|
351
|
+
const baseKey = key.slice(0, -suffix.length);
|
|
352
|
+
const fullBaseKey = entry.namespace === "default" ? baseKey : `${entry.namespace}:${baseKey}`;
|
|
353
|
+
if (usedKeys.has(fullBaseKey)) {
|
|
354
|
+
isPluralVariant = true;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (isPluralVariant) continue;
|
|
360
|
+
unusedKeys.push({
|
|
361
|
+
namespace: entry.namespace,
|
|
362
|
+
key: fullKey,
|
|
363
|
+
definedIn: entry.file,
|
|
364
|
+
line: entry.line
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (unusedKeys.length === 0) {
|
|
369
|
+
const totalKeys = dictEntries.reduce((sum, e) => sum + e.keys.length, 0);
|
|
370
|
+
console.log(import_chalk2.default.green("No unused translations found!\n"));
|
|
371
|
+
console.log(import_chalk2.default.gray(`Checked ${totalKeys} dictionary key(s) against ${tCalls.length} t() call(s)`));
|
|
372
|
+
return { unusedKeys };
|
|
373
|
+
}
|
|
374
|
+
console.log(import_chalk2.default.yellow(`Found ${unusedKeys.length} unused translation key(s):
|
|
375
|
+
`));
|
|
376
|
+
for (const item of unusedKeys) {
|
|
377
|
+
const relativePath = item.definedIn.replace(process.cwd() + "/", "");
|
|
378
|
+
console.log(` ${import_chalk2.default.red("-")} ${import_chalk2.default.cyan(item.key)}`);
|
|
379
|
+
console.log(import_chalk2.default.gray(` defined in ${relativePath}:${item.line}`));
|
|
380
|
+
}
|
|
381
|
+
console.log();
|
|
382
|
+
return { unusedKeys };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/commands/validate.ts
|
|
386
|
+
function checkVariableConsistency(entry) {
|
|
387
|
+
const varsByLocale = [];
|
|
388
|
+
for (const [locale, text] of Object.entries(entry.translations)) {
|
|
389
|
+
const matches = text.match(/\{(\w+)\}/g) || [];
|
|
390
|
+
const varNames = [...new Set(matches.map((v) => v.slice(1, -1)))].sort();
|
|
391
|
+
varsByLocale.push({ locale, vars: varNames });
|
|
392
|
+
}
|
|
393
|
+
if (varsByLocale.length < 2) return null;
|
|
394
|
+
const reference = varsByLocale[0];
|
|
395
|
+
const details = [];
|
|
396
|
+
for (let i = 1; i < varsByLocale.length; i++) {
|
|
397
|
+
const current = varsByLocale[i];
|
|
398
|
+
const refSet = new Set(reference.vars);
|
|
399
|
+
const curSet = new Set(current.vars);
|
|
400
|
+
const onlyInRef = reference.vars.filter((v) => !curSet.has(v));
|
|
401
|
+
const onlyInCur = current.vars.filter((v) => !refSet.has(v));
|
|
402
|
+
if (onlyInRef.length > 0) {
|
|
403
|
+
details.push(
|
|
404
|
+
`${reference.locale} has {${onlyInRef.join("}, {")}} missing in ${current.locale}`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (onlyInCur.length > 0) {
|
|
408
|
+
details.push(
|
|
409
|
+
`${current.locale} has {${onlyInCur.join("}, {")}} missing in ${reference.locale}`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (details.length === 0) return null;
|
|
414
|
+
return {
|
|
415
|
+
type: "variable_mismatch",
|
|
416
|
+
message: "Variable mismatch between translations",
|
|
417
|
+
entries: [entry],
|
|
418
|
+
details
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function checkICUTypeConsistency(entry) {
|
|
422
|
+
if (!entry.icuTypes) return null;
|
|
423
|
+
const locales = Object.keys(entry.icuTypes);
|
|
424
|
+
if (locales.length < 2) return null;
|
|
425
|
+
const details = [];
|
|
426
|
+
const reference = locales[0];
|
|
427
|
+
const refTypes = entry.icuTypes[reference];
|
|
428
|
+
for (let i = 1; i < locales.length; i++) {
|
|
429
|
+
const locale = locales[i];
|
|
430
|
+
const curTypes = entry.icuTypes[locale];
|
|
431
|
+
for (const refType of refTypes) {
|
|
432
|
+
const match = curTypes.find((t) => t.variable === refType.variable);
|
|
433
|
+
if (match && match.type !== refType.type) {
|
|
434
|
+
details.push(
|
|
435
|
+
`{${refType.variable}} is "${refType.type}" in ${reference} but "${match.type}" in ${locale}`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (details.length === 0) return null;
|
|
441
|
+
return {
|
|
442
|
+
type: "icu_type_mismatch",
|
|
443
|
+
message: "ICU type mismatch between translations",
|
|
444
|
+
entries: [entry],
|
|
445
|
+
details
|
|
446
|
+
};
|
|
447
|
+
}
|
|
183
448
|
async function validate(options = {}) {
|
|
184
|
-
const { cwd, locales } = options;
|
|
185
|
-
console.log(
|
|
449
|
+
const { cwd, locales, strict, unused: checkUnused } = options;
|
|
450
|
+
console.log(import_chalk3.default.blue("\nValidating translations...\n"));
|
|
186
451
|
const entries = await parseProject({ cwd });
|
|
187
452
|
const issues = [];
|
|
188
453
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -218,49 +483,62 @@ async function validate(options = {}) {
|
|
|
218
483
|
}
|
|
219
484
|
}
|
|
220
485
|
for (const entry of entries) {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
issues.push(
|
|
228
|
-
type: "variable_mismatch",
|
|
229
|
-
message: "Variable mismatch between translations",
|
|
230
|
-
entries: [entry]
|
|
231
|
-
});
|
|
486
|
+
const issue = checkVariableConsistency(entry);
|
|
487
|
+
if (issue) issues.push(issue);
|
|
488
|
+
}
|
|
489
|
+
if (strict) {
|
|
490
|
+
for (const entry of entries) {
|
|
491
|
+
const issue = checkICUTypeConsistency(entry);
|
|
492
|
+
if (issue) issues.push(issue);
|
|
232
493
|
}
|
|
233
494
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
495
|
+
let unusedCount = 0;
|
|
496
|
+
if (checkUnused) {
|
|
497
|
+
const result = await unused({ cwd });
|
|
498
|
+
unusedCount = result.unusedKeys.length;
|
|
499
|
+
}
|
|
500
|
+
if (issues.length === 0 && unusedCount === 0) {
|
|
501
|
+
console.log(import_chalk3.default.green("All translations are valid!\n"));
|
|
502
|
+
console.log(import_chalk3.default.gray(`Checked ${entries.length} translation(s)`));
|
|
503
|
+
if (strict) {
|
|
504
|
+
console.log(import_chalk3.default.gray("(strict mode enabled)"));
|
|
505
|
+
}
|
|
506
|
+
if (checkUnused) {
|
|
507
|
+
console.log(import_chalk3.default.gray("(unused key detection enabled)"));
|
|
508
|
+
}
|
|
237
509
|
return;
|
|
238
510
|
}
|
|
239
|
-
console.log(
|
|
511
|
+
console.log(import_chalk3.default.red(`Found ${issues.length} issue(s):
|
|
240
512
|
`));
|
|
241
513
|
for (const issue of issues) {
|
|
242
|
-
|
|
243
|
-
console.log(`${icon} ${import_chalk2.default.yellow(issue.message)}`);
|
|
514
|
+
console.log(` ${import_chalk3.default.yellow(issue.message)}`);
|
|
244
515
|
for (const entry of issue.entries) {
|
|
245
516
|
const relativePath = entry.file.replace(process.cwd() + "/", "");
|
|
246
|
-
console.log(
|
|
517
|
+
console.log(import_chalk3.default.gray(` ${relativePath}:${entry.line}`));
|
|
247
518
|
for (const [locale, text] of Object.entries(entry.translations)) {
|
|
248
|
-
console.log(` ${
|
|
519
|
+
console.log(` ${import_chalk3.default.cyan(locale)}: ${text}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (issue.details && issue.details.length > 0) {
|
|
523
|
+
for (const detail of issue.details) {
|
|
524
|
+
console.log(import_chalk3.default.gray(` \u2192 ${detail}`));
|
|
249
525
|
}
|
|
250
526
|
}
|
|
251
527
|
console.log();
|
|
252
528
|
}
|
|
253
|
-
|
|
529
|
+
if (issues.length > 0 || unusedCount > 0) {
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
254
532
|
}
|
|
255
533
|
|
|
256
534
|
// src/commands/coverage.ts
|
|
257
|
-
var
|
|
535
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
258
536
|
async function coverage(options) {
|
|
259
537
|
const { cwd, locales } = options;
|
|
260
|
-
console.log(
|
|
538
|
+
console.log(import_chalk4.default.blue("\nAnalyzing translation coverage...\n"));
|
|
261
539
|
const entries = await parseProject({ cwd });
|
|
262
540
|
if (entries.length === 0) {
|
|
263
|
-
console.log(
|
|
541
|
+
console.log(import_chalk4.default.yellow("No translations found."));
|
|
264
542
|
return;
|
|
265
543
|
}
|
|
266
544
|
const coverageData = [];
|
|
@@ -279,20 +557,20 @@ async function coverage(options) {
|
|
|
279
557
|
percentage
|
|
280
558
|
});
|
|
281
559
|
}
|
|
282
|
-
console.log(
|
|
560
|
+
console.log(import_chalk4.default.bold("Translation Coverage:\n"));
|
|
283
561
|
const maxLocaleLen = Math.max(...locales.map((l) => l.length), 6);
|
|
284
562
|
console.log(
|
|
285
|
-
|
|
563
|
+
import_chalk4.default.gray(
|
|
286
564
|
`${"Locale".padEnd(maxLocaleLen)} ${"Coverage".padStart(10)} ${"Translated".padStart(12)}`
|
|
287
565
|
)
|
|
288
566
|
);
|
|
289
|
-
console.log(
|
|
567
|
+
console.log(import_chalk4.default.gray("\u2500".repeat(maxLocaleLen + 26)));
|
|
290
568
|
for (const data of coverageData) {
|
|
291
|
-
const color = data.percentage === 100 ?
|
|
569
|
+
const color = data.percentage === 100 ? import_chalk4.default.green : data.percentage >= 80 ? import_chalk4.default.yellow : import_chalk4.default.red;
|
|
292
570
|
const bar = createProgressBar(data.percentage, 10);
|
|
293
571
|
const percentStr = `${data.percentage}%`.padStart(4);
|
|
294
572
|
console.log(
|
|
295
|
-
`${data.locale.padEnd(maxLocaleLen)} ${color(bar)} ${color(percentStr)} ${
|
|
573
|
+
`${data.locale.padEnd(maxLocaleLen)} ${color(bar)} ${color(percentStr)} ${import_chalk4.default.gray(`${data.translated}/${data.total}`)}`
|
|
296
574
|
);
|
|
297
575
|
}
|
|
298
576
|
console.log();
|
|
@@ -300,9 +578,9 @@ async function coverage(options) {
|
|
|
300
578
|
const partiallyCovered = coverageData.filter((d) => d.percentage > 0 && d.percentage < 100).length;
|
|
301
579
|
const notCovered = coverageData.filter((d) => d.percentage === 0).length;
|
|
302
580
|
if (fullyCovered === locales.length) {
|
|
303
|
-
console.log(
|
|
581
|
+
console.log(import_chalk4.default.green("All locales are fully translated!"));
|
|
304
582
|
} else {
|
|
305
|
-
console.log(
|
|
583
|
+
console.log(import_chalk4.default.gray(`Full: ${fullyCovered}, Partial: ${partiallyCovered}, Empty: ${notCovered}`));
|
|
306
584
|
}
|
|
307
585
|
}
|
|
308
586
|
function createProgressBar(percentage, width) {
|
|
@@ -311,17 +589,88 @@ function createProgressBar(percentage, width) {
|
|
|
311
589
|
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
312
590
|
}
|
|
313
591
|
|
|
592
|
+
// src/commands/typegen.ts
|
|
593
|
+
var fs2 = __toESM(require("fs"));
|
|
594
|
+
var path = __toESM(require("path"));
|
|
595
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
596
|
+
var PLURAL_SUFFIXES2 = ["_zero", "_one", "_two", "_few", "_many", "_other"];
|
|
597
|
+
async function typegen(options = {}) {
|
|
598
|
+
const { cwd, output = "src/i18n.d.ts" } = options;
|
|
599
|
+
console.log(import_chalk5.default.blue("\nGenerating TypeScript types...\n"));
|
|
600
|
+
const dictEntries = await extractProjectDictionaryKeys({ cwd });
|
|
601
|
+
const allKeys = /* @__PURE__ */ new Set();
|
|
602
|
+
for (const entry of dictEntries) {
|
|
603
|
+
for (const key of entry.keys) {
|
|
604
|
+
const fullKey = entry.namespace === "default" ? key : `${entry.namespace}:${key}`;
|
|
605
|
+
allKeys.add(fullKey);
|
|
606
|
+
for (const suffix of PLURAL_SUFFIXES2) {
|
|
607
|
+
if (key.endsWith(suffix)) {
|
|
608
|
+
const baseKey = key.slice(0, -suffix.length);
|
|
609
|
+
const fullBaseKey = entry.namespace === "default" ? baseKey : `${entry.namespace}:${baseKey}`;
|
|
610
|
+
allKeys.add(fullBaseKey);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (allKeys.size === 0) {
|
|
616
|
+
console.log(import_chalk5.default.yellow("No dictionary keys found. Make sure loadDictionaries() calls are present."));
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const sortedKeys = [...allKeys].sort();
|
|
620
|
+
const keyUnion = sortedKeys.map((k) => ` | '${k}'`).join("\n");
|
|
621
|
+
const content = `// Auto-generated by inline-i18n typegen
|
|
622
|
+
// Do not edit manually. Re-run: npx inline-i18n typegen
|
|
623
|
+
|
|
624
|
+
import 'inline-i18n-multi'
|
|
625
|
+
|
|
626
|
+
declare module 'inline-i18n-multi' {
|
|
627
|
+
export type TranslationKey =
|
|
628
|
+
${keyUnion}
|
|
629
|
+
|
|
630
|
+
export function t(
|
|
631
|
+
key: TranslationKey,
|
|
632
|
+
vars?: TranslationVars,
|
|
633
|
+
locale?: string
|
|
634
|
+
): string
|
|
635
|
+
|
|
636
|
+
export function hasTranslation(
|
|
637
|
+
key: TranslationKey,
|
|
638
|
+
locale?: string
|
|
639
|
+
): boolean
|
|
640
|
+
}
|
|
641
|
+
`;
|
|
642
|
+
const outputPath = path.resolve(cwd || process.cwd(), output);
|
|
643
|
+
const outputDir = path.dirname(outputPath);
|
|
644
|
+
if (!fs2.existsSync(outputDir)) {
|
|
645
|
+
fs2.mkdirSync(outputDir, { recursive: true });
|
|
646
|
+
}
|
|
647
|
+
fs2.writeFileSync(outputPath, content, "utf-8");
|
|
648
|
+
console.log(import_chalk5.default.green(`Generated ${sortedKeys.length} translation key types`));
|
|
649
|
+
console.log(import_chalk5.default.gray(`Output: ${outputPath}
|
|
650
|
+
`));
|
|
651
|
+
const sample = sortedKeys.slice(0, 5);
|
|
652
|
+
for (const key of sample) {
|
|
653
|
+
console.log(` ${import_chalk5.default.cyan(key)}`);
|
|
654
|
+
}
|
|
655
|
+
if (sortedKeys.length > 5) {
|
|
656
|
+
console.log(import_chalk5.default.gray(` ... and ${sortedKeys.length - 5} more`));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
314
660
|
// src/bin.ts
|
|
315
661
|
var program = new import_commander.Command();
|
|
316
|
-
program.name("inline-i18n").description("CLI tools for inline-i18n-multi").version("0.
|
|
662
|
+
program.name("inline-i18n").description("CLI tools for inline-i18n-multi").version("0.8.0");
|
|
317
663
|
program.command("find <query>").description("Find translations containing the query text").option("-c, --cwd <path>", "Working directory").action(async (query, options) => {
|
|
318
664
|
await find({ query, cwd: options.cwd });
|
|
319
665
|
});
|
|
320
|
-
program.command("validate").description("Validate translation consistency").option("-c, --cwd <path>", "Working directory").option("-l, --locales <locales...>", "Required locales to check").action(async (options) => {
|
|
666
|
+
program.command("validate").description("Validate translation consistency").option("-c, --cwd <path>", "Working directory").option("-l, --locales <locales...>", "Required locales to check").option("-s, --strict", "Enable strict mode (ICU type consistency check)").option("-u, --unused", "Detect unused dictionary keys").action(async (options) => {
|
|
321
667
|
await validate(options);
|
|
322
668
|
});
|
|
323
669
|
program.command("coverage").description("Show translation coverage by locale").option("-c, --cwd <path>", "Working directory").option("-l, --locales <locales...>", "Locales to check", ["ko", "en"]).action(async (options) => {
|
|
324
670
|
await coverage(options);
|
|
325
671
|
});
|
|
672
|
+
program.command("typegen").description("Generate TypeScript types for translation keys").option("-c, --cwd <path>", "Working directory").option("-o, --output <path>", "Output file path", "src/i18n.d.ts").action(async (options) => {
|
|
673
|
+
await typegen(options);
|
|
674
|
+
});
|
|
326
675
|
program.parse();
|
|
327
676
|
//# sourceMappingURL=bin.js.map
|