i18ntk 2.1.0 → 2.3.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.
Files changed (73) hide show
  1. package/README.md +87 -50
  2. package/main/i18ntk-analyze.js +63 -63
  3. package/main/i18ntk-backup-class.js +37 -41
  4. package/main/i18ntk-backup.js +28 -30
  5. package/main/i18ntk-complete.js +75 -74
  6. package/main/i18ntk-doctor.js +7 -6
  7. package/main/i18ntk-fixer.js +3 -3
  8. package/main/i18ntk-init.js +49 -13
  9. package/main/i18ntk-scanner.js +2 -2
  10. package/main/i18ntk-sizing.js +36 -37
  11. package/main/i18ntk-summary.js +4 -4
  12. package/main/i18ntk-ui.js +95 -96
  13. package/main/i18ntk-usage.js +31 -19
  14. package/main/i18ntk-validate.js +78 -27
  15. package/main/manage/commands/AnalyzeCommand.js +71 -73
  16. package/main/manage/commands/CommandRouter.js +15 -12
  17. package/main/manage/commands/FixerCommand.js +94 -38
  18. package/main/manage/commands/ScannerCommand.js +2 -2
  19. package/main/manage/commands/ValidateCommand.js +87 -36
  20. package/main/manage/index.js +165 -152
  21. package/main/manage/managers/DebugMenu.js +6 -6
  22. package/main/manage/managers/InteractiveMenu.js +6 -6
  23. package/main/manage/managers/LanguageMenu.js +12 -6
  24. package/main/manage/managers/SettingsMenu.js +6 -6
  25. package/main/manage/services/AuthenticationService.js +5 -6
  26. package/main/manage/services/ConfigurationService.js +22 -34
  27. package/main/manage/services/FileManagementService.js +6 -6
  28. package/main/manage/services/InitService.js +44 -8
  29. package/main/manage/services/UsageService.js +24 -12
  30. package/package.json +21 -42
  31. package/settings/settings-cli.js +5 -5
  32. package/settings/settings-manager.js +984 -968
  33. package/ui-locales/de.json +12 -11
  34. package/ui-locales/en.json +12 -11
  35. package/ui-locales/es.json +12 -11
  36. package/ui-locales/fr.json +12 -11
  37. package/ui-locales/ja.json +12 -11
  38. package/ui-locales/ru.json +12 -11
  39. package/ui-locales/zh.json +12 -11
  40. package/utils/config-helper.js +27 -16
  41. package/utils/config-manager.js +8 -7
  42. package/utils/i18n-helper.js +161 -166
  43. package/utils/init-helper.js +3 -2
  44. package/utils/json-output.js +11 -10
  45. package/{scripts → utils}/locale-optimizer.js +61 -60
  46. package/utils/logger.js +4 -4
  47. package/utils/safe-json.js +3 -3
  48. package/utils/secure-backup.js +8 -7
  49. package/utils/setup-enforcer.js +63 -98
  50. package/main/i18ntk-go.js +0 -283
  51. package/main/i18ntk-java.js +0 -380
  52. package/main/i18ntk-js.js +0 -512
  53. package/main/i18ntk-manage.js +0 -1694
  54. package/main/i18ntk-php.js +0 -462
  55. package/main/i18ntk-py.js +0 -379
  56. package/main/i18ntk-settings.js +0 -23
  57. package/main/manage/index-fixed.js +0 -1447
  58. package/scripts/build-lite.js +0 -279
  59. package/scripts/deprecate-versions.js +0 -317
  60. package/scripts/export-translations.js +0 -84
  61. package/scripts/fix-all-i18n.js +0 -236
  62. package/scripts/fix-and-purify-i18n.js +0 -233
  63. package/scripts/fix-locale-control-chars.js +0 -110
  64. package/scripts/lint-locales.js +0 -80
  65. package/scripts/prepublish-dev.js +0 -221
  66. package/scripts/prepublish.js +0 -362
  67. package/scripts/security-check.js +0 -117
  68. package/scripts/sync-translations.js +0 -151
  69. package/scripts/sync-ui-locales.js +0 -20
  70. package/scripts/validate-all-translations.js +0 -195
  71. package/scripts/verify-deprecations.js +0 -157
  72. package/scripts/verify-translations.js +0 -63
  73. package/utils/security-fixed.js +0 -609
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * I18NTK TRANSLATION VALIDATION TOOLKIT
@@ -60,7 +60,7 @@ const SetupEnforcer = require('../utils/setup-enforcer');
60
60
  }
61
61
  })();
62
62
 
63
- loadTranslations( 'en', path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales'));
63
+ loadTranslations('en', path.resolve(__dirname, '..', 'ui-locales'));
64
64
 
65
65
  class I18nValidator {
66
66
  constructor(config = {}) {
@@ -87,7 +87,7 @@ class I18nValidator {
87
87
  this.config = { ...baseConfig, ...(this.config || {}) };
88
88
 
89
89
  const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
90
- loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales'));
90
+ loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
91
91
 
92
92
  SecurityUtils.logSecurityEvent(
93
93
  'I18n validator initializing',
@@ -186,18 +186,31 @@ class I18nValidator {
186
186
  this.warnings.push({ message, details, type: 'warning' });
187
187
  }
188
188
 
189
- // Get all available languages
190
- getAvailableLanguages() {
191
- try {
192
- if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
193
- throw new Error(`Source directory not found: ${this.sourceDir}`);
194
- }
195
-
196
- const languages = fs.readdirSync(this.sourceDir)
197
- .filter(item => {
198
- const itemPath = path.join(this.sourceDir, item);
199
- return fs.statSync(itemPath).isDirectory() && item !== this.config.sourceLanguage;
200
- });
189
+ // Get all available languages
190
+ isExcludedLanguageDirectory(name) {
191
+ if (!name || typeof name !== 'string') return true;
192
+ const lowered = name.toLowerCase();
193
+ return lowered.startsWith('backup-') ||
194
+ lowered === 'backup' ||
195
+ lowered === 'backups' ||
196
+ lowered === 'i18ntk-backups' ||
197
+ lowered === 'reports' ||
198
+ lowered === 'i18ntk-reports';
199
+ }
200
+
201
+ getAvailableLanguages() {
202
+ try {
203
+ if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
204
+ throw new Error(`Source directory not found: ${this.sourceDir}`);
205
+ }
206
+
207
+ const languages = fs.readdirSync(this.sourceDir)
208
+ .filter(item => {
209
+ const itemPath = path.join(this.sourceDir, item);
210
+ return fs.statSync(itemPath).isDirectory() &&
211
+ item !== this.config.sourceLanguage &&
212
+ !this.isExcludedLanguageDirectory(item);
213
+ });
201
214
 
202
215
  return languages;
203
216
  } catch (error) {
@@ -620,10 +633,42 @@ class I18nValidator {
620
633
  return warnings;
621
634
  }
622
635
 
623
- // Show help message
624
- showHelp() {
625
- console.log(t('validate.help_message'));
626
- }
636
+ // Show help message
637
+ showHelp() {
638
+ console.log(t('validate.help_message'));
639
+ }
640
+
641
+ saveValidationSummaryReport(results = {}, success = true) {
642
+ try {
643
+ const outputDir = path.resolve(this.config.outputDir || './i18ntk-reports');
644
+ SecurityUtils.safeMkdirSync(outputDir, process.cwd(), { recursive: true });
645
+
646
+ const timestamp = new Date().toISOString();
647
+ const safeTimestamp = timestamp.replace(/[:.]/g, '-');
648
+ const reportPath = path.join(outputDir, `validation-summary-${safeTimestamp}.txt`);
649
+
650
+ const lines = [];
651
+ lines.push('I18NTK Validation Summary');
652
+ lines.push('========================');
653
+ lines.push(`Generated: ${timestamp}`);
654
+ lines.push(`Result: ${success ? 'PASS' : 'FAIL'}`);
655
+ lines.push(`Errors: ${this.errors.length}`);
656
+ lines.push(`Warnings: ${this.warnings.length}`);
657
+ lines.push('');
658
+ lines.push('Language Results');
659
+ lines.push('----------------');
660
+
661
+ Object.entries(results).forEach(([language, validation]) => {
662
+ const summary = validation?.summary || {};
663
+ lines.push(`${language}: ${summary.percentage || 0}% (${summary.translatedKeys || 0}/${summary.totalKeys || 0})`);
664
+ });
665
+
666
+ SecurityUtils.safeWriteFileSync(reportPath, lines.join('\n') + '\n', process.cwd(), 'utf8');
667
+ return reportPath;
668
+ } catch (error) {
669
+ return null;
670
+ }
671
+ }
627
672
 
628
673
  // Main validation process
629
674
  async validate() {
@@ -854,11 +899,16 @@ class I18nValidator {
854
899
  console.log(t('validate.considerRunningUsageAnalysis'));
855
900
  }
856
901
 
857
- // Exit with appropriate code
858
- const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
859
-
860
- return {
861
- success,
902
+ // Exit with appropriate code
903
+ const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
904
+ const summaryReportPath = this.saveValidationSummaryReport(results, success);
905
+ if (summaryReportPath) {
906
+ console.log('');
907
+ console.log(`📄 Validation summary report saved: ${summaryReportPath}`);
908
+ }
909
+
910
+ return {
911
+ success,
862
912
  errors: this.errors.length,
863
913
  warnings: this.warnings.length,
864
914
  results
@@ -892,7 +942,8 @@ class I18nValidator {
892
942
  this.config = { ...baseConfig, ...(this.config || {}) };
893
943
 
894
944
  const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
895
- loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales'));this.sourceDir = this.config.sourceDir;
945
+ loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
946
+ this.sourceDir = this.config.sourceDir;
896
947
  this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
897
948
  } else {
898
949
  await this.initialize();
@@ -983,7 +1034,7 @@ if (require.main === module) {
983
1034
  // Initialize translations for CLI usage
984
1035
  const config = configManager.getConfig();
985
1036
  const uiLanguage = config.language || 'en';
986
- loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales'));
1037
+ loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
987
1038
 
988
1039
  SecurityUtils.logSecurityEvent(t('validate.scriptExecution'), 'info', {
989
1040
  script: 'i18ntk-validate.js',
@@ -1050,4 +1101,4 @@ if (require.main === module) {
1050
1101
  process.exit(ExitCodes.CONFIG_ERROR);
1051
1102
  }
1052
1103
  })();
1053
- }
1104
+ }
@@ -7,16 +7,17 @@
7
7
  * Contains embedded business logic from I18nAnalyzer.
8
8
  */
9
9
 
10
- const path = require('path');
11
- const cliHelper = require('../../../utils/cli-helper');
12
- const { loadTranslations, t } = require('../../../utils/i18n-helper');
13
- const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../../../utils/config-helper');
10
+ const path = require('path');
11
+ const cliHelper = require('../../../utils/cli-helper');
12
+ const { loadTranslations, t } = require('../../../utils/i18n-helper');
13
+ const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../../../utils/config-helper');
14
14
  const SecurityUtils = require('../../../utils/security');
15
15
  const AdminCLI = require('../../../utils/admin-cli');
16
+ const AdminAuth = require('../../../utils/admin-auth');
16
17
  const watchLocales = require('../../../utils/watch-locales');
17
18
  const JsonOutput = require('../../../utils/json-output');
18
-
19
- loadTranslations('en', path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
19
+
20
+ loadTranslations('en', path.resolve(__dirname, '../../../ui-locales'));
20
21
 
21
22
  const PROJECT_ROOT = process.cwd();
22
23
 
@@ -74,15 +75,14 @@ class AnalyzeCommand {
74
75
  this.config = { ...baseConfig, ...(this.config || {}) };
75
76
 
76
77
  const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
77
- loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
78
+ loadTranslations(uiLanguage, path.resolve(__dirname, '../../../ui-locales'));
78
79
 
79
80
  this.sourceDir = this.config.sourceDir;
80
81
  this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
81
82
  this.outputDir = this.config.outputDir;
82
83
 
83
84
  // Validate source directory exists
84
- const { validateSourceDir } = require('../../../utils/config-helper');
85
- validateSourceDir(this.sourceDir, 'i18ntk-analyze');
85
+ validateSourceDir(this.sourceDir, 'i18ntk-analyze');
86
86
 
87
87
  } catch (error) {
88
88
  console.error(`Fatal analysis error: ${error.message}`);
@@ -149,44 +149,44 @@ class AnalyzeCommand {
149
149
  }
150
150
  }
151
151
 
152
- // Get all available languages
153
- isValidLanguageCode(code) {
154
- if (!code || typeof code !== 'string') return false;
155
- return /^[a-z]{2}(?:-[A-Za-z0-9]{2,8})*$/i.test(code.trim());
156
- }
157
-
158
- isExcludedLanguageDirectory(name) {
159
- if (!name || typeof name !== 'string') return true;
160
- const lowered = name.toLowerCase();
161
- return lowered.startsWith('backup-') || lowered === 'backup' || lowered === 'reports' || lowered === 'i18ntk-reports';
162
- }
163
-
164
- getAvailableLanguages() {
165
- try {
166
- const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
167
- if (!items) {
168
- console.error('Error reading source directory: Unable to access directory');
152
+ // Get all available languages
153
+ isValidLanguageCode(code) {
154
+ if (!code || typeof code !== 'string') return false;
155
+ return /^[a-z]{2}(?:-[A-Za-z0-9]{2,8})*$/i.test(code.trim());
156
+ }
157
+
158
+ isExcludedLanguageDirectory(name) {
159
+ if (!name || typeof name !== 'string') return true;
160
+ const lowered = name.toLowerCase();
161
+ return lowered.startsWith('backup-') || lowered === 'backup' || lowered === 'reports' || lowered === 'i18ntk-reports';
162
+ }
163
+
164
+ getAvailableLanguages() {
165
+ try {
166
+ const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
167
+ if (!items) {
168
+ console.error('Error reading source directory: Unable to access directory');
169
169
  return [];
170
170
  }
171
171
 
172
172
  const languages = [];
173
173
 
174
174
  // Check for directory-based structure
175
- const directories = items
176
- .filter(item => item.isDirectory())
177
- .map(item => item.name)
178
- .filter(name =>
179
- name !== 'node_modules' &&
180
- !name.startsWith('.') &&
181
- name !== this.config.sourceLanguage &&
182
- !this.isExcludedLanguageDirectory(name) &&
183
- this.isValidLanguageCode(name)
184
- )
185
- .filter(name => {
186
- const dirPath = path.join(this.sourceDir, name);
187
- const dirItems = SecurityUtils.safeReaddirSync(dirPath, process.cwd(), { withFileTypes: true }) || [];
188
- return dirItems.some(item => item.isFile() && item.name.endsWith('.json'));
189
- });
175
+ const directories = items
176
+ .filter(item => item.isDirectory())
177
+ .map(item => item.name)
178
+ .filter(name =>
179
+ name !== 'node_modules' &&
180
+ !name.startsWith('.') &&
181
+ name !== this.config.sourceLanguage &&
182
+ !this.isExcludedLanguageDirectory(name) &&
183
+ this.isValidLanguageCode(name)
184
+ )
185
+ .filter(name => {
186
+ const dirPath = path.join(this.sourceDir, name);
187
+ const dirItems = SecurityUtils.safeReaddirSync(dirPath, process.cwd(), { withFileTypes: true }) || [];
188
+ return dirItems.some(item => item.isFile() && item.name.endsWith('.json'));
189
+ });
190
190
 
191
191
  // Check for monolith files (language.json files)
192
192
  const files = items
@@ -197,17 +197,17 @@ class AnalyzeCommand {
197
197
  languages.push(...directories);
198
198
 
199
199
  // Add monolith files as languages (without .json extension)
200
- const monolithLanguages = files
201
- .map(file => file.replace('.json', ''))
202
- .filter(lang =>
203
- !languages.includes(lang) &&
204
- lang !== this.config.sourceLanguage &&
205
- !this.isExcludedLanguageDirectory(lang) &&
206
- this.isValidLanguageCode(lang)
207
- );
208
- languages.push(...monolithLanguages);
209
-
210
- return [...new Set(languages)].sort();
200
+ const monolithLanguages = files
201
+ .map(file => file.replace('.json', ''))
202
+ .filter(lang =>
203
+ !languages.includes(lang) &&
204
+ lang !== this.config.sourceLanguage &&
205
+ !this.isExcludedLanguageDirectory(lang) &&
206
+ this.isValidLanguageCode(lang)
207
+ );
208
+ languages.push(...monolithLanguages);
209
+
210
+ return [...new Set(languages)].sort();
211
211
  } catch (error) {
212
212
  console.error('Error reading source directory:', error.message);
213
213
  return [];
@@ -719,7 +719,7 @@ class AnalyzeCommand {
719
719
  throw new Error(t('analyze.failedToWriteReportFile') || 'Failed to write report file securely');
720
720
  }
721
721
 
722
- return reportPath;
722
+ return reportPath;
723
723
 
724
724
  } catch (error) {
725
725
  console.error(`Failed to save report for ${language}:`, error.message);
@@ -786,21 +786,21 @@ class AnalyzeCommand {
786
786
  console.log(t('analyze.analyzing', { language }) || `\n🔄 Analyzing ${language}...`);
787
787
  }
788
788
 
789
- const analysis = this.analyzeLanguage(language);
790
- const report = this.generateLanguageReport(analysis);
791
-
792
- // Save report
793
- const reportPath = await this.saveReport(language, report);
794
- const processedCount = results.length + 1;
795
-
796
- if (!args.json) {
797
- console.log(t('analyze.completed', { language }) || `✅ Analysis completed for ${language}`);
798
- console.log(t('analyze.progress', {
799
- translated: processedCount,
800
- total: languages.length
801
- }) || ` Progress: ${processedCount}/${languages.length} languages processed`);
802
- console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
803
- }
789
+ const analysis = this.analyzeLanguage(language);
790
+ const report = this.generateLanguageReport(analysis);
791
+
792
+ // Save report
793
+ const reportPath = await this.saveReport(language, report);
794
+ const processedCount = results.length + 1;
795
+
796
+ if (!args.json) {
797
+ console.log(t('analyze.completed', { language }) || `✅ Analysis completed for ${language}`);
798
+ console.log(t('analyze.progress', {
799
+ translated: processedCount,
800
+ total: languages.length
801
+ }) || ` Progress: ${processedCount}/${languages.length} languages processed`);
802
+ console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
803
+ }
804
804
 
805
805
  results.push({
806
806
  language,
@@ -896,7 +896,7 @@ class AnalyzeCommand {
896
896
  this.config = { ...baseConfig, ...this.config };
897
897
 
898
898
  const uiLanguage = this.config.uiLanguage || 'en';
899
- loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
899
+ loadTranslations(uiLanguage, path.resolve(__dirname, '../../../ui-locales'));
900
900
 
901
901
  this.sourceDir = this.config.sourceDir;
902
902
  this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
@@ -908,15 +908,13 @@ class AnalyzeCommand {
908
908
  const isCalledDirectly = require.main === module;
909
909
  if (isCalledDirectly && !args.noPrompt && !fromMenu) {
910
910
  // Only check admin authentication when running directly and not in no-prompt mode
911
- const AdminAuth = require('../../../utils/admin-auth');
912
- const adminAuth = new AdminAuth();
911
+ const adminAuth = new AdminAuth();
913
912
  await adminAuth.initialize();
914
913
 
915
914
  const isRequired = await adminAuth.isAuthRequired();
916
915
  if (isRequired) {
917
916
  console.log('\n' + t('adminCli.authRequiredForOperation', { operation: 'analyze translations' }));
918
- const cliHelper = require('../../../utils/cli-helper');
919
- const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
917
+ const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
920
918
  const isValid = await this.adminAuth.verifyPin(pin);
921
919
 
922
920
  if (!isValid) {
@@ -7,8 +7,9 @@
7
7
  * Handles command execution, authentication, and completion management.
8
8
  */
9
9
 
10
- const path = require('path');
11
- const { t } = require('../../../utils/i18n-helper');
10
+ const path = require('path');
11
+ const { t } = require('../../../utils/i18n-helper');
12
+ const cliHelper = require('../../../utils/cli-helper');
12
13
 
13
14
  // Import command handlers
14
15
  const InitCommand = require('./InitCommand');
@@ -90,20 +91,22 @@ class CommandRouter {
90
91
  /**
91
92
  * Check admin authentication for protected commands
92
93
  */
93
- async checkAdminAuth() {
94
- if (!this.adminAuth) {
95
- return true; // No auth service available, allow execution
96
- }
94
+ async checkAdminAuth(adminPin = null) {
95
+ if (!this.adminAuth) {
96
+ return true; // No auth service available, allow execution
97
+ }
97
98
 
98
99
  const isRequired = await this.adminAuth.isAuthRequired();
99
100
  if (!isRequired) {
100
101
  return true;
101
102
  }
102
103
 
103
- console.log(t('adminCli.authRequired'));
104
- const cliHelper = require('../../../utils/cli-helper');
105
- const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
106
- const isValid = await this.adminAuth.verifyPin(pin);
104
+ let pin = adminPin;
105
+ if (!pin) {
106
+ console.log(t('adminCli.authRequired'));
107
+ pin = await cliHelper.promptPin(t('adminCli.enterPin'));
108
+ }
109
+ const isValid = await this.adminAuth.verifyPin(pin);
107
110
 
108
111
  if (!isValid) {
109
112
  console.log(t('adminCli.invalidPin'));
@@ -141,7 +144,7 @@ class CommandRouter {
141
144
  ];
142
145
 
143
146
  if (authRequiredCommands.includes(command)) {
144
- const authPassed = await this.checkAdminAuth();
147
+ const authPassed = await this.checkAdminAuth(options.adminPin || null);
145
148
  if (!authPassed) {
146
149
  if (!this.isNonInteractiveMode && !isDirectCommand && this.prompt) {
147
150
  await this.prompt(t('menu.pressEnterToContinue'));
@@ -292,4 +295,4 @@ class CommandRouter {
292
295
  }
293
296
  }
294
297
 
295
- module.exports = CommandRouter;
298
+ module.exports = CommandRouter;
@@ -27,10 +27,21 @@ class FixerCommand {
27
27
  // Initialize fixer properties
28
28
  this.sourceDir = null;
29
29
  this.outputDir = null;
30
- this.backupDir = null;
31
- this.dryRun = false;
32
- this.force = false;
33
- }
30
+ this.backupDir = null;
31
+ this.dryRun = false;
32
+ this.force = false;
33
+ }
34
+
35
+ isExcludedLanguageDirectory(name) {
36
+ if (!name || typeof name !== 'string') return true;
37
+ const lowered = name.toLowerCase();
38
+ return lowered.startsWith('backup-') ||
39
+ lowered === 'backup' ||
40
+ lowered === 'backups' ||
41
+ lowered === 'i18ntk-backups' ||
42
+ lowered === 'reports' ||
43
+ lowered === 'i18ntk-reports';
44
+ }
34
45
 
35
46
  /**
36
47
  * Set runtime dependencies for interactive operations
@@ -64,11 +75,11 @@ class FixerCommand {
64
75
  this.config = { ...baseConfig, ...(this.config || {}) };
65
76
 
66
77
  const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
67
- loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
78
+ loadTranslations(uiLanguage, path.resolve(__dirname, '../../../ui-locales'));
68
79
 
69
80
  this.sourceDir = this.config.sourceDir;
70
81
  this.outputDir = this.config.outputDir;
71
- this.backupDir = path.join(this.sourceDir, 'backup');
82
+ this.backupDir = path.resolve(this.config.backup?.location || './i18ntk-backups', 'fixer');
72
83
 
73
84
  // Validate source directory exists
74
85
  const { validateSourceDir } = require('../../../utils/config-helper');
@@ -127,10 +138,15 @@ class FixerCommand {
127
138
  const languages = [];
128
139
 
129
140
  // Check for directory-based structure
130
- const directories = items
131
- .filter(item => item.isDirectory())
132
- .map(item => item.name)
133
- .filter(name => name !== 'node_modules' && !name.startsWith('.') && name !== this.config.sourceLanguage);
141
+ const directories = items
142
+ .filter(item => item.isDirectory())
143
+ .map(item => item.name)
144
+ .filter(name =>
145
+ name !== 'node_modules' &&
146
+ !name.startsWith('.') &&
147
+ name !== this.config.sourceLanguage &&
148
+ !this.isExcludedLanguageDirectory(name)
149
+ );
134
150
 
135
151
  // Check for monolith files (language.json files)
136
152
  const files = items
@@ -142,9 +158,13 @@ class FixerCommand {
142
158
 
143
159
  // Add monolith files as languages (without .json extension)
144
160
  const monolithLanguages = files
145
- .map(file => file.replace('.json', ''))
146
- .filter(lang => !languages.includes(lang) && lang !== this.config.sourceLanguage);
147
- languages.push(...monolithLanguages);
161
+ .map(file => file.replace('.json', ''))
162
+ .filter(lang =>
163
+ !languages.includes(lang) &&
164
+ lang !== this.config.sourceLanguage &&
165
+ !this.isExcludedLanguageDirectory(lang)
166
+ );
167
+ languages.push(...monolithLanguages);
148
168
 
149
169
  return [...new Set(languages)].sort();
150
170
  } catch (error) {
@@ -279,14 +299,21 @@ class FixerCommand {
279
299
  }
280
300
 
281
301
  // Create backup of translation files
282
- async createBackup() {
283
- if (this.dryRun) return;
284
-
285
- try {
286
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
287
- this.backupDir = path.join(this.sourceDir, `backup-${timestamp}`);
288
-
289
- console.log(t('fixer.creatingBackup', { dir: this.backupDir }));
302
+ async createBackup() {
303
+ if (this.dryRun) return;
304
+
305
+ try {
306
+ const backupEnabled = this.config?.backup?.enabled === true;
307
+ if (!backupEnabled) {
308
+ this.backupDir = null;
309
+ return;
310
+ }
311
+
312
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
313
+ const backupRoot = path.resolve(this.config?.backup?.location || './i18ntk-backups');
314
+ this.backupDir = path.join(backupRoot, 'fixer', `backup-${timestamp}`);
315
+
316
+ console.log(t('fixer.creatingBackup', { dir: this.backupDir }));
290
317
 
291
318
  // Ensure backup directory exists
292
319
  const dirCreated = SecurityUtils.safeMkdirSync(this.backupDir, process.cwd(), { recursive: true });
@@ -299,8 +326,8 @@ class FixerCommand {
299
326
  const languages = this.getAvailableLanguages();
300
327
  languages.push(this.config.sourceLanguage); // Include source language
301
328
 
302
- for (const language of languages) {
303
- const languageFiles = this.getLanguageFiles(language);
329
+ for (const language of languages) {
330
+ const languageFiles = this.getLanguageFiles(language);
304
331
 
305
332
  for (const fileName of languageFiles) {
306
333
  const sourcePath = path.join(this.sourceDir, language, fileName);
@@ -316,13 +343,42 @@ class FixerCommand {
316
343
  SecurityUtils.safeWriteFileSync(backupPath, content, process.cwd(), 'utf8');
317
344
  }
318
345
  }
319
- }
320
-
321
- console.log(t('fixer.backupCreated'));
322
- } catch (error) {
323
- console.warn(`Failed to create backup: ${error.message}`);
324
- }
325
- }
346
+ }
347
+
348
+ this.cleanupOldBackups();
349
+
350
+ console.log(t('fixer.backupCreated'));
351
+ } catch (error) {
352
+ console.warn(`Failed to create backup: ${error.message}`);
353
+ }
354
+ }
355
+
356
+ cleanupOldBackups() {
357
+ try {
358
+ const fixerBackupRoot = path.resolve(this.config?.backup?.location || './i18ntk-backups', 'fixer');
359
+ if (!SecurityUtils.safeExistsSync(fixerBackupRoot, process.cwd())) return;
360
+
361
+ const configuredKeep = parseInt(this.config?.backup?.maxBackups, 10);
362
+ const keepCount = Number.isInteger(configuredKeep) ? Math.min(Math.max(configuredKeep, 1), 3) : 1;
363
+
364
+ const backupDirs = SecurityUtils.safeReaddirSync(fixerBackupRoot, process.cwd(), { withFileTypes: true })
365
+ .filter(entry => entry.isDirectory() && entry.name.startsWith('backup-'))
366
+ .map(entry => {
367
+ const dirPath = path.join(fixerBackupRoot, entry.name);
368
+ const stat = SecurityUtils.safeStatSync(dirPath, process.cwd());
369
+ return { name: entry.name, path: dirPath, mtimeMs: stat ? stat.mtimeMs : 0 };
370
+ })
371
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
372
+
373
+ if (backupDirs.length <= keepCount) return;
374
+
375
+ for (const staleDir of backupDirs.slice(keepCount)) {
376
+ fs.rmSync(staleDir.path, { recursive: true, force: true });
377
+ }
378
+ } catch (error) {
379
+ console.warn(`Failed to clean old backups: ${error.message}`);
380
+ }
381
+ }
326
382
 
327
383
  // Analyze translation issues for fixing
328
384
  analyzeIssues(language, fileName) {
@@ -471,9 +527,9 @@ class FixerCommand {
471
527
  }
472
528
 
473
529
  // Create backup unless disabled
474
- if (!args.noBackup && !this.dryRun) {
475
- await this.createBackup();
476
- }
530
+ if (!args.noBackup && !this.dryRun && this.config?.backup?.enabled === true) {
531
+ await this.createBackup();
532
+ }
477
533
 
478
534
  const languages = this.getAvailableLanguages();
479
535
 
@@ -535,9 +591,9 @@ class FixerCommand {
535
591
  console.log(t('fixer.totalIssues', { count: totalIssues }));
536
592
  console.log(t('fixer.totalFixed', { count: totalFixed }));
537
593
 
538
- if (this.backupDir && !args.noBackup) {
539
- console.log(t('fixer.backupLocation', { dir: this.backupDir }));
540
- }
594
+ if (this.backupDir && !args.noBackup && this.config?.backup?.enabled === true) {
595
+ console.log(t('fixer.backupLocation', { dir: this.backupDir }));
596
+ }
541
597
 
542
598
  console.log(t('fixer.completed'));
543
599
 
@@ -567,7 +623,7 @@ class FixerCommand {
567
623
  this.config = { ...baseConfig, ...this.config };
568
624
 
569
625
  const uiLanguage = this.config.uiLanguage || 'en';
570
- loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
626
+ loadTranslations(uiLanguage, path.resolve(__dirname, '../../../ui-locales'));
571
627
 
572
628
  this.sourceDir = this.config.sourceDir;
573
629
  this.outputDir = this.config.outputDir;
@@ -621,4 +677,4 @@ class FixerCommand {
621
677
  }
622
678
  }
623
679
 
624
- module.exports = FixerCommand;
680
+ module.exports = FixerCommand;
@@ -41,8 +41,8 @@ class ScannerCommand {
41
41
  }
42
42
 
43
43
  loadLocale() {
44
- const uiLocalesDir = path.join(__dirname, '../../../resources', 'i18n', 'ui-locales');
45
- const localeFile = path.join(uiLocalesDir, 'en.json');
44
+ const uiLocalesDir = path.join(__dirname, '../../../ui-locales');
45
+ const localeFile = path.join(uiLocalesDir, 'en.json');
46
46
 
47
47
  try {
48
48
  const localeContent = SecurityUtils.safeReadFileSync(localeFile, uiLocalesDir, 'utf8');