@inline-i18n-multi/cli 0.7.0 → 0.9.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 +76 -8
- package/dist/bin.js +402 -26
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +354 -23
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -14,12 +14,23 @@ npm install -g @inline-i18n-multi/cli
|
|
|
14
14
|
|
|
15
15
|
## Commands
|
|
16
16
|
|
|
17
|
-
### Extract Translations
|
|
17
|
+
### Extract Translations (v0.9.0)
|
|
18
18
|
|
|
19
|
-
Extract all translations from your codebase:
|
|
19
|
+
Extract all inline translations from your codebase into a structured JSON file:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
npx inline-i18n extract --output translations.json
|
|
23
|
+
npx inline-i18n extract --src ./src --output i18n/translations.json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The extracted file contains all `it()`, `T`, and key-based translations found in source files, grouped by file path:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"src/Header.tsx": [
|
|
31
|
+
{ "key": "greeting", "translations": { "en": "Hello", "ko": "안녕하세요" } }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
23
34
|
```
|
|
24
35
|
|
|
25
36
|
### Check Missing Translations
|
|
@@ -37,6 +48,26 @@ Check translation consistency across locales:
|
|
|
37
48
|
```bash
|
|
38
49
|
npx inline-i18n validate
|
|
39
50
|
npx inline-i18n validate --locales en ko ja
|
|
51
|
+
npx inline-i18n validate --unused
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Detect Unused Keys (v0.8.0)
|
|
55
|
+
|
|
56
|
+
Find translation keys defined in dictionaries but never referenced in source code:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx inline-i18n validate --unused
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Example output:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Found 2 unused translation key(s):
|
|
66
|
+
|
|
67
|
+
- old.feature.title
|
|
68
|
+
defined in src/locales.ts:5
|
|
69
|
+
- deprecated.banner
|
|
70
|
+
defined in src/locales.ts:12
|
|
40
71
|
```
|
|
41
72
|
|
|
42
73
|
### Strict Mode (v0.7.0)
|
|
@@ -54,14 +85,24 @@ Strict mode detects:
|
|
|
54
85
|
Example output:
|
|
55
86
|
|
|
56
87
|
```
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
ICU type mismatch between translations
|
|
89
|
+
src/Header.tsx:12
|
|
90
|
+
en: {count, plural, one {# item} other {# items}}
|
|
91
|
+
ko: {count, select, male {He} female {She}}
|
|
61
92
|
|
|
62
|
-
|
|
93
|
+
Found 1 issue(s)
|
|
63
94
|
```
|
|
64
95
|
|
|
96
|
+
### Generate Types (v0.8.0)
|
|
97
|
+
|
|
98
|
+
Generate TypeScript type definitions from your translation keys for type-safe `t()` calls:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx inline-i18n typegen --output src/i18n.d.ts
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This scans your dictionaries and produces a `.d.ts` file with autocomplete-ready key types.
|
|
105
|
+
|
|
65
106
|
### Generate Report
|
|
66
107
|
|
|
67
108
|
Generate a translation coverage report:
|
|
@@ -70,6 +111,31 @@ Generate a translation coverage report:
|
|
|
70
111
|
npx inline-i18n report
|
|
71
112
|
```
|
|
72
113
|
|
|
114
|
+
### Watch Mode (v0.9.0)
|
|
115
|
+
|
|
116
|
+
Run `validate` or `typegen` in watch mode to automatically re-run on file changes:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npx inline-i18n validate --watch
|
|
120
|
+
npx inline-i18n typegen --output src/i18n.d.ts --watch
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Watch mode monitors your source directory for changes and re-executes the command on each save. Useful during development to catch translation issues or regenerate types in real time.
|
|
124
|
+
|
|
125
|
+
### Context System Support (v0.9.0)
|
|
126
|
+
|
|
127
|
+
The CLI commands (`extract`, `validate`, `typegen`) recognize contextual translation keys using the `key#context` convention:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"greeting": "Hello",
|
|
132
|
+
"greeting#formal": "Good day",
|
|
133
|
+
"greeting#casual": "Hey"
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Validation checks context variants for completeness across locales. Type generation includes context-aware overloads for `t()`.
|
|
138
|
+
|
|
73
139
|
## Options
|
|
74
140
|
|
|
75
141
|
```
|
|
@@ -77,7 +143,9 @@ npx inline-i18n report
|
|
|
77
143
|
--output, -o Output file path
|
|
78
144
|
--locales, -l Comma-separated list of locales
|
|
79
145
|
--format, -f Output format: json, csv (default: json)
|
|
80
|
-
--strict
|
|
146
|
+
--strict Enable strict mode (ICU type consistency check)
|
|
147
|
+
--unused Detect unused translation keys (validate command)
|
|
148
|
+
--watch, -w Watch mode for validate/typegen (re-run on file changes)
|
|
81
149
|
```
|
|
82
150
|
|
|
83
151
|
## Documentation
|
package/dist/bin.js
CHANGED
|
@@ -148,6 +148,125 @@ function parseFile(filePath) {
|
|
|
148
148
|
});
|
|
149
149
|
return entries;
|
|
150
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
|
+
}
|
|
151
270
|
async function parseProject(options = {}) {
|
|
152
271
|
const {
|
|
153
272
|
cwd = process.cwd(),
|
|
@@ -204,7 +323,66 @@ Searching for: "${query}"
|
|
|
204
323
|
}
|
|
205
324
|
|
|
206
325
|
// src/commands/validate.ts
|
|
326
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
327
|
+
|
|
328
|
+
// src/commands/unused.ts
|
|
207
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
|
|
208
386
|
function checkVariableConsistency(entry) {
|
|
209
387
|
const varsByLocale = [];
|
|
210
388
|
for (const [locale, text] of Object.entries(entry.translations)) {
|
|
@@ -268,8 +446,8 @@ function checkICUTypeConsistency(entry) {
|
|
|
268
446
|
};
|
|
269
447
|
}
|
|
270
448
|
async function validate(options = {}) {
|
|
271
|
-
const { cwd, locales, strict } = options;
|
|
272
|
-
console.log(
|
|
449
|
+
const { cwd, locales, strict, unused: checkUnused } = options;
|
|
450
|
+
console.log(import_chalk3.default.blue("\nValidating translations...\n"));
|
|
273
451
|
const entries = await parseProject({ cwd });
|
|
274
452
|
const issues = [];
|
|
275
453
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -314,44 +492,53 @@ async function validate(options = {}) {
|
|
|
314
492
|
if (issue) issues.push(issue);
|
|
315
493
|
}
|
|
316
494
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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)`));
|
|
320
503
|
if (strict) {
|
|
321
|
-
console.log(
|
|
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)"));
|
|
322
508
|
}
|
|
323
509
|
return;
|
|
324
510
|
}
|
|
325
|
-
console.log(
|
|
511
|
+
console.log(import_chalk3.default.red(`Found ${issues.length} issue(s):
|
|
326
512
|
`));
|
|
327
513
|
for (const issue of issues) {
|
|
328
|
-
|
|
329
|
-
console.log(`${icon} ${import_chalk2.default.yellow(issue.message)}`);
|
|
514
|
+
console.log(` ${import_chalk3.default.yellow(issue.message)}`);
|
|
330
515
|
for (const entry of issue.entries) {
|
|
331
516
|
const relativePath = entry.file.replace(process.cwd() + "/", "");
|
|
332
|
-
console.log(
|
|
517
|
+
console.log(import_chalk3.default.gray(` ${relativePath}:${entry.line}`));
|
|
333
518
|
for (const [locale, text] of Object.entries(entry.translations)) {
|
|
334
|
-
console.log(` ${
|
|
519
|
+
console.log(` ${import_chalk3.default.cyan(locale)}: ${text}`);
|
|
335
520
|
}
|
|
336
521
|
}
|
|
337
522
|
if (issue.details && issue.details.length > 0) {
|
|
338
523
|
for (const detail of issue.details) {
|
|
339
|
-
console.log(
|
|
524
|
+
console.log(import_chalk3.default.gray(` \u2192 ${detail}`));
|
|
340
525
|
}
|
|
341
526
|
}
|
|
342
527
|
console.log();
|
|
343
528
|
}
|
|
344
|
-
|
|
529
|
+
if ((issues.length > 0 || unusedCount > 0) && !options.noExit) {
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
345
532
|
}
|
|
346
533
|
|
|
347
534
|
// src/commands/coverage.ts
|
|
348
|
-
var
|
|
535
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
349
536
|
async function coverage(options) {
|
|
350
537
|
const { cwd, locales } = options;
|
|
351
|
-
console.log(
|
|
538
|
+
console.log(import_chalk4.default.blue("\nAnalyzing translation coverage...\n"));
|
|
352
539
|
const entries = await parseProject({ cwd });
|
|
353
540
|
if (entries.length === 0) {
|
|
354
|
-
console.log(
|
|
541
|
+
console.log(import_chalk4.default.yellow("No translations found."));
|
|
355
542
|
return;
|
|
356
543
|
}
|
|
357
544
|
const coverageData = [];
|
|
@@ -370,20 +557,20 @@ async function coverage(options) {
|
|
|
370
557
|
percentage
|
|
371
558
|
});
|
|
372
559
|
}
|
|
373
|
-
console.log(
|
|
560
|
+
console.log(import_chalk4.default.bold("Translation Coverage:\n"));
|
|
374
561
|
const maxLocaleLen = Math.max(...locales.map((l) => l.length), 6);
|
|
375
562
|
console.log(
|
|
376
|
-
|
|
563
|
+
import_chalk4.default.gray(
|
|
377
564
|
`${"Locale".padEnd(maxLocaleLen)} ${"Coverage".padStart(10)} ${"Translated".padStart(12)}`
|
|
378
565
|
)
|
|
379
566
|
);
|
|
380
|
-
console.log(
|
|
567
|
+
console.log(import_chalk4.default.gray("\u2500".repeat(maxLocaleLen + 26)));
|
|
381
568
|
for (const data of coverageData) {
|
|
382
|
-
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;
|
|
383
570
|
const bar = createProgressBar(data.percentage, 10);
|
|
384
571
|
const percentStr = `${data.percentage}%`.padStart(4);
|
|
385
572
|
console.log(
|
|
386
|
-
`${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}`)}`
|
|
387
574
|
);
|
|
388
575
|
}
|
|
389
576
|
console.log();
|
|
@@ -391,9 +578,9 @@ async function coverage(options) {
|
|
|
391
578
|
const partiallyCovered = coverageData.filter((d) => d.percentage > 0 && d.percentage < 100).length;
|
|
392
579
|
const notCovered = coverageData.filter((d) => d.percentage === 0).length;
|
|
393
580
|
if (fullyCovered === locales.length) {
|
|
394
|
-
console.log(
|
|
581
|
+
console.log(import_chalk4.default.green("All locales are fully translated!"));
|
|
395
582
|
} else {
|
|
396
|
-
console.log(
|
|
583
|
+
console.log(import_chalk4.default.gray(`Full: ${fullyCovered}, Partial: ${partiallyCovered}, Empty: ${notCovered}`));
|
|
397
584
|
}
|
|
398
585
|
}
|
|
399
586
|
function createProgressBar(percentage, width) {
|
|
@@ -402,17 +589,206 @@ function createProgressBar(percentage, width) {
|
|
|
402
589
|
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
403
590
|
}
|
|
404
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
|
+
|
|
660
|
+
// src/commands/extract.ts
|
|
661
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
662
|
+
var fs3 = __toESM(require("fs"));
|
|
663
|
+
var path2 = __toESM(require("path"));
|
|
664
|
+
async function extract(options = {}) {
|
|
665
|
+
const { cwd, output = "translations", format = "flat" } = options;
|
|
666
|
+
console.log(import_chalk6.default.blue("\nExtracting inline translations...\n"));
|
|
667
|
+
const entries = await parseProject({ cwd });
|
|
668
|
+
if (entries.length === 0) {
|
|
669
|
+
console.log(import_chalk6.default.yellow("No translations found."));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const byLocale = {};
|
|
673
|
+
const basePath = cwd || process.cwd();
|
|
674
|
+
for (const entry of entries) {
|
|
675
|
+
for (const [locale, text] of Object.entries(entry.translations)) {
|
|
676
|
+
if (!byLocale[locale]) byLocale[locale] = {};
|
|
677
|
+
const relativePath = entry.file.replace(basePath + "/", "");
|
|
678
|
+
const key = `${relativePath}:${entry.line}`;
|
|
679
|
+
byLocale[locale][key] = text;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const outputDir = path2.resolve(basePath, output);
|
|
683
|
+
if (!fs3.existsSync(outputDir)) {
|
|
684
|
+
fs3.mkdirSync(outputDir, { recursive: true });
|
|
685
|
+
}
|
|
686
|
+
const locales = Object.keys(byLocale).sort();
|
|
687
|
+
for (const locale of locales) {
|
|
688
|
+
const translations = byLocale[locale];
|
|
689
|
+
let content;
|
|
690
|
+
if (format === "nested") {
|
|
691
|
+
const nested = buildNestedObject(translations);
|
|
692
|
+
content = JSON.stringify(nested, null, 2) + "\n";
|
|
693
|
+
} else {
|
|
694
|
+
content = JSON.stringify(translations, null, 2) + "\n";
|
|
695
|
+
}
|
|
696
|
+
const filePath = path2.join(outputDir, `${locale}.json`);
|
|
697
|
+
fs3.writeFileSync(filePath, content);
|
|
698
|
+
console.log(
|
|
699
|
+
import_chalk6.default.green(` ${locale}.json`) + import_chalk6.default.gray(` (${Object.keys(translations).length} keys)`)
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
console.log(import_chalk6.default.gray(`
|
|
703
|
+
Extracted ${entries.length} translation(s) to ${output}/`));
|
|
704
|
+
}
|
|
705
|
+
function buildNestedObject(flat) {
|
|
706
|
+
const result = {};
|
|
707
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
708
|
+
const parts = key.split(".");
|
|
709
|
+
let current = result;
|
|
710
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
711
|
+
const part = parts[i];
|
|
712
|
+
if (!current[part] || typeof current[part] !== "object") {
|
|
713
|
+
current[part] = {};
|
|
714
|
+
}
|
|
715
|
+
current = current[part];
|
|
716
|
+
}
|
|
717
|
+
current[parts[parts.length - 1]] = value;
|
|
718
|
+
}
|
|
719
|
+
return result;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/watch.ts
|
|
723
|
+
var import_chokidar = require("chokidar");
|
|
724
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
725
|
+
function startWatch(options) {
|
|
726
|
+
const { cwd, patterns, ignore, onChange, label } = options;
|
|
727
|
+
console.log(import_chalk7.default.blue(`
|
|
728
|
+
Watching for changes (${label})...
|
|
729
|
+
`));
|
|
730
|
+
console.log(import_chalk7.default.gray("Press Ctrl+C to stop\n"));
|
|
731
|
+
const watcher = (0, import_chokidar.watch)(patterns, {
|
|
732
|
+
cwd,
|
|
733
|
+
ignored: ignore,
|
|
734
|
+
ignoreInitial: true
|
|
735
|
+
});
|
|
736
|
+
let debounceTimer = null;
|
|
737
|
+
const handleChange = (filePath) => {
|
|
738
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
739
|
+
debounceTimer = setTimeout(async () => {
|
|
740
|
+
console.log(import_chalk7.default.gray(`
|
|
741
|
+
Change detected: ${filePath}`));
|
|
742
|
+
console.log(import_chalk7.default.gray("-".repeat(40)));
|
|
743
|
+
try {
|
|
744
|
+
await onChange();
|
|
745
|
+
} catch {
|
|
746
|
+
}
|
|
747
|
+
console.log(import_chalk7.default.gray(`
|
|
748
|
+
Watching for changes (${label})...`));
|
|
749
|
+
}, 300);
|
|
750
|
+
};
|
|
751
|
+
watcher.on("change", handleChange);
|
|
752
|
+
watcher.on("add", handleChange);
|
|
753
|
+
}
|
|
754
|
+
|
|
405
755
|
// src/bin.ts
|
|
406
756
|
var program = new import_commander.Command();
|
|
407
|
-
program.name("inline-i18n").description("CLI tools for inline-i18n-multi").version("0.
|
|
757
|
+
program.name("inline-i18n").description("CLI tools for inline-i18n-multi").version("0.9.0");
|
|
408
758
|
program.command("find <query>").description("Find translations containing the query text").option("-c, --cwd <path>", "Working directory").action(async (query, options) => {
|
|
409
759
|
await find({ query, cwd: options.cwd });
|
|
410
760
|
});
|
|
411
|
-
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)").action(async (options) => {
|
|
412
|
-
await validate(options);
|
|
761
|
+
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").option("-w, --watch", "Watch mode - re-validate on file changes").action(async (options) => {
|
|
762
|
+
await validate(options.watch ? { ...options, noExit: true } : options);
|
|
763
|
+
if (options.watch) {
|
|
764
|
+
const cwd = options.cwd || process.cwd();
|
|
765
|
+
startWatch({
|
|
766
|
+
cwd,
|
|
767
|
+
patterns: ["**/*.{ts,tsx,js,jsx}"],
|
|
768
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**"],
|
|
769
|
+
onChange: () => validate({ ...options, noExit: true }),
|
|
770
|
+
label: "validate"
|
|
771
|
+
});
|
|
772
|
+
}
|
|
413
773
|
});
|
|
414
774
|
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) => {
|
|
415
775
|
await coverage(options);
|
|
416
776
|
});
|
|
777
|
+
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").option("-w, --watch", "Watch mode - regenerate types on file changes").action(async (options) => {
|
|
778
|
+
await typegen(options);
|
|
779
|
+
if (options.watch) {
|
|
780
|
+
const cwd = options.cwd || process.cwd();
|
|
781
|
+
startWatch({
|
|
782
|
+
cwd,
|
|
783
|
+
patterns: ["**/*.{ts,tsx,js,jsx}"],
|
|
784
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**"],
|
|
785
|
+
onChange: () => typegen(options),
|
|
786
|
+
label: "typegen"
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
program.command("extract").description("Extract inline translations to JSON files").option("-c, --cwd <path>", "Working directory").option("-o, --output <path>", "Output directory", "translations").option("-f, --format <format>", "Output format (flat|nested)", "flat").action(async (options) => {
|
|
791
|
+
await extract(options);
|
|
792
|
+
});
|
|
417
793
|
program.parse();
|
|
418
794
|
//# sourceMappingURL=bin.js.map
|