@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 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(import_chalk2.default.blue("\nValidating translations...\n"));
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 variableSets = Object.entries(entry.translations).map(([locale, text]) => {
222
- const vars = text.match(/\{(\w+)\}/g) || [];
223
- return { locale, vars: vars.sort().join(",") };
224
- });
225
- const uniqueVarSets = [...new Set(variableSets.map((v) => v.vars))];
226
- if (uniqueVarSets.length > 1) {
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
- if (issues.length === 0) {
235
- console.log(import_chalk2.default.green("\u2705 All translations are valid!\n"));
236
- console.log(import_chalk2.default.gray(`Checked ${entries.length} translation(s)`));
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(import_chalk2.default.red(`\u274C Found ${issues.length} issue(s):
511
+ console.log(import_chalk3.default.red(`Found ${issues.length} issue(s):
240
512
  `));
241
513
  for (const issue of issues) {
242
- const icon = issue.type === "inconsistent" ? "\u26A0\uFE0F" : issue.type === "missing" ? "\u{1F4ED}" : "\u{1F500}";
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(import_chalk2.default.gray(` ${relativePath}:${entry.line}`));
517
+ console.log(import_chalk3.default.gray(` ${relativePath}:${entry.line}`));
247
518
  for (const [locale, text] of Object.entries(entry.translations)) {
248
- console.log(` ${import_chalk2.default.cyan(locale)}: ${text}`);
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
- process.exit(1);
529
+ if (issues.length > 0 || unusedCount > 0) {
530
+ process.exit(1);
531
+ }
254
532
  }
255
533
 
256
534
  // src/commands/coverage.ts
257
- var import_chalk3 = __toESM(require("chalk"));
535
+ var import_chalk4 = __toESM(require("chalk"));
258
536
  async function coverage(options) {
259
537
  const { cwd, locales } = options;
260
- console.log(import_chalk3.default.blue("\nAnalyzing translation coverage...\n"));
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(import_chalk3.default.yellow("No translations found."));
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(import_chalk3.default.bold("Translation Coverage:\n"));
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
- import_chalk3.default.gray(
563
+ import_chalk4.default.gray(
286
564
  `${"Locale".padEnd(maxLocaleLen)} ${"Coverage".padStart(10)} ${"Translated".padStart(12)}`
287
565
  )
288
566
  );
289
- console.log(import_chalk3.default.gray("\u2500".repeat(maxLocaleLen + 26)));
567
+ console.log(import_chalk4.default.gray("\u2500".repeat(maxLocaleLen + 26)));
290
568
  for (const data of coverageData) {
291
- const color = data.percentage === 100 ? import_chalk3.default.green : data.percentage >= 80 ? import_chalk3.default.yellow : import_chalk3.default.red;
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)} ${import_chalk3.default.gray(`${data.translated}/${data.total}`)}`
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(import_chalk3.default.green("\u2705 All locales are fully translated!"));
581
+ console.log(import_chalk4.default.green("All locales are fully translated!"));
304
582
  } else {
305
- console.log(import_chalk3.default.gray(`Full: ${fullyCovered}, Partial: ${partiallyCovered}, Empty: ${notCovered}`));
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.1.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