i18ntk 4.5.3 → 4.6.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.
@@ -22,6 +22,7 @@ const { getUnifiedConfig, displayHelp } = require('../utils/config-helper');
22
22
  const { loadTranslations } = require('../utils/i18n-helper');
23
23
  const SecurityUtils = require('../utils/security');
24
24
  const SetupEnforcer = require('../utils/setup-enforcer');
25
+ const { detectProjectFramework, getFrameworkPatterns, getFrameworkSuggestions, SCANNER_EXTENSIONS, WRAPPER_SKIP_PATTERNS, getExcludeDirs } = require('../utils/framework-detector');
25
26
 
26
27
  // Ensure setup is complete before running
27
28
  (async () => {
@@ -133,6 +134,8 @@ class I18nTextScanner {
133
134
 
134
135
  switch (key) {
135
136
  case 'source-dir':
137
+ case 'code-dir':
138
+ case 'source-code-dir':
136
139
  parsed.sourceDir = value || '';
137
140
  if (value === args[i + 1]) i++;
138
141
  break;
@@ -167,6 +170,7 @@ class I18nTextScanner {
167
170
  parsed.includeTests = true;
168
171
  break;
169
172
  case 'source-language':
173
+ case 'source-locale':
170
174
  parsed.sourceLanguage = value || '';
171
175
  if (value === args[i + 1]) i++;
172
176
  break;
@@ -222,11 +226,26 @@ class I18nTextScanner {
222
226
  const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
223
227
  const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
224
228
 
229
+ if (deps.next || deps['next-intl'] || deps['next-i18next']) return 'next';
225
230
  if (deps.react || deps['react-dom']) return 'react';
226
231
  if (deps.vue || deps['vue-router']) return 'vue';
227
232
  if (deps['@angular/core'] || deps.angular) return 'angular';
228
- if (deps.next) return 'next';
229
233
  if (deps.svelte) return 'svelte';
234
+ if (deps.astro || deps['astro-i18next'] || deps['@astrojs/i18n']) return 'astro';
235
+ if (deps['@remix-run/react'] || deps['remix-i18next']) return 'remix';
236
+ if (deps.gatsby || deps['gatsby-plugin-react-i18next']) return 'gatsby';
237
+ if (deps['@builder.io/qwik'] || deps['qwik-speak']) return 'qwik';
238
+ if (deps['solid-js'] || deps['@solid-primitives/i18n']) return 'solid';
239
+ if (deps['ember-source'] || deps['ember-intl']) return 'ember';
240
+ if (deps.nuxt || deps['@nuxtjs/i18n']) return 'nuxt';
241
+
242
+ // Check for Rust project
243
+ const cargoTomlPath = path.join(projectRoot, 'Cargo.toml');
244
+ if (SecurityUtils.safeExistsSync(cargoTomlPath, projectRoot)) return 'rust';
245
+
246
+ // Check for Go project
247
+ const goModPath = path.join(projectRoot, 'go.mod');
248
+ if (SecurityUtils.safeExistsSync(goModPath, projectRoot)) return 'go';
230
249
 
231
250
  return 'vanilla';
232
251
  } catch (error) {
@@ -252,57 +271,110 @@ class I18nTextScanner {
252
271
 
253
272
  const frameworkSpecific = {
254
273
  react: [
255
- // React specific patterns - enhanced for i18next detection
256
274
  /children:\s*["']([^"']{2,99})["']/g,
257
275
  /dangerouslySetInnerHTML={{\s*__html:\s*["']([^"']{2,99})["']/g,
258
- // JSX text content without translation
259
276
  />([^<{][^<>{]*[^}>])</g,
260
- // Button text
261
277
  /<button[^>]*>([^<]{2,99})<\/button>/g,
262
- // Span text
263
- /<span[^>]*>([^<]{2,99})<\/span>/g
278
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
279
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g,
280
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*\{[']\s*([^'}]+\.[^'}]+)\s*[']/g
264
281
  ],
265
282
  vue: [
266
- // Vue specific patterns - enhanced for vue-i18n detection
267
283
  /v-text=["']([^"']{2,99})["']/g,
268
284
  /v-html=["']([^"']{2,99})["']/g,
269
- // Vue template text
270
285
  />([^<{][^<>{]*[^}>])</g,
271
- // Button text
272
286
  /<button[^>]*>([^<]{2,99})<\/button>/g,
273
- // Span text
274
- /<span[^>]*>([^<]{2,99})<\/span>/g
287
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
288
+ /\$t\(["']([^"']{2,99})["']\)/g,
289
+ /v-t=["']([^"']{2,99})["']/g
275
290
  ],
276
291
  angular: [
277
- // Angular specific patterns - enhanced for ngx-translate detection
278
292
  /\[innerHTML\]=["']([^"']{2,99})["']/g,
279
293
  /\[textContent\]=["']([^"']{2,99})["']/g,
280
- // Angular template text
281
294
  />([^<{][^<>{]*[^}>])</g,
282
- // Button text
283
295
  /<button[^>]*>([^<]{2,99})<\/button>/g,
284
- // Span text
285
- /<span[^>]*>([^<]{2,99})<\/span>/g
296
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
297
+ /i18n=["']([^"']{2,99})["']/g,
298
+ /\[attr\.title\]=["']([^"']{2,99})["']/g
299
+ ],
300
+ next: [
301
+ /children:\s*["']([^"']{2,99})["']/g,
302
+ />([^<{][^<>{]*[^}>])</g,
303
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
304
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
305
+ ],
306
+ svelte: [
307
+ />([^<{][^<>{]*[^}>])</g,
308
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
309
+ /\$_\(["']([^"']{2,99})["']\)/g,
310
+ /t\.set\(["']([^"']{2,99})["']/g,
311
+ /\{#if[^}]*\}([^<]{2,99})\{\/if\}/g
312
+ ],
313
+ astro: [
314
+ />([^<{][^<>{]*[^}>])</g,
315
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
316
+ /t\(["']([^"']{2,99})["']\)/g
317
+ ],
318
+ remix: [
319
+ /children:\s*["']([^"']{2,99})["']/g,
320
+ />([^<{][^<>{]*[^}>])</g,
321
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
322
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
323
+ ],
324
+ qwik: [
325
+ />([^<{][^<>{]*[^}>])</g,
326
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
327
+ /t\(["']([^"']{2,99})["']\)/g,
328
+ /useTranslate\(\)/g
329
+ ],
330
+ solid: [
331
+ />([^<{][^<>{]*[^}>])</g,
332
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
333
+ /t\(["']([^"']{2,99})["']\)/g,
334
+ /useI18n\(\)\.t\(["']([^"']{2,99})["']\)/g
335
+ ],
336
+ ember: [
337
+ />([^<{][^<>{]*[^}>])</g,
338
+ /\{\{t\s+["']([^"']{2,99})["']\s*\}\}/g,
339
+ /\{\{intl\.t\s+["']([^"']{2,99})["']\s*\}\}/g
340
+ ],
341
+ gatsby: [
342
+ /children:\s*["']([^"']{2,99})["']/g,
343
+ />([^<{][^<>{]*[^}>])</g,
344
+ /<(?:Trans)[\s\S]*?\bi18nKey\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
286
345
  ],
287
346
  django: [
288
- // Django template patterns
289
347
  /\{\%\s*trans\s+["']([^"']{2,99})["']\s*%\}/g,
290
348
  /\{\%\s*blocktrans\s*%\}([^%]{2,99})\{\%\s*endblocktrans\s*%\}/g,
291
349
  /{{\s*_["']([^"']{2,99})["']\s*}}/g,
292
350
  /{{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g
293
351
  ],
294
352
  flask: [
295
- // Flask/Jinja2 template patterns
296
353
  /\{\{\s*_["']([^"']{2,99})["']\s*}}/g,
297
354
  /\{\{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g,
298
355
  /\{\{\s*lazy_gettext\(["']([^"']{2,99})["']\)\s*}}/g
299
356
  ],
300
357
  python: [
301
- // Python source patterns
302
358
  /gettext\(["']([^"']{2,99})["']\)/g,
303
359
  /_\(["']([^"']{2,99})["']\)/g,
304
360
  /gettext_lazy\(["']([^"']{2,99})["']\)/g,
305
361
  /lazy_gettext\(["']([^"']{2,99})["']\)/g
362
+ ],
363
+ rust: [
364
+ /bundle\.get_message\(\s*["']([^"']{2,99})["']\)/g,
365
+ /\.get_message\s*\(\s*["']([^"']{2,99})["']\)/g,
366
+ /ts!\s*\(\s*["']([^"']{2,99})["']/g,
367
+ /fluent!\s*\(\s*["']([^"']{2,99})["']/g
368
+ ],
369
+ go: [
370
+ /i18n\.Translate\([^,]+,\s*["']([^"']{2,99})["']\)/g,
371
+ /i18n\.NewMessage\([^,]+,\s*["']([^"']{2,99})["']\)/g,
372
+ /t\.Get\([^,]+,\s*["']([^"']{2,99})["']\)/g
373
+ ],
374
+ vanilla: [
375
+ /t\(["']([^"']{2,99})["']\)/g,
376
+ /i18n\.t\(["']([^"']{2,99})["']\)/g,
377
+ /translate\(["']([^"']{2,99})["']\)/g
306
378
  ]
307
379
  };
308
380
 
@@ -515,7 +587,7 @@ class I18nTextScanner {
515
587
  key: `ui.${key}`,
516
588
  original: text,
517
589
  translationKey: `t('ui.${key}')`,
518
- frameworkSpecific: this.getFrameworkSpecific(text)
590
+ frameworkSpecific: getFrameworkSuggestions(this.framework, text)
519
591
  };
520
592
  }
521
593
 
@@ -523,16 +595,51 @@ class I18nTextScanner {
523
595
  const frameworks = {
524
596
  react: {
525
597
  hook: `const { t } = useTranslation();`,
526
- usage: `{t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')}`,
527
- component: `<Trans i18nKey="ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}">${text}</Trans>`
598
+ usage: `{t('ui.${this._keySnippet(text)}')}`,
599
+ component: `<Trans i18nKey="ui.${this._keySnippet(text)}">${text}</Trans>`
600
+ },
601
+ next: {
602
+ hook: `const t = useTranslations();`,
603
+ usage: `{t('ui.${this._keySnippet(text)}')}`,
604
+ component: `<Trans i18nKey="ui.${this._keySnippet(text)}">${text}</Trans>`
528
605
  },
529
606
  vue: {
530
- directive: `{{ $t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}') }}`,
531
- method: `this.$t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
607
+ directive: `{{ $t('ui.${this._keySnippet(text)}') }}`,
608
+ method: `this.$t('ui.${this._keySnippet(text)}')`
532
609
  },
533
610
  angular: {
534
611
  pipe: `{{ '${text}' | translate }}`,
535
- service: `this.translateService.instant('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
612
+ service: `this.translateService.instant('ui.${this._keySnippet(text)}')`
613
+ },
614
+ svelte: {
615
+ store: `$_(('ui.${this._keySnippet(text)}'))`,
616
+ method: `t.set('ui.${this._keySnippet(text)}', '${text}')`
617
+ },
618
+ astro: {
619
+ import: `import { t } from 'astro-i18next';`,
620
+ usage: `{t('ui.${this._keySnippet(text)}')}`
621
+ },
622
+ remix: {
623
+ hook: `const { t } = useTranslation();`,
624
+ usage: `{t('ui.${this._keySnippet(text)}')}`,
625
+ server: `export const handle = i18next.handle;`
626
+ },
627
+ qwik: {
628
+ hook: `const t = useTranslate();`,
629
+ usage: `{t('ui.${this._keySnippet(text)}')}`
630
+ },
631
+ solid: {
632
+ hook: `const [t] = useI18n();`,
633
+ usage: `{t('ui.${this._keySnippet(text)}')}`
634
+ },
635
+ ember: {
636
+ template: `{{t 'ui.${this._keySnippet(text)}'}}`,
637
+ helper: `this.intl.t('ui.${this._keySnippet(text)}')`
638
+ },
639
+ gatsby: {
640
+ hook: `const { t } = useTranslation();`,
641
+ usage: `{t('ui.${this._keySnippet(text)}')}`,
642
+ plugin: `'gatsby-plugin-react-i18next'`
536
643
  },
537
644
  django: {
538
645
  template: `{% trans '${text}' %}`,
@@ -549,14 +656,29 @@ class I18nTextScanner {
549
656
  underscore: `from gettext import gettext as _\n_('${text}')`,
550
657
  lazy: `from gettext import gettext_lazy as _\n_('${text}')`
551
658
  },
659
+ rust: {
660
+ fluent: `bundle.get_message("ui_${this._keySnippet(text)}")`,
661
+ gettext: `gettext("ui_${this._keySnippet(text)}")`
662
+ },
663
+ go: {
664
+ translate: `i18n.Translate("ui_${this._keySnippet(text)}")`,
665
+ message: `i18n.NewMessage("ui_${this._keySnippet(text)}")`
666
+ },
552
667
  vanilla: {
553
- generic: `t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
668
+ generic: `t('ui.${this._keySnippet(text)}')`
554
669
  }
555
670
  };
556
671
 
557
672
  return frameworks[this.framework] || frameworks.vanilla;
558
673
  }
559
674
 
675
+ _keySnippet(text) {
676
+ return text.toLowerCase()
677
+ .replace(/[^a-z0-9\s]/g, '')
678
+ .replace(/\s+/g, '_')
679
+ .substring(0, 40);
680
+ }
681
+
560
682
  async scanDirectory(dir, options = {}) {
561
683
  const {
562
684
  patterns = [],
@@ -571,7 +693,7 @@ class I18nTextScanner {
571
693
  }
572
694
 
573
695
  const allResults = [];
574
- const extensions = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.html', '.svelte', '.py', '.pyx', '.pyi'];
696
+ const extensions = [...SCANNER_EXTENSIONS];
575
697
 
576
698
  const scanRecursive = (currentDir) => {
577
699
  const items = SecurityUtils.safeReaddirSync(currentDir, path.dirname(currentDir), { withFileTypes: true });
@@ -733,7 +855,7 @@ class I18nTextScanner {
733
855
  } else if (fwPref && fwPref !== 'auto') {
734
856
  this.framework = fwPref;
735
857
  } else if (fwDetectEnabled) {
736
- const detected = this.detectFramework(process.cwd());
858
+ const detected = detectProjectFramework(process.cwd());
737
859
  this.framework = detected || fwFallback;
738
860
  } else {
739
861
  this.framework = fwFallback;
@@ -759,7 +881,7 @@ class I18nTextScanner {
759
881
  console.log(this.t('scanner.starting', { framework: this.framework }));
760
882
  console.log(this.t('scanner.sourceDirectory', { sourceDir: this.sourceDir }));
761
883
 
762
- const patterns = this.getFrameworkPatterns(this.framework);
884
+ const patterns = getFrameworkPatterns(this.framework);
763
885
  const exclusions = this.config.exclude || ['node_modules', '.git', 'dist', 'build'];
764
886
  const minLength = this.config.minLength || 3;
765
887
  const maxLength = this.config.maxLength || 100;
@@ -1141,7 +1141,7 @@ Generated: ${new Date().toISOString()}
1141
1141
  const key = keyValueMatch[1];
1142
1142
  const value = keyValueMatch[2];
1143
1143
 
1144
- if (key === 'source-dir' || key === 's') {
1144
+ if (key === 'source-dir' || key === 'locales-dir' || key === 'i18n-dir' || key === 's') {
1145
1145
  options['source-dir'] = value;
1146
1146
  options.s = value;
1147
1147
  options.sourceDirExplicit = true;
@@ -1162,7 +1162,7 @@ Generated: ${new Date().toISOString()}
1162
1162
  options.threshold = numValue;
1163
1163
  options.t = numValue;
1164
1164
  }
1165
- } else if (key === 'source-language') {
1165
+ } else if (key === 'source-language' || key === 'source-locale') {
1166
1166
  options['source-language'] = value;
1167
1167
  } else if (key === 'detailed' || key === 'd') {
1168
1168
  options.detailed = value.toLowerCase() !== 'false';
@@ -1184,7 +1184,7 @@ Generated: ${new Date().toISOString()}
1184
1184
  const key = match[1];
1185
1185
  const nextArg = args[i + 1];
1186
1186
 
1187
- if (key === 'source-dir' || key === 's') {
1187
+ if (key === 'source-dir' || key === 'locales-dir' || key === 'i18n-dir' || key === 's') {
1188
1188
  options['source-dir'] = nextArg || options['source-dir'];
1189
1189
  options.s = options['source-dir'];
1190
1190
  options.sourceDirExplicit = true;
@@ -1216,7 +1216,7 @@ Generated: ${new Date().toISOString()}
1216
1216
  options.t = value;
1217
1217
  }
1218
1218
  if (nextArg && !nextArg.startsWith('-') && !isNaN(parseInt(nextArg))) i++;
1219
- } else if (key === 'source-language') {
1219
+ } else if (key === 'source-language' || key === 'source-locale') {
1220
1220
  options['source-language'] = nextArg || options['source-language'];
1221
1221
  if (nextArg && !nextArg.startsWith('-')) i++;
1222
1222
  } else if (key === 'detailed' || key === 'd') {
@@ -1257,12 +1257,15 @@ I18NTK Sizing Analysis Tool
1257
1257
  Usage: i18ntk-sizing [options]
1258
1258
 
1259
1259
  Options:
1260
- -s, --source-dir <dir> Source directory containing translation files (default: ./locales)
1260
+ -s, --locales-dir <dir> Locale directory containing translation files (default: ./locales)
1261
+ --i18n-dir <dir> Alias for --locales-dir
1262
+ --source-dir <dir> Legacy alias for --locales-dir
1261
1263
  -l, --languages <langs> Comma-separated list of languages to analyze
1262
1264
  -o, --output-report Generate detailed sizing report (default: true)
1263
1265
  -f, --format <format> Output format: json, csv, table (default: table)
1264
1266
  -t, --threshold <number> Size difference threshold for warnings (%) (default: 50)
1265
- --source-language <code> Source language baseline for comparisons (default: en)
1267
+ --source-locale <code> Source locale baseline for comparisons (default: en)
1268
+ --source-language <code> Legacy alias for --source-locale
1266
1269
  -d, --detailed Generate detailed report with more information
1267
1270
  --detailed-keys Show detailed key-level analysis
1268
1271
  --predict-expansion Predict UI layout expansion risk per language
@@ -56,11 +56,14 @@ class I18nSummaryReporter {
56
56
  const uiLanguage = this.config.uiLanguage || 'en';
57
57
  loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
58
58
 
59
- this.sourceDir = this.config.sourceDir;
60
-
61
- // Validate source directory
62
- const { validateSourceDir } = require('../utils/config-helper');
63
- validateSourceDir(this.sourceDir, 'i18ntk-summary');
59
+ this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
60
+ this.config.sourceDir = this.sourceDir;
61
+
62
+ if (!SecurityUtils.safeExistsSync(this.sourceDir, process.cwd())) {
63
+ const err = new Error(`Locale directory not found: ${this.sourceDir}`);
64
+ err.exitCode = 2;
65
+ throw err;
66
+ }
64
67
  } catch (error) {
65
68
  console.error(`Error initializing summary reporter: ${error.message}`);
66
69
  throw error;
@@ -523,7 +526,10 @@ class I18nSummaryReporter {
523
526
  report.push(t('summary.languagesCount', {count: this.stats.languages.length}));
524
527
  report.push(t('summary.totalFiles', {count: this.stats.totalFiles}));
525
528
  report.push(t('summary.totalKeys', {count: this.stats.totalKeys}));
526
- report.push(t('summary.avgKeysPerLanguage', {count: Math.round(this.stats.totalKeys / this.stats.languages.length)}));
529
+ const averageKeysPerLanguage = this.stats.languages.length > 0
530
+ ? Math.round(this.stats.totalKeys / this.stats.languages.length)
531
+ : 0;
532
+ report.push(t('summary.avgKeysPerLanguage', {count: averageKeysPerLanguage}));
527
533
 
528
534
  // Calculate total size
529
535
  const totalSize = Object.values(this.stats.folderSizes).reduce((sum, size) => sum + size, 0);
@@ -666,8 +672,13 @@ class I18nSummaryReporter {
666
672
  }
667
673
  report.push('');
668
674
  }
669
- } else {
670
- report.push(t('summary.noIssuesFound'));
675
+ } else if (this.stats.languages.length === 0) {
676
+ report.push('⚠️ NO LOCALE LANGUAGES FOUND');
677
+ report.push('='.repeat(30));
678
+ report.push('No locale language files or directories were found in the selected locale root.');
679
+ report.push('');
680
+ } else {
681
+ report.push(t('summary.noIssuesFound'));
671
682
  report.push('='.repeat(30));
672
683
  report.push(t('summary.allFilesConsistent'));
673
684
  report.push('');
@@ -795,13 +806,20 @@ class I18nSummaryReporter {
795
806
  const args = process.argv.slice(2);
796
807
  const parsed = {};
797
808
 
798
- args.forEach(arg => {
799
- if (arg.startsWith('--')) {
800
- const [key, value] = arg.substring(2).split('=');
801
- if (key === 'source-dir') {
802
- parsed.sourceDir = value;
803
- } else if (key === 'source-language') {
804
- parsed.sourceLanguage = value;
809
+ for (let i = 0; i < args.length; i++) {
810
+ const arg = args[i];
811
+ if (arg.startsWith('--')) {
812
+ const [key, rawValue] = arg.substring(2).split('=');
813
+ const value = rawValue !== undefined
814
+ ? rawValue
815
+ : (args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true);
816
+ if (key === 'source-dir' || key === 'locales-dir' || key === 'i18n-dir') {
817
+ parsed.sourceDir = value;
818
+ parsed.i18nDir = value;
819
+ } else if (key === 'source-language' || key === 'source-locale') {
820
+ parsed.sourceLanguage = value;
821
+ } else if (key === 'code-dir' || key === 'source-code-dir') {
822
+ parsed.codeDir = value;
805
823
  } else if (key === 'output-file') {
806
824
  parsed.outputFile = value;
807
825
  } else if (key === 'verbose') {
@@ -813,10 +831,10 @@ class I18nSummaryReporter {
813
831
  } else if (key === 'no-prompt') {
814
832
  parsed.noPrompt = true;
815
833
  } else if (key === 'help') {
816
- parsed.help = true;
817
- }
818
- }
819
- });
834
+ parsed.help = true;
835
+ }
836
+ }
837
+ }
820
838
 
821
839
  return parsed;
822
840
  }
@@ -827,14 +845,16 @@ class I18nSummaryReporter {
827
845
  const { fromMenu = false, noPrompt = false } = options;
828
846
 
829
847
  // Always use unified configuration to respect settings
830
- const baseConfig = await getUnifiedConfig('summary', args);
831
- this.config = { ...this.config, ...baseConfig };
848
+ const baseConfig = await getUnifiedConfig('summary', args);
849
+ this.config = { ...this.config, ...baseConfig };
832
850
 
833
851
  const uiLanguage = this.config.uiLanguage || 'en';
834
852
  loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
835
- if (!this.config.sourceDir) {
836
- this.config.sourceDir = this.detectI18nDirectory();
837
- }
853
+ if (!this.config.sourceDir) {
854
+ this.config.sourceDir = this.detectI18nDirectory();
855
+ }
856
+ this.config.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
857
+ this.sourceDir = this.config.sourceDir;
838
858
 
839
859
  if (!fromMenu) {
840
860
  // Check admin authentication for sensitive operations (only when called directly)
@@ -866,10 +886,10 @@ class I18nSummaryReporter {
866
886
  }
867
887
 
868
888
  // Validate source directory exists
869
- if (!SecurityUtils.safeExistsSync(this.config.sourceDir, this.config.sourceDir)) {
870
- console.error(t('summary.sourceDirectoryDoesNotExist', { sourceDir: this.config.sourceDir }));
871
- process.exit(1);
872
- }
889
+ if (!SecurityUtils.safeExistsSync(this.config.sourceDir, process.cwd())) {
890
+ console.error(t('summary.sourceDirectoryDoesNotExist', { sourceDir: this.config.sourceDir }));
891
+ process.exit(2);
892
+ }
873
893
  }
874
894
 
875
895
  console.log(t('summary.i18nSummaryReportGenerator'));
@@ -992,17 +1012,19 @@ class I18nSummaryReporter {
992
1012
 
993
1013
  if (totalIssues > 0) {
994
1014
  console.log(t('summary.foundIssues', {count: totalIssues}));
995
- } else {
996
- console.log(t('summary.noIssuesConsole'));
997
- }
1015
+ } else if (this.stats.languages.length === 0) {
1016
+ console.log('⚠️ No locale languages found in the selected locale root.');
1017
+ } else {
1018
+ console.log(t('summary.noIssuesConsole'));
1019
+ }
998
1020
 
999
1021
  } catch (error) {
1000
1022
  console.error(t('summary.errorDuringAnalysis', { error: error.message }));
1001
1023
  if (args.verbose) {
1002
1024
  console.error(error.stack);
1003
1025
  }
1004
- process.exit(1);
1005
- }
1026
+ process.exit(error.exitCode || 1);
1027
+ }
1006
1028
  process.exit(0);
1007
1029
  }
1008
1030
  }
@@ -1014,10 +1036,10 @@ if (require.main === module) {
1014
1036
  await reporter.initialize();
1015
1037
  await reporter.run();
1016
1038
  }
1017
- main().catch(error => {
1018
- console.error('Error in i18ntk-summary:', error.message);
1019
- process.exit(1);
1020
- });
1021
- }
1039
+ main().catch(error => {
1040
+ console.error('Error in i18ntk-summary:', error.message);
1041
+ process.exit(error.exitCode || 1);
1042
+ });
1043
+ }
1022
1044
 
1023
- module.exports = I18nSummaryReporter;
1045
+ module.exports = I18nSummaryReporter;
@@ -46,6 +46,7 @@ const JsonOutput = require('../utils/json-output');
46
46
  const SetupEnforcer = require('../utils/setup-enforcer');
47
47
  const { resolveUsageSourceDir } = require('../utils/usage-source');
48
48
  const { analyzeSourceForUsageInsights } = require('../utils/usage-insights');
49
+ const { isInteractive } = require('../utils/prompt-helper');
49
50
 
50
51
  // Ensure setup is complete before running
51
52
  (async () => {
@@ -207,9 +208,10 @@ class I18nUsageAnalyzer {
207
208
 
208
209
  // Normalize CLI arguments to handle both camelCase and hyphenated flags
209
210
  normalizeArgs(a) {
210
- return {
211
- sourceDir: a.sourceDir ?? a['source-dir'],
212
- i18nDir: a.i18nDir ?? a['i18n-dir'],
211
+ return {
212
+ sourceDir: a.sourceDir ?? a['source-dir'] ?? a['code-dir'] ?? a['source-code-dir'],
213
+ i18nDir: a.i18nDir ?? a['i18n-dir'] ?? a['locales-dir'],
214
+ sourceLanguage: a.sourceLanguage ?? a['source-language'] ?? a['source-locale'],
213
215
  outputReport: a.outputReport ?? a['output-report'],
214
216
  outputDir: a.outputDir ?? a['output-dir'],
215
217
  uiLanguage: a.uiLanguage ?? a['ui-language'],
@@ -231,11 +233,18 @@ class I18nUsageAnalyzer {
231
233
  const args = process.argv.slice(2);
232
234
  const parsed = {};
233
235
 
234
- for (const arg of args) {
235
- if (arg.startsWith('--')) {
236
- const [key, value] = arg.substring(2).split('=');
237
- if (value !== undefined) {
238
- parsed[key] = value;
236
+ for (let i = 0; i < args.length; i++) {
237
+ const arg = args[i];
238
+ if (arg.startsWith('--')) {
239
+ let [key, value] = arg.substring(2).split('=');
240
+ if (value === undefined && args[i + 1] && !args[i + 1].startsWith('-')) {
241
+ value = args[++i];
242
+ }
243
+ if (key === 'code-dir' || key === 'source-code-dir') key = 'source-dir';
244
+ if (key === 'locales-dir') key = 'i18n-dir';
245
+ if (key === 'source-locale') key = 'source-language';
246
+ if (value !== undefined) {
247
+ parsed[key] = value;
239
248
  } else {
240
249
  parsed[key] = true;
241
250
  }
@@ -513,11 +522,15 @@ class I18nUsageAnalyzer {
513
522
  this.sourceDir = path.resolve(args.sourceDir);
514
523
  }
515
524
 
516
- if (args.i18nDir) {
517
- this.config.i18nDir = args.i18nDir;
518
- this.i18nDir = path.resolve(args.i18nDir);
519
- this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
520
- }
525
+ if (args.i18nDir) {
526
+ this.config.i18nDir = args.i18nDir;
527
+ this.i18nDir = path.resolve(args.i18nDir);
528
+ this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
529
+ }
530
+ if (args.sourceLanguage) {
531
+ this.config.sourceLanguage = args.sourceLanguage;
532
+ this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
533
+ }
521
534
 
522
535
  if (this.sourceDir || this.i18nDir) {
523
536
  await configManager.updateConfig({
@@ -722,9 +735,9 @@ class I18nUsageAnalyzer {
722
735
 
723
736
  console.log('\n' + t('usage.analysisCompletedSuccessfully'));
724
737
 
725
- if (require.main === module && !args.noPrompt) {
726
- await this.prompt('\nPress Enter to continue...');
727
- }
738
+ if (require.main === module && isInteractive(args)) {
739
+ await this.prompt('\nPress Enter to continue...');
740
+ }
728
741
  this.closeReadline();
729
742
 
730
743
  } catch (error) {