@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 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
- ICU type mismatch:
58
- en: count → plural
59
- ko: count select
60
- Details: Variable "count" has type "plural" in en but "select" in ko
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
- 2 issues found
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, -s Enable strict mode (ICU type consistency check)
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(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"));
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
- if (issues.length === 0) {
318
- console.log(import_chalk2.default.green("\u2705 All translations are valid!\n"));
319
- 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)`));
320
503
  if (strict) {
321
- console.log(import_chalk2.default.gray("(strict mode enabled)"));
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(import_chalk2.default.red(`\u274C Found ${issues.length} issue(s):
511
+ console.log(import_chalk3.default.red(`Found ${issues.length} issue(s):
326
512
  `));
327
513
  for (const issue of issues) {
328
- const icon = issue.type === "inconsistent" ? "\u26A0\uFE0F" : issue.type === "missing" ? "\u{1F4ED}" : issue.type === "icu_type_mismatch" ? "\u{1F527}" : "\u{1F500}";
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(import_chalk2.default.gray(` ${relativePath}:${entry.line}`));
517
+ console.log(import_chalk3.default.gray(` ${relativePath}:${entry.line}`));
333
518
  for (const [locale, text] of Object.entries(entry.translations)) {
334
- console.log(` ${import_chalk2.default.cyan(locale)}: ${text}`);
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(import_chalk2.default.gray(` \u2192 ${detail}`));
524
+ console.log(import_chalk3.default.gray(` \u2192 ${detail}`));
340
525
  }
341
526
  }
342
527
  console.log();
343
528
  }
344
- process.exit(1);
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 import_chalk3 = __toESM(require("chalk"));
535
+ var import_chalk4 = __toESM(require("chalk"));
349
536
  async function coverage(options) {
350
537
  const { cwd, locales } = options;
351
- console.log(import_chalk3.default.blue("\nAnalyzing translation coverage...\n"));
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(import_chalk3.default.yellow("No translations found."));
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(import_chalk3.default.bold("Translation Coverage:\n"));
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
- import_chalk3.default.gray(
563
+ import_chalk4.default.gray(
377
564
  `${"Locale".padEnd(maxLocaleLen)} ${"Coverage".padStart(10)} ${"Translated".padStart(12)}`
378
565
  )
379
566
  );
380
- console.log(import_chalk3.default.gray("\u2500".repeat(maxLocaleLen + 26)));
567
+ console.log(import_chalk4.default.gray("\u2500".repeat(maxLocaleLen + 26)));
381
568
  for (const data of coverageData) {
382
- 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;
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)} ${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}`)}`
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(import_chalk3.default.green("\u2705 All locales are fully translated!"));
581
+ console.log(import_chalk4.default.green("All locales are fully translated!"));
395
582
  } else {
396
- 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}`));
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.7.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