i18ntk 1.10.2 → 2.0.2

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 (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +141 -1191
  3. package/main/i18ntk-analyze.js +65 -84
  4. package/main/i18ntk-backup-class.js +420 -0
  5. package/main/i18ntk-backup.js +3 -3
  6. package/main/i18ntk-complete.js +90 -65
  7. package/main/i18ntk-doctor.js +123 -103
  8. package/main/i18ntk-fixer.js +61 -725
  9. package/main/i18ntk-go.js +14 -15
  10. package/main/i18ntk-init.js +77 -26
  11. package/main/i18ntk-java.js +27 -32
  12. package/main/i18ntk-js.js +70 -68
  13. package/main/i18ntk-manage.js +129 -30
  14. package/main/i18ntk-php.js +75 -75
  15. package/main/i18ntk-py.js +55 -56
  16. package/main/i18ntk-scanner.js +59 -57
  17. package/main/i18ntk-setup.js +9 -404
  18. package/main/i18ntk-sizing.js +6 -6
  19. package/main/i18ntk-summary.js +21 -18
  20. package/main/i18ntk-ui.js +11 -10
  21. package/main/i18ntk-usage.js +54 -18
  22. package/main/i18ntk-validate.js +13 -13
  23. package/main/manage/commands/AnalyzeCommand.js +1124 -0
  24. package/main/manage/commands/BackupCommand.js +62 -0
  25. package/main/manage/commands/CommandRouter.js +295 -0
  26. package/main/manage/commands/CompleteCommand.js +61 -0
  27. package/main/manage/commands/DoctorCommand.js +60 -0
  28. package/main/manage/commands/FixerCommand.js +624 -0
  29. package/main/manage/commands/InitCommand.js +62 -0
  30. package/main/manage/commands/ScannerCommand.js +654 -0
  31. package/main/manage/commands/SizingCommand.js +60 -0
  32. package/main/manage/commands/SummaryCommand.js +61 -0
  33. package/main/manage/commands/UsageCommand.js +60 -0
  34. package/main/manage/commands/ValidateCommand.js +978 -0
  35. package/main/manage/index-fixed.js +1447 -0
  36. package/main/manage/index.js +1462 -0
  37. package/main/manage/managers/DebugMenu.js +140 -0
  38. package/main/manage/managers/InteractiveMenu.js +177 -0
  39. package/main/manage/managers/LanguageMenu.js +62 -0
  40. package/main/manage/managers/SettingsMenu.js +53 -0
  41. package/main/manage/services/AuthenticationService.js +263 -0
  42. package/main/manage/services/ConfigurationService-fixed.js +449 -0
  43. package/main/manage/services/ConfigurationService.js +449 -0
  44. package/main/manage/services/FileManagementService.js +368 -0
  45. package/main/manage/services/FrameworkDetectionService.js +458 -0
  46. package/main/manage/services/InitService.js +1051 -0
  47. package/main/manage/services/SetupService.js +462 -0
  48. package/main/manage/services/SummaryService.js +450 -0
  49. package/main/manage/services/UsageService.js +1502 -0
  50. package/package.json +32 -29
  51. package/runtime/enhanced.d.ts +221 -221
  52. package/runtime/index.d.ts +29 -29
  53. package/runtime/index.full.d.ts +331 -331
  54. package/runtime/index.js +7 -6
  55. package/scripts/build-lite.js +17 -17
  56. package/scripts/deprecate-versions.js +23 -6
  57. package/scripts/export-translations.js +5 -5
  58. package/scripts/fix-all-i18n.js +3 -3
  59. package/scripts/fix-and-purify-i18n.js +3 -2
  60. package/scripts/fix-locale-control-chars.js +110 -0
  61. package/scripts/lint-locales.js +80 -0
  62. package/scripts/locale-optimizer.js +8 -8
  63. package/scripts/prepublish.js +21 -21
  64. package/scripts/security-check.js +117 -117
  65. package/scripts/sync-translations.js +4 -4
  66. package/scripts/sync-ui-locales.js +9 -8
  67. package/scripts/validate-all-translations.js +8 -7
  68. package/scripts/verify-deprecations.js +157 -161
  69. package/scripts/verify-translations.js +6 -5
  70. package/settings/i18ntk-config.json +282 -282
  71. package/settings/language-config.json +5 -5
  72. package/settings/settings-cli.js +9 -9
  73. package/settings/settings-manager.js +18 -18
  74. package/ui-locales/de.json +2417 -2348
  75. package/ui-locales/en.json +2415 -2352
  76. package/ui-locales/es.json +2425 -2353
  77. package/ui-locales/fr.json +2418 -2348
  78. package/ui-locales/ja.json +2463 -2361
  79. package/ui-locales/ru.json +2463 -2359
  80. package/ui-locales/zh.json +2418 -2351
  81. package/utils/admin-auth.js +2 -2
  82. package/utils/admin-cli.js +297 -297
  83. package/utils/admin-pin.js +9 -9
  84. package/utils/cli-helper.js +9 -9
  85. package/utils/config-helper.js +73 -104
  86. package/utils/config-manager.js +204 -171
  87. package/utils/config.js +5 -4
  88. package/utils/env-manager.js +249 -263
  89. package/utils/framework-detector.js +27 -24
  90. package/utils/i18n-helper.js +85 -41
  91. package/utils/init-helper.js +152 -94
  92. package/utils/json-output.js +98 -98
  93. package/utils/mini-commander.js +179 -0
  94. package/utils/missing-key-validator.js +5 -5
  95. package/utils/plugin-loader.js +40 -29
  96. package/utils/prompt.js +14 -44
  97. package/utils/safe-json.js +40 -0
  98. package/utils/secure-errors.js +3 -3
  99. package/utils/security-check-improved.js +390 -0
  100. package/utils/security-config.js +5 -5
  101. package/utils/security-fixed.js +607 -0
  102. package/utils/security.js +652 -602
  103. package/utils/setup-enforcer.js +136 -44
  104. package/utils/setup-validator.js +33 -32
  105. package/utils/ultra-performance-optimizer.js +11 -9
  106. package/utils/watch-locales.js +2 -1
  107. package/utils/prompt-fixed.js +0 -55
  108. package/utils/security-check.js +0 -454
@@ -27,7 +27,7 @@ const SetupEnforcer = require('../utils/setup-enforcer');
27
27
  }
28
28
  })();
29
29
 
30
- loadTranslations( 'en', path.resolve(__dirname, '..', 'ui-locales'));
30
+ loadTranslations( 'en', path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales'));
31
31
 
32
32
  const PROJECT_ROOT = process.cwd();
33
33
 
@@ -67,7 +67,7 @@ class I18nAnalyzer {
67
67
  this.config = { ...baseConfig, ...(this.config || {}) };
68
68
 
69
69
  const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
70
- loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
70
+ loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales'));
71
71
 
72
72
  this.sourceDir = this.config.sourceDir;
73
73
  this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
@@ -884,7 +884,7 @@ try {
884
884
  this.config = { ...baseConfig, ...this.config };
885
885
 
886
886
  const uiLanguage = this.config.uiLanguage || 'en';
887
- loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
887
+ loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales'));
888
888
 
889
889
  this.sourceDir = this.config.sourceDir;
890
890
  this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
@@ -969,130 +969,110 @@ try {
969
969
  }
970
970
 
971
971
  async runSetupWizard() {
972
- console.log('🧙‍♂️ Translation Analysis Setup Wizard');
972
+ console.log('Translation Analysis Setup Wizard');
973
973
  console.log('='.repeat(50));
974
-
975
- const prompts = require('prompts');
976
-
974
+
977
975
  try {
978
- // Detect current structure
979
976
  const structure = this.detectStructureType();
980
977
  console.log(`Current structure detected: ${structure.type}`);
981
-
982
- const questions = [
983
- {
984
- type: 'select',
985
- name: 'structureType',
986
- message: 'Choose your translation file structure:',
987
- choices: [
988
- { title: 'Monolith files (en.json, de.json, etc.)', value: 'monolith' },
989
- { title: 'Directory structure (en/common.json, de/common.json)', value: 'directory' },
990
- { title: 'Namespace structure (common/en.json, forms/en.json)', value: 'namespace' },
991
- { title: 'Mixed structure (auto-detect)', value: 'mixed' }
992
- ],
993
- initial: structure.type === 'monolith' ? 0 : structure.type === 'directory' ? 1 : 2
994
- },
995
- {
996
- type: 'text',
997
- name: 'sourceDir',
998
- message: 'Enter source directory path:',
999
- initial: this.sourceDir,
1000
- validate: value => fs.existsSync(value) ? true : 'Directory does not exist'
1001
- },
1002
- {
1003
- type: 'text',
1004
- name: 'languages',
1005
- message: 'Enter languages to analyze (comma-separated, e.g., de,fr,es):',
1006
- initial: 'de,fr,es,ja,ru',
1007
- validate: value => value.trim() ? true : 'Please enter at least one language'
1008
- },
1009
- {
1010
- type: 'confirm',
1011
- name: 'outputReports',
1012
- message: 'Generate detailed reports for each language?',
1013
- initial: true
1014
- },
1015
- {
1016
- type: 'text',
1017
- name: 'outputDir',
1018
- message: 'Enter output directory for reports:',
1019
- initial: this.outputDir
1020
- }
1021
- ];
1022
-
1023
- const response = await prompts(questions);
1024
-
1025
- if (!response.structureType) {
1026
- console.log('Setup cancelled.');
978
+
979
+ const structureOptions = ['monolith', 'directory', 'namespace', 'mixed'];
980
+ const defaultStructureIndex = structureOptions.indexOf(structure.type);
981
+ const defaultStructureChoice = defaultStructureIndex >= 0 ? String(defaultStructureIndex + 1) : '4';
982
+
983
+ console.log('Choose your translation file structure:');
984
+ console.log('1) Monolith files (en.json, de.json, etc.)');
985
+ console.log('2) Directory structure (en/common.json, de/common.json)');
986
+ console.log('3) Namespace structure (common/en.json, forms/en.json)');
987
+ console.log('4) Mixed structure (auto-detect)');
988
+ const structureChoiceInput = await this.prompt(`Select option [${defaultStructureChoice}]: `);
989
+ const structureChoice = structureChoiceInput.trim() || defaultStructureChoice;
990
+ const structureType = structureOptions[Number(structureChoice) - 1] || 'mixed';
991
+
992
+ const sourceDirInput = await this.prompt(`Enter source directory path [${this.sourceDir}]: `);
993
+ const sourceDir = sourceDirInput.trim() || this.sourceDir;
994
+ if (!SecurityUtils.safeExistsSync(sourceDir, process.cwd())) {
995
+ console.log('Setup cancelled: directory does not exist.');
1027
996
  return { success: false, cancelled: true };
1028
997
  }
1029
-
1030
- // Update configuration
998
+
999
+ const languagesInput = await this.prompt('Enter languages to analyze (comma-separated) [de,fr,es,ja,ru]: ');
1000
+ const languagesValue = languagesInput.trim() || 'de,fr,es,ja,ru';
1001
+ const languages = languagesValue.split(',').map(lang => lang.trim()).filter(Boolean);
1002
+ if (languages.length === 0) {
1003
+ console.log('Setup cancelled: no languages provided.');
1004
+ return { success: false, cancelled: true };
1005
+ }
1006
+
1007
+ const outputReportsInput = await this.prompt('Generate detailed reports for each language? (Y/n): ');
1008
+ const outputReports = !['n', 'no'].includes(outputReportsInput.trim().toLowerCase());
1009
+
1010
+ const outputDirInput = await this.prompt(`Enter output directory for reports [${this.outputDir}]: `);
1011
+ const outputDir = outputDirInput.trim() || this.outputDir;
1012
+
1013
+ const response = {
1014
+ structureType,
1015
+ sourceDir,
1016
+ languages: languagesValue,
1017
+ outputReports,
1018
+ outputDir
1019
+ };
1020
+
1031
1021
  this.sourceDir = path.resolve(response.sourceDir);
1032
1022
  this.outputDir = path.resolve(response.outputDir);
1033
1023
  this.outputReports = response.outputReports;
1034
-
1035
- const languages = response.languages.split(',').map(lang => lang.trim()).filter(Boolean);
1036
-
1037
- console.log('\n📊 Configuration Summary:');
1024
+
1025
+ console.log('\nConfiguration Summary:');
1038
1026
  console.log(`Source: ${this.sourceDir}`);
1039
1027
  console.log(`Output: ${this.outputDir}`);
1040
- console.log(`Languages: ${languages.join(', ')}`);
1028
+ console.log(`Languages: ${languages.join(", ")}`);
1041
1029
  console.log(`Structure: ${response.structureType}`);
1042
-
1043
- const confirm = await prompts({
1044
- type: 'confirm',
1045
- name: 'proceed',
1046
- message: 'Proceed with analysis?',
1047
- initial: true
1048
- });
1049
-
1050
- if (!confirm.proceed) {
1030
+
1031
+ const proceedInput = await this.prompt('Proceed with analysis? (Y/n): ');
1032
+ const proceed = !['n', 'no'].includes(proceedInput.trim().toLowerCase());
1033
+ if (!proceed) {
1051
1034
  console.log('Setup cancelled.');
1052
1035
  return { success: false, cancelled: true };
1053
1036
  }
1054
-
1055
- // Run analysis with specified languages
1037
+
1056
1038
  const results = [];
1057
1039
  for (const language of languages) {
1058
1040
  try {
1059
- console.log(`\n🔍 Analyzing ${language}...`);
1041
+ console.log(`\nAnalyzing ${language}...`);
1060
1042
  const result = await this.analyzeLanguage(language);
1061
1043
  results.push({ language, ...result });
1062
-
1044
+
1063
1045
  if (this.outputReports) {
1064
1046
  await this.saveReport(language, result);
1065
- console.log(`✅ Report saved: ${language}.json`);
1047
+ console.log(`Report saved: ${language}.json`);
1066
1048
  }
1067
1049
  } catch (error) {
1068
- console.error(`❌ Error analyzing ${language}:`, error.message);
1050
+ console.error(`Error analyzing ${language}:`, error.message);
1069
1051
  results.push({ language, error: error.message });
1070
1052
  }
1071
1053
  }
1072
-
1073
- // Generate summary
1054
+
1074
1055
  const summary = {
1075
1056
  totalLanguages: results.length,
1076
1057
  successful: results.filter(r => !r.error).length,
1077
1058
  failed: results.filter(r => r.error).length,
1078
1059
  results
1079
1060
  };
1080
-
1061
+
1081
1062
  await this.saveReport('wizard-summary', {
1082
1063
  summary,
1083
1064
  configuration: response,
1084
1065
  timestamp: new Date().toISOString()
1085
1066
  });
1086
-
1087
- console.log('\n🎉 Setup complete!');
1067
+
1068
+ console.log('\nSetup complete.');
1088
1069
  console.log(`Analyzed ${summary.successful}/${summary.totalLanguages} languages successfully`);
1089
-
1070
+
1090
1071
  return {
1091
1072
  success: true,
1092
1073
  summary,
1093
1074
  configuration: response
1094
1075
  };
1095
-
1096
1076
  } catch (error) {
1097
1077
  console.error('Setup wizard error:', error.message);
1098
1078
  return { success: false, error: error.message };
@@ -1168,4 +1148,5 @@ async function analyzeTranslations(datasetPath) {
1168
1148
  module.exports = {
1169
1149
  I18nAnalyzer,
1170
1150
  analyzeTranslations
1171
- };
1151
+ };
1152
+
@@ -0,0 +1,420 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('fs/promises');
6
+ const path = require('path');
7
+ const { existsSync } = require('fs');
8
+ const configManager = require('../utils/config-manager');
9
+ const { logger } = require('../utils/logger');
10
+ const { colors } = require('../utils/logger');
11
+ const SecurityUtils = require('../utils/security');
12
+
13
+ /**
14
+ * I18NTK BACKUP CLASS
15
+ *
16
+ * Class-based implementation of backup functionality for use with CommandRouter
17
+ */
18
+ class I18nBackup {
19
+ constructor(config = {}) {
20
+ this.config = config;
21
+ this.backupDir = path.join(process.cwd(), 'i18n-backups');
22
+ this.maxBackups = config.backup?.maxBackups || 10;
23
+ }
24
+
25
+ /**
26
+ * Main run method for the backup command
27
+ */
28
+ async run(options = {}) {
29
+ const command = options.command || options._ && options._[0];
30
+
31
+ // Show help if no command provided
32
+ if (!command) {
33
+ this.showHelp();
34
+ return { success: true, command: 'help' };
35
+ }
36
+
37
+ try {
38
+ switch (command) {
39
+ case 'create':
40
+ return await this.handleCreate(options);
41
+ case 'restore':
42
+ return await this.handleRestore(options);
43
+ case 'list':
44
+ return await this.handleList();
45
+ case 'verify':
46
+ return await this.handleVerify(options);
47
+ case 'cleanup':
48
+ return await this.handleCleanup(options);
49
+ default:
50
+ logger.error(`Unknown command: ${command}`);
51
+ this.showHelp();
52
+ return { success: false, error: `Unknown command: ${command}` };
53
+ }
54
+ } catch (error) {
55
+ this.handleError(error);
56
+ return { success: false, error: error.message };
57
+ }
58
+ }
59
+
60
+ showHelp() {
61
+ console.log(`
62
+ i18ntk-backup - Secure backup and restore for i18n translation files
63
+
64
+ Usage:
65
+ i18ntk-backup <command> [options]
66
+
67
+ Commands:
68
+ create <dir> Create a backup of translation files
69
+ restore <file> Restore from a backup
70
+ list List available backups
71
+ verify <file> Verify the integrity of a backup file
72
+ cleanup Remove old backups
73
+
74
+ Options:
75
+ --output <path> Output directory for backup/restore
76
+ --force Overwrite existing files without prompting
77
+ --keep <number> Number of backups to keep (default: 10)
78
+ `);
79
+ }
80
+
81
+ // Command handlers
82
+ async handleCreate(options = {}) {
83
+ // Use absolute path for the locales directory
84
+ const dir = (options._ && options._[1]) || options.dir || path.join(__dirname, '..', 'locales');
85
+ const outputDir = options.output || this.backupDir;
86
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
87
+ const backupName = `backup-${timestamp}.json`;
88
+ const backupPath = path.join(outputDir, backupName);
89
+
90
+ // Log the paths for debugging
91
+ logger.debug(`Source directory: ${dir}`);
92
+ logger.debug(`Backup will be saved to: ${backupPath}`);
93
+
94
+ // Create backup directory if it doesn't exist
95
+ try {
96
+ await fs.mkdir(outputDir, { recursive: true });
97
+ logger.debug(`Created backup directory: ${outputDir}`);
98
+ } catch (err) {
99
+ if (err.code !== 'EEXIST') {
100
+ logger.error(`Failed to create backup directory: ${err.message}`);
101
+ throw err;
102
+ }
103
+ logger.debug(`Using existing backup directory: ${outputDir}`);
104
+ }
105
+
106
+ // Validate directory
107
+ const sourceDir = path.resolve(dir);
108
+ try {
109
+ const stats = await fs.stat(sourceDir);
110
+ if (!stats.isDirectory()) {
111
+ throw new Error(`Path exists but is not a directory: ${sourceDir}`);
112
+ }
113
+ logger.debug(`Source directory exists: ${sourceDir}`);
114
+ } catch (err) {
115
+ if (err.code === 'ENOENT') {
116
+ throw new Error(`Directory not found: ${sourceDir}. Please specify a valid directory.`);
117
+ }
118
+ throw new Error(`Error accessing directory ${sourceDir}: ${err.message}`);
119
+ }
120
+
121
+ logger.info('\nCreating backup...');
122
+
123
+ // Read all files in the directory
124
+ const files = (await fs.readdir(sourceDir, { withFileTypes: true }))
125
+ .filter(dirent => dirent.isFile() && dirent.name.endsWith('.json'))
126
+ .map(dirent => dirent.name);
127
+
128
+ if (files.length === 0) {
129
+ logger.warn('No JSON files found in the specified directory');
130
+ return { success: true, message: 'No files to backup' };
131
+ }
132
+
133
+ // Read all translation files
134
+ const translations = {};
135
+ for (const file of files) {
136
+ const filePath = path.join(sourceDir, file);
137
+ try {
138
+ const content = JSON.parse(await fs.readFile(filePath, 'utf8'));
139
+ translations[file] = content;
140
+ } catch (error) {
141
+ logger.error(`Could not read file ${file}: ${error.message}`);
142
+ }
143
+ }
144
+
145
+ // Create the backup
146
+ await fs.writeFile(backupPath, JSON.stringify(translations, null, 2));
147
+ const stats = await fs.stat(backupPath);
148
+
149
+ logger.success('Backup created successfully');
150
+ logger.info(` Location: ${backupPath}`);
151
+ logger.info(` Size: ${(stats.size / 1024).toFixed(2)} KB`);
152
+ logger.info(` Timestamp: ${new Date().toLocaleString()}`);
153
+
154
+ // Clean up old backups
155
+ await this.cleanupOldBackups(outputDir);
156
+
157
+ return {
158
+ success: true,
159
+ command: 'create',
160
+ backupPath,
161
+ size: stats.size,
162
+ fileCount: files.length
163
+ };
164
+ }
165
+
166
+ async handleRestore(options = {}) {
167
+ const backupFile = options._ && options._[1];
168
+ if (!backupFile) {
169
+ throw new Error('Backup file path is required');
170
+ }
171
+
172
+ const backupPath = path.resolve(process.cwd(), backupFile);
173
+ const outputDir = options.output
174
+ ? path.resolve(process.cwd(), options.output)
175
+ : path.join(process.cwd(), 'restored');
176
+
177
+ // Validate backup file
178
+ if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
179
+ throw new Error(`Backup file not found: ${backupPath}`);
180
+ }
181
+
182
+ logger.info('\nRestoring backup...');
183
+
184
+ try {
185
+ // Read the backup file
186
+ const backupData = await fs.readFile(backupPath, 'utf8');
187
+ const translations = JSON.parse(backupData);
188
+
189
+ // Create output directory if it doesn't exist
190
+ try {
191
+ await fs.mkdir(outputDir, { recursive: true });
192
+ } catch (err) {
193
+ if (err.code !== 'EEXIST') throw err;
194
+ }
195
+
196
+ // Write the restored files
197
+ for (const [file, content] of Object.entries(translations)) {
198
+ const filePath = path.join(outputDir, file);
199
+ await fs.writeFile(filePath, JSON.stringify(content, null, 2));
200
+ }
201
+
202
+ logger.success('Backup restored successfully');
203
+ logger.info(` Restored ${Object.keys(translations).length} files to: ${outputDir}`);
204
+
205
+ return {
206
+ success: true,
207
+ command: 'restore',
208
+ outputDir,
209
+ fileCount: Object.keys(translations).length
210
+ };
211
+ } catch (error) {
212
+ this.handleError(error);
213
+ throw error;
214
+ }
215
+ }
216
+
217
+ async handleList() {
218
+ try {
219
+ // Ensure backup directory exists
220
+ try {
221
+ await fs.access(this.backupDir);
222
+ } catch (err) {
223
+ if (err.code === 'ENOENT') {
224
+ logger.warn('No backups found. The backup directory does not exist yet.');
225
+ } else {
226
+ logger.error(`Error accessing backup directory: ${err.message}`);
227
+ }
228
+ return { success: true, backups: [] };
229
+ }
230
+
231
+ const files = await fs.readdir(this.backupDir);
232
+ const backups = [];
233
+
234
+ for (const file of files) {
235
+ if (file.startsWith('backup-') && file.endsWith('.json')) {
236
+ try {
237
+ const filePath = path.join(this.backupDir, file);
238
+ const stats = await fs.stat(filePath);
239
+ backups.push({
240
+ name: file,
241
+ path: filePath,
242
+ size: stats.size,
243
+ createdAt: stats.mtime
244
+ });
245
+ } catch (err) {
246
+ logger.warn(`Skipping invalid backup file ${file}: ${err.message}`);
247
+ }
248
+ }
249
+ }
250
+
251
+ if (backups.length === 0) {
252
+ logger.warn('No valid backup files found in the backup directory.');
253
+ return { success: true, backups: [] };
254
+ }
255
+
256
+ // Sort by creation time (newest first)
257
+ backups.sort((a, b) => b.createdAt - a.createdAt);
258
+
259
+ logger.info('\n📋 Available Backups');
260
+ logger.info('='.repeat(50));
261
+
262
+ backups.forEach((backup, index) => {
263
+ const sizeKB = (backup.size / 1024).toFixed(2);
264
+ const formattedDate = backup.createdAt.toLocaleString();
265
+
266
+ logger.info(`🔹 ${index + 1}. ${backup.name}`);
267
+ logger.info(` 📏 Size: ${sizeKB} KB`);
268
+ logger.info(` 📅 Created: ${formattedDate}`);
269
+ logger.info(` 📂 Path: ${backup.path}\n`);
270
+ });
271
+
272
+ logger.info(`Total backups: ${backups.length}`);
273
+
274
+ return { success: true, backups, count: backups.length };
275
+ } catch (err) {
276
+ if (err.code === 'ENOENT') {
277
+ logger.warn('No backups found.');
278
+ return { success: true, backups: [] };
279
+ } else {
280
+ throw err;
281
+ }
282
+ }
283
+ }
284
+
285
+ async handleVerify(options = {}) {
286
+ const backupFile = options._ && options._[1];
287
+ if (!backupFile) {
288
+ throw new Error('Backup file path is required');
289
+ }
290
+
291
+ const backupPath = path.resolve(process.cwd(), backupFile);
292
+
293
+ // Validate backup file
294
+ if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
295
+ throw new Error(`Backup file not found: ${backupPath}`);
296
+ }
297
+
298
+ logger.info('\nVerifying backup...');
299
+
300
+ try {
301
+ const data = await fs.readFile(backupPath, 'utf8');
302
+ const content = JSON.parse(data);
303
+
304
+ if (typeof content === 'object' && content !== null) {
305
+ const fileCount = Object.keys(content).length;
306
+ logger.success('Backup is valid');
307
+ logger.info(` Contains ${fileCount} translation files`);
308
+ logger.info(` Last modified: ${(await fs.stat(backupPath)).mtime.toLocaleString()}`);
309
+
310
+ return {
311
+ success: true,
312
+ command: 'verify',
313
+ valid: true,
314
+ fileCount
315
+ };
316
+ } else {
317
+ throw new Error('Invalid backup format');
318
+ }
319
+ } catch (error) {
320
+ logger.error('Backup verification failed!');
321
+ logger.error(` Error: ${error.message}`);
322
+ return {
323
+ success: false,
324
+ command: 'verify',
325
+ valid: false,
326
+ error: error.message
327
+ };
328
+ }
329
+ }
330
+
331
+ async handleCleanup(options = {}) {
332
+ const keep = options.keep ? parseInt(options.keep, 10) : this.maxBackups;
333
+
334
+ logger.info('\nCleaning up old backups...');
335
+
336
+ try {
337
+ const files = await fs.readdir(this.backupDir);
338
+ const backupFiles = files
339
+ .filter(file => file.startsWith('backup-') && file.endsWith('.json'))
340
+ .map(file => ({
341
+ name: file,
342
+ path: path.join(this.backupDir, file),
343
+ time: fs.statSync(path.join(this.backupDir, file)).mtime.getTime()
344
+ }))
345
+ .sort((a, b) => b.time - a.time);
346
+
347
+ // Keep only the most recent 'keep' files
348
+ const toDelete = backupFiles.slice(keep);
349
+
350
+ if (toDelete.length === 0) {
351
+ logger.info('No old backups to delete.');
352
+ return { success: true, deleted: 0 };
353
+ }
354
+
355
+ // Delete old backups
356
+ for (const file of toDelete) {
357
+ try {
358
+ await fs.unlink(file.path);
359
+ logger.info(` - Deleted: ${file.name}`);
360
+ } catch (err) {
361
+ logger.error(` - Failed to delete ${file.name}: ${err.message}`);
362
+ }
363
+ }
364
+
365
+ logger.info(`\nRemoved ${toDelete.length} old backups`);
366
+ logger.info(`Total backups kept: ${keep}`);
367
+
368
+ return {
369
+ success: true,
370
+ command: 'cleanup',
371
+ deleted: toDelete.length,
372
+ kept: keep
373
+ };
374
+ } catch (error) {
375
+ logger.error('Error cleaning up backups:');
376
+ logger.error(` ${error.message}`);
377
+ if (process.env.DEBUG) {
378
+ console.error(error);
379
+ }
380
+ throw error;
381
+ }
382
+ }
383
+
384
+ async cleanupOldBackups(outputDir) {
385
+ try {
386
+ const files = await fs.readdir(outputDir);
387
+ const backupFiles = files
388
+ .filter(file => file.startsWith('backup-') && file.endsWith('.json'))
389
+ .map(file => ({
390
+ name: file,
391
+ path: path.join(outputDir, file),
392
+ time: fs.statSync(path.join(outputDir, file)).mtime.getTime()
393
+ }))
394
+ .sort((a, b) => b.time - a.time);
395
+
396
+ // Keep only the most recent files
397
+ const toDelete = backupFiles.slice(this.maxBackups);
398
+
399
+ for (const file of toDelete) {
400
+ try {
401
+ await fs.unlink(file.path);
402
+ } catch (err) {
403
+ // Ignore cleanup errors
404
+ }
405
+ }
406
+ } catch (error) {
407
+ // Ignore cleanup errors
408
+ }
409
+ }
410
+
411
+ handleError(error) {
412
+ logger.error('Backup operation failed:');
413
+ logger.error(` ${error.message}`);
414
+ if (process.env.DEBUG) {
415
+ console.error(error);
416
+ }
417
+ }
418
+ }
419
+
420
+ module.exports = I18nBackup;
@@ -40,7 +40,7 @@ const { existsSync } = require('fs');
40
40
  const configManager = require('../utils/config-manager');
41
41
  const { logger } = require('../utils/logger');
42
42
  const { colors } = require('../utils/logger');
43
- const prompt = require('../utils/prompt-fixed');
43
+ const prompt = require('../utils/prompt');
44
44
 
45
45
  // Backup configuration
46
46
  const config = configManager.getConfig();
@@ -195,7 +195,7 @@ async function handleRestore(args) {
195
195
  : path.join(process.cwd(), 'restored');
196
196
 
197
197
  // Validate backup file
198
- if (!fs.existsSync(backupPath)) {
198
+ if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
199
199
  throw new Error(`Backup file not found: ${backupPath}`);
200
200
  }
201
201
 
@@ -300,7 +300,7 @@ async function handleVerify(args) {
300
300
  const backupPath = path.resolve(process.cwd(), backupFile);
301
301
 
302
302
  // Validate backup file
303
- if (!fs.existsSync(backupPath)) {
303
+ if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
304
304
  throw new Error(`Backup file not found: ${backupPath}`);
305
305
  }
306
306