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.
@@ -71,9 +71,16 @@ class I18nAnalyzer {
71
71
  const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
72
72
  loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
73
73
 
74
- this.sourceDir = this.config.sourceDir;
75
- this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
76
- this.outputDir = this.config.outputDir;
74
+ this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
75
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
76
+ this.outputDir = this.config.outputDir;
77
+
78
+ const explicitLocaleDir = args.i18nDir || (!args.i18nDir && args.sourceDir ? args.sourceDir : null);
79
+ if (explicitLocaleDir && !SecurityUtils.safeExistsSync(this.sourceDir, process.cwd())) {
80
+ const err = new Error(`Locale directory not found: ${this.sourceDir}`);
81
+ err.exitCode = 2;
82
+ throw err;
83
+ }
77
84
 
78
85
  // Validate source directory exists
79
86
  const { validateSourceDir } = require('../utils/config-helper');
@@ -710,10 +717,32 @@ try {
710
717
  }
711
718
  }
712
719
 
713
- // Show help message
714
- showHelp() {
715
- console.log(t('analyze.help_message'));
716
- }
720
+ // Show help message
721
+ showHelp() {
722
+ console.log(t('analyze.help_message'));
723
+ }
724
+
725
+ provideSetupGuidance() {
726
+ return [
727
+ 'Setup guidance:',
728
+ `- Ensure the locale root exists: ${this.sourceDir}`,
729
+ `- Ensure the source locale exists: ${path.join(this.sourceDir, this.config.sourceLanguage)}`,
730
+ '- Add at least one target locale, for example de/common.json or de.json.',
731
+ '- Prefer --locales-dir for locale files. Use --code-dir for application source files.'
732
+ ].join('\n');
733
+ }
734
+
735
+ detectStructureType() {
736
+ const sourceLocaleDir = path.join(this.sourceDir, this.config.sourceLanguage);
737
+ const sourceLocaleFile = path.join(this.sourceDir, `${this.config.sourceLanguage}.json`);
738
+ if (SecurityUtils.safeExistsSync(sourceLocaleFile, this.sourceDir)) {
739
+ return { type: 'monolith' };
740
+ }
741
+ if (SecurityUtils.safeExistsSync(sourceLocaleDir, this.sourceDir)) {
742
+ return { type: 'directory' };
743
+ }
744
+ return { type: 'unknown' };
745
+ }
717
746
 
718
747
  // Main analyze method
719
748
  async analyze() {
@@ -737,9 +766,9 @@ try {
737
766
 
738
767
  const languages = this.getAvailableLanguages();
739
768
 
740
- if (languages.length === 0) {
741
- const error = t('analyze.noLanguages') || '⚠️ No target languages found.';
742
- const guidance = this.provideSetupGuidance();
769
+ if (languages.length === 0) {
770
+ const error = t('analyze.noLanguages') || '⚠️ No target languages found.';
771
+ const guidance = this.provideSetupGuidance();
743
772
 
744
773
  if (args.json) {
745
774
  jsonOutput.setStatus('error', error);
@@ -751,10 +780,10 @@ try {
751
780
  console.log(JSON.stringify(jsonOutput.data, null, args.indent || 2));
752
781
  return;
753
782
  }
754
- console.log(error);
755
- console.log('\n' + guidance);
756
- return;
757
- }
783
+ console.log(error);
784
+ console.log('\n' + guidance);
785
+ return { success: true, warnings: 1, message: error };
786
+ }
758
787
 
759
788
  if (!args.json) {
760
789
  console.log(t('analyze.foundLanguages', { count: languages.length, languages: languages.join(', ') }) || `📋 Found ${languages.length} languages to analyze: ${languages.join(', ')}`);
@@ -773,8 +802,11 @@ try {
773
802
  const report = this.generateLanguageReport(analysis);
774
803
 
775
804
  // Save report
776
- const reportPath = await this.saveReport(language, report);
777
- const processedCount = results.length + 1;
805
+ const reportPath = await this.saveReport(language, report);
806
+ if (!reportPath) {
807
+ throw new Error(`Failed to save analysis report for ${language}`);
808
+ }
809
+ const processedCount = results.length + 1;
778
810
 
779
811
  if (!args.json) {
780
812
  console.log(t('analyze.completed', { language }) || `✅ Analysis completed for ${language}`);
@@ -842,9 +874,9 @@ try {
842
874
  console.log(t('analyze.finished') || '\n✅ Analysis completed successfully!');
843
875
 
844
876
  // Only prompt for input if running standalone and not in no-prompt mode
845
- if (require.main === module && !this.noPrompt) {
846
- await this.prompt('\nPress Enter to continue...');
847
- }
877
+ if (require.main === module && !this.noPrompt && process.env.CI !== 'true' && process.stdin.isTTY && process.stdout.isTTY) {
878
+ await this.prompt('\nPress Enter to continue...');
879
+ }
848
880
  this.closeReadline();
849
881
 
850
882
  return results;
@@ -881,8 +913,8 @@ try {
881
913
  const uiLanguage = this.config.uiLanguage || 'en';
882
914
  loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
883
915
 
884
- this.sourceDir = this.config.sourceDir;
885
- this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
916
+ this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
917
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
886
918
  this.outputDir = this.config.outputDir;
887
919
  }
888
920
 
@@ -921,19 +953,20 @@ try {
921
953
  loadTranslations(args.uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));}
922
954
 
923
955
  // Update config if source directory is provided
924
- if (args.sourceDir) {
925
- this.config.sourceDir = args.sourceDir;
926
- this.sourceDir = path.resolve(PROJECT_ROOT, this.config.sourceDir);
927
- this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
928
- }
956
+ const localeDirArg = args.i18nDir || (!args.i18nDir && args.sourceDir ? args.sourceDir : null);
957
+ if (localeDirArg) {
958
+ this.config.i18nDir = localeDirArg;
959
+ this.sourceDir = path.resolve(PROJECT_ROOT, localeDirArg);
960
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
961
+ }
929
962
 
930
963
  if (args.outputDir) {
931
964
  this.config.outputDir = args.outputDir;
932
965
  this.outputDir = path.resolve(this.config.outputDir);
933
966
  }
934
- const execute = async () => {
935
- await this.analyze();
936
- };
967
+ const execute = async () => {
968
+ return await this.analyze();
969
+ };
937
970
 
938
971
  if (args.watch) {
939
972
  await execute();
@@ -949,19 +982,21 @@ try {
949
982
  });
950
983
  console.log('��� Watching for translation changes. Press Ctrl+C to exit.');
951
984
  } else {
952
- await execute();
953
- if (!fromMenu && require.main === module) {
954
- process.exit(0);
955
- }
956
- }
957
- } catch (error) {
958
- console.error(t('analyze.error') || '❌ Analysis failed:', error.message);
959
- this.closeReadline();
960
- if (!fromMenu && require.main === module) {
961
- process.exit(1);
962
- }
963
- }
964
- }
985
+ const result = await execute();
986
+ if (!fromMenu && require.main === module) {
987
+ process.exit(0);
988
+ }
989
+ return result;
990
+ }
991
+ } catch (error) {
992
+ console.error(t('analyze.error') || '❌ Analysis failed:', error.message);
993
+ this.closeReadline();
994
+ if (!fromMenu && require.main === module) {
995
+ process.exit(1);
996
+ }
997
+ throw error;
998
+ }
999
+ }
965
1000
 
966
1001
  async runSetupWizard() {
967
1002
  console.log('Translation Analysis Setup Wizard');
@@ -16,9 +16,10 @@ const path = require('path');
16
16
  const fs = require('fs');
17
17
  const SecurityUtils = require('../utils/security');
18
18
  const { loadTranslations, t } = require('../utils/i18n-helper');
19
- const { getGlobalReadline, closeGlobalReadline } = require('../utils/cli');
20
- const SetupEnforcer = require('../utils/setup-enforcer');
21
- const { getUnifiedConfig } = require('../utils/config-helper');
19
+ const { getGlobalReadline, closeGlobalReadline } = require('../utils/cli');
20
+ const SetupEnforcer = require('../utils/setup-enforcer');
21
+ const { getUnifiedConfig, displayHelp } = require('../utils/config-helper');
22
+ const { isInteractive } = require('../utils/prompt-helper');
22
23
 
23
24
  // Ensure setup is complete before running, except for help output.
24
25
  (async () => {
@@ -64,7 +65,7 @@ class I18nCompletionTool {
64
65
 
65
66
  const baseConfig = await getUnifiedConfig('complete', args);
66
67
  this.config = { ...baseConfig, ...(this.config || {}) };
67
- this.sourceDir = this.config.sourceDir;
68
+ this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
68
69
  this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
69
70
 
70
71
  // Validate source directory exists
@@ -99,24 +100,31 @@ class I18nCompletionTool {
99
100
  const args = process.argv.slice(2);
100
101
  const parsed = {};
101
102
 
102
- args.forEach(arg => {
103
- if (arg.startsWith('--')) {
104
- const [key, value] = arg.substring(2).split('=');
105
- if (key === 'source-dir') {
106
- parsed.sourceDir = value;
107
- } else if (key === 'source-language') {
108
- parsed.sourceLanguage = value;
109
- } else if (key === 'auto-translate') {
103
+ for (let i = 0; i < args.length; i++) {
104
+ const arg = args[i];
105
+ if (arg.startsWith('--')) {
106
+ const [key, rawValue] = arg.substring(2).split('=');
107
+ const value = rawValue !== undefined
108
+ ? rawValue
109
+ : (args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true);
110
+ if (key === 'source-dir' || key === 'locales-dir' || key === 'i18n-dir') {
111
+ parsed.sourceDir = value;
112
+ parsed.i18nDir = value;
113
+ } else if (key === 'source-language' || key === 'source-locale') {
114
+ parsed.sourceLanguage = value;
115
+ } else if (key === 'code-dir' || key === 'source-code-dir') {
116
+ parsed.codeDir = value;
117
+ } else if (key === 'auto-translate') {
110
118
  parsed.autoTranslate = true;
111
119
  } else if (key === 'dry-run') {
112
120
  parsed.dryRun = true;
113
121
  } else if (key === 'no-prompt') {
114
122
  parsed.noPrompt = true;
115
123
  } else if (key === 'help' || key === 'h') {
116
- parsed.help = true;
117
- }
118
- }
119
- });
124
+ parsed.help = true;
125
+ }
126
+ }
127
+ }
120
128
 
121
129
  return parsed;
122
130
  }
@@ -567,22 +575,24 @@ class I18nCompletionTool {
567
575
 
568
576
  const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
569
577
  loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
570
- this.sourceDir = this.config.sourceDir;
578
+ this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
571
579
  this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
572
580
  } else {
573
581
  await this.initialize();
574
582
 
575
- if (args.sourceDir) {
576
- const resolvedSourceDir = path.resolve(args.sourceDir);
577
- const validatedSourceDir = SecurityUtils.validatePath(resolvedSourceDir, process.cwd());
578
- if (!validatedSourceDir) {
579
- console.error(t("complete.invalidSourceDir", { sourceDir: args.sourceDir }));
580
- console.error(`Path validation failed source directory is outside the allowed project boundary.`);
581
- process.exit(1);
582
- }
583
- this.config.sourceDir = resolvedSourceDir;
584
- this.sourceDir = validatedSourceDir;
585
- }
583
+ const localesDirArg = args.i18nDir || args.sourceDir;
584
+ if (localesDirArg) {
585
+ const resolvedSourceDir = path.resolve(localesDirArg);
586
+ const validatedSourceDir = SecurityUtils.validatePath(resolvedSourceDir, process.cwd());
587
+ if (!validatedSourceDir) {
588
+ console.error(t("complete.invalidSourceDir", { sourceDir: localesDirArg }));
589
+ console.error(`Path validation failed — source directory is outside the allowed project boundary.`);
590
+ process.exit(1);
591
+ }
592
+ this.config.sourceDir = resolvedSourceDir;
593
+ this.config.i18nDir = resolvedSourceDir;
594
+ this.sourceDir = validatedSourceDir;
595
+ }
586
596
 
587
597
  if (args.sourceLanguage) {
588
598
  this.config.sourceLanguage = SecurityUtils.sanitizeInput(args.sourceLanguage, {
@@ -643,15 +653,24 @@ class I18nCompletionTool {
643
653
  }
644
654
 
645
655
  console.log('\n');
646
- console.log(t("complete.summaryTitle"));
647
- console.log(t("complete.separator"));
648
- console.log(t("complete.totalChanges", { totalChanges }));
649
- console.log(t("complete.languagesProcessed", { languagesProcessed: languages.length }));
650
- console.log(t("complete.missingKeysAdded", { missingKeysAdded: missingKeys.length }));
651
-
652
- if (!args.dryRun && allChanges.length > 0 && !args.noPrompt) {
653
- const rl = this.rl || this.initReadline();
654
- const answer = await this.prompt('\n' + t('complete.generateReportPrompt') + ' (Y/N): ');
656
+ console.log(t("complete.summaryTitle"));
657
+ console.log(t("complete.separator"));
658
+ const targetLocalesChanged = allChanges.length;
659
+ const filesModified = new Set(allChanges.flatMap(lang => lang.changes.map(change => `${lang.language}/${change.file}`))).size;
660
+ const filesSkipped = Math.max(0, targetLanguages.length - targetLocalesChanged);
661
+ console.log(`Locales scanned: ${languages.length}`);
662
+ console.log(`Target locales changed: ${targetLocalesChanged}`);
663
+ console.log(`Unique source keys added: ${missingKeys.length}`);
664
+ console.log(`Total key insertions: ${totalChanges}`);
665
+ console.log(`Files modified: ${args.dryRun ? 0 : filesModified}`);
666
+ console.log(`Files skipped: ${filesSkipped}`);
667
+ if (args.dryRun) {
668
+ console.log('Dry run only. No files were modified.');
669
+ }
670
+
671
+ if (!args.dryRun && allChanges.length > 0 && isInteractive(args)) {
672
+ const rl = this.rl || this.initReadline();
673
+ const answer = await this.prompt('\n' + t('complete.generateReportPrompt') + ' (Y/N): ');
655
674
 
656
675
  if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
657
676
  await this.generateReport(allChanges, languages);
@@ -22,23 +22,32 @@ function parseArgs(argv = process.argv.slice(2)) {
22
22
  help: false,
23
23
  };
24
24
 
25
- for (const raw of argv) {
25
+ for (let i = 0; i < argv.length; i++) {
26
+ const raw = argv[i];
26
27
  const arg = raw === 'report' ? '' : raw;
27
28
  if (!arg) continue;
29
+ const nextValue = () => {
30
+ if (i + 1 < argv.length && !String(argv[i + 1]).startsWith('--')) {
31
+ return argv[++i];
32
+ }
33
+ return null;
34
+ };
28
35
  if (arg === '--help' || arg === '-h') args.help = true;
29
36
  else if (arg === '--json') args.json = true;
30
37
  else if (arg === '--markdown' || arg === '--md') args.markdown = true;
31
38
  else if (arg === '--html') args.html = true;
32
39
  else if (arg.startsWith('--out=')) args.out = arg.slice('--out='.length);
33
- else if (arg === '--out') args.expectOut = true;
34
- else if (args.expectOut) {
35
- args.out = arg;
36
- args.expectOut = false;
37
- } else if (arg.startsWith('--source-dir=')) args.sourceDir = arg.slice('--source-dir='.length);
40
+ else if (arg === '--out') args.out = nextValue();
41
+ else if (arg.startsWith('--source-dir=')) args.sourceDir = arg.slice('--source-dir='.length);
42
+ else if (arg.startsWith('--code-dir=')) args.sourceDir = arg.slice('--code-dir='.length);
43
+ else if (arg.startsWith('--source-code-dir=')) args.sourceDir = arg.slice('--source-code-dir='.length);
44
+ else if (arg === '--source-dir' || arg === '--code-dir' || arg === '--source-code-dir') args.sourceDir = nextValue();
38
45
  else if (arg.startsWith('--i18n-dir=')) args.i18nDir = arg.slice('--i18n-dir='.length);
39
46
  else if (arg.startsWith('--locales-dir=')) args.i18nDir = arg.slice('--locales-dir='.length);
47
+ else if (arg === '--i18n-dir' || arg === '--locales-dir') args.i18nDir = nextValue();
40
48
  else if (arg.startsWith('--source-language=')) args.sourceLocale = arg.slice('--source-language='.length);
41
49
  else if (arg.startsWith('--source-locale=')) args.sourceLocale = arg.slice('--source-locale='.length);
50
+ else if (arg === '--source-language' || arg === '--source-locale') args.sourceLocale = nextValue();
42
51
  }
43
52
 
44
53
  if (!args.json && !args.markdown && !args.html) {
@@ -107,9 +116,13 @@ Options:
107
116
  --markdown Print/write Markdown report output
108
117
  --html Print/write HTML report output
109
118
  --out <dir> Write selected report formats into a directory
110
- --source-dir=<dir> Source code directory to scan
111
- --i18n-dir=<dir> Locale directory
112
- --source-language=<code> Source locale code (default: en)
119
+ --code-dir=<dir> Source code directory to scan
120
+ --source-code-dir=<dir> Alias for --code-dir
121
+ --source-dir=<dir> Legacy alias for --code-dir
122
+ --locales-dir=<dir> Locale directory
123
+ --i18n-dir=<dir> Alias for --locales-dir
124
+ --source-locale=<code> Source locale code (default: en)
125
+ --source-language=<code> Legacy alias for --source-locale
113
126
  `;
114
127
  }
115
128