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.
- package/LICENSE +1 -1
- package/README.md +141 -1191
- package/main/i18ntk-analyze.js +65 -84
- package/main/i18ntk-backup-class.js +420 -0
- package/main/i18ntk-backup.js +3 -3
- package/main/i18ntk-complete.js +90 -65
- package/main/i18ntk-doctor.js +123 -103
- package/main/i18ntk-fixer.js +61 -725
- package/main/i18ntk-go.js +14 -15
- package/main/i18ntk-init.js +77 -26
- package/main/i18ntk-java.js +27 -32
- package/main/i18ntk-js.js +70 -68
- package/main/i18ntk-manage.js +129 -30
- package/main/i18ntk-php.js +75 -75
- package/main/i18ntk-py.js +55 -56
- package/main/i18ntk-scanner.js +59 -57
- package/main/i18ntk-setup.js +9 -404
- package/main/i18ntk-sizing.js +6 -6
- package/main/i18ntk-summary.js +21 -18
- package/main/i18ntk-ui.js +11 -10
- package/main/i18ntk-usage.js +54 -18
- package/main/i18ntk-validate.js +13 -13
- package/main/manage/commands/AnalyzeCommand.js +1124 -0
- package/main/manage/commands/BackupCommand.js +62 -0
- package/main/manage/commands/CommandRouter.js +295 -0
- package/main/manage/commands/CompleteCommand.js +61 -0
- package/main/manage/commands/DoctorCommand.js +60 -0
- package/main/manage/commands/FixerCommand.js +624 -0
- package/main/manage/commands/InitCommand.js +62 -0
- package/main/manage/commands/ScannerCommand.js +654 -0
- package/main/manage/commands/SizingCommand.js +60 -0
- package/main/manage/commands/SummaryCommand.js +61 -0
- package/main/manage/commands/UsageCommand.js +60 -0
- package/main/manage/commands/ValidateCommand.js +978 -0
- package/main/manage/index-fixed.js +1447 -0
- package/main/manage/index.js +1462 -0
- package/main/manage/managers/DebugMenu.js +140 -0
- package/main/manage/managers/InteractiveMenu.js +177 -0
- package/main/manage/managers/LanguageMenu.js +62 -0
- package/main/manage/managers/SettingsMenu.js +53 -0
- package/main/manage/services/AuthenticationService.js +263 -0
- package/main/manage/services/ConfigurationService-fixed.js +449 -0
- package/main/manage/services/ConfigurationService.js +449 -0
- package/main/manage/services/FileManagementService.js +368 -0
- package/main/manage/services/FrameworkDetectionService.js +458 -0
- package/main/manage/services/InitService.js +1051 -0
- package/main/manage/services/SetupService.js +462 -0
- package/main/manage/services/SummaryService.js +450 -0
- package/main/manage/services/UsageService.js +1502 -0
- package/package.json +32 -29
- package/runtime/enhanced.d.ts +221 -221
- package/runtime/index.d.ts +29 -29
- package/runtime/index.full.d.ts +331 -331
- package/runtime/index.js +7 -6
- package/scripts/build-lite.js +17 -17
- package/scripts/deprecate-versions.js +23 -6
- package/scripts/export-translations.js +5 -5
- package/scripts/fix-all-i18n.js +3 -3
- package/scripts/fix-and-purify-i18n.js +3 -2
- package/scripts/fix-locale-control-chars.js +110 -0
- package/scripts/lint-locales.js +80 -0
- package/scripts/locale-optimizer.js +8 -8
- package/scripts/prepublish.js +21 -21
- package/scripts/security-check.js +117 -117
- package/scripts/sync-translations.js +4 -4
- package/scripts/sync-ui-locales.js +9 -8
- package/scripts/validate-all-translations.js +8 -7
- package/scripts/verify-deprecations.js +157 -161
- package/scripts/verify-translations.js +6 -5
- package/settings/i18ntk-config.json +282 -282
- package/settings/language-config.json +5 -5
- package/settings/settings-cli.js +9 -9
- package/settings/settings-manager.js +18 -18
- package/ui-locales/de.json +2417 -2348
- package/ui-locales/en.json +2415 -2352
- package/ui-locales/es.json +2425 -2353
- package/ui-locales/fr.json +2418 -2348
- package/ui-locales/ja.json +2463 -2361
- package/ui-locales/ru.json +2463 -2359
- package/ui-locales/zh.json +2418 -2351
- package/utils/admin-auth.js +2 -2
- package/utils/admin-cli.js +297 -297
- package/utils/admin-pin.js +9 -9
- package/utils/cli-helper.js +9 -9
- package/utils/config-helper.js +73 -104
- package/utils/config-manager.js +204 -171
- package/utils/config.js +5 -4
- package/utils/env-manager.js +249 -263
- package/utils/framework-detector.js +27 -24
- package/utils/i18n-helper.js +85 -41
- package/utils/init-helper.js +152 -94
- package/utils/json-output.js +98 -98
- package/utils/mini-commander.js +179 -0
- package/utils/missing-key-validator.js +5 -5
- package/utils/plugin-loader.js +40 -29
- package/utils/prompt.js +14 -44
- package/utils/safe-json.js +40 -0
- package/utils/secure-errors.js +3 -3
- package/utils/security-check-improved.js +390 -0
- package/utils/security-config.js +5 -5
- package/utils/security-fixed.js +607 -0
- package/utils/security.js +652 -602
- package/utils/setup-enforcer.js +136 -44
- package/utils/setup-validator.js +33 -32
- package/utils/ultra-performance-optimizer.js +11 -9
- package/utils/watch-locales.js +2 -1
- package/utils/prompt-fixed.js +0 -55
- package/utils/security-check.js +0 -454
package/main/i18ntk-analyze.js
CHANGED
|
@@ -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('
|
|
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
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1044
|
-
|
|
1045
|
-
|
|
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(`\
|
|
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(
|
|
1047
|
+
console.log(`Report saved: ${language}.json`);
|
|
1066
1048
|
}
|
|
1067
1049
|
} catch (error) {
|
|
1068
|
-
console.error(
|
|
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('\
|
|
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;
|
package/main/i18ntk-backup.js
CHANGED
|
@@ -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
|
|
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 (!
|
|
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 (!
|
|
303
|
+
if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
|
|
304
304
|
throw new Error(`Backup file not found: ${backupPath}`);
|
|
305
305
|
}
|
|
306
306
|
|