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.
- package/README.md +87 -50
- package/main/i18ntk-analyze.js +63 -63
- package/main/i18ntk-backup-class.js +37 -41
- package/main/i18ntk-backup.js +28 -30
- package/main/i18ntk-complete.js +75 -74
- package/main/i18ntk-doctor.js +7 -6
- package/main/i18ntk-fixer.js +3 -3
- package/main/i18ntk-init.js +49 -13
- package/main/i18ntk-scanner.js +2 -2
- package/main/i18ntk-sizing.js +36 -37
- package/main/i18ntk-summary.js +4 -4
- package/main/i18ntk-ui.js +95 -96
- package/main/i18ntk-usage.js +31 -19
- package/main/i18ntk-validate.js +78 -27
- package/main/manage/commands/AnalyzeCommand.js +71 -73
- package/main/manage/commands/CommandRouter.js +15 -12
- package/main/manage/commands/FixerCommand.js +94 -38
- package/main/manage/commands/ScannerCommand.js +2 -2
- package/main/manage/commands/ValidateCommand.js +87 -36
- package/main/manage/index.js +165 -152
- package/main/manage/managers/DebugMenu.js +6 -6
- package/main/manage/managers/InteractiveMenu.js +6 -6
- package/main/manage/managers/LanguageMenu.js +12 -6
- package/main/manage/managers/SettingsMenu.js +6 -6
- package/main/manage/services/AuthenticationService.js +5 -6
- package/main/manage/services/ConfigurationService.js +22 -34
- package/main/manage/services/FileManagementService.js +6 -6
- package/main/manage/services/InitService.js +44 -8
- package/main/manage/services/UsageService.js +24 -12
- package/package.json +21 -42
- package/settings/settings-cli.js +5 -5
- package/settings/settings-manager.js +984 -968
- package/ui-locales/de.json +12 -11
- package/ui-locales/en.json +12 -11
- package/ui-locales/es.json +12 -11
- package/ui-locales/fr.json +12 -11
- package/ui-locales/ja.json +12 -11
- package/ui-locales/ru.json +12 -11
- package/ui-locales/zh.json +12 -11
- package/utils/config-helper.js +27 -16
- package/utils/config-manager.js +8 -7
- package/utils/i18n-helper.js +161 -166
- package/utils/init-helper.js +3 -2
- package/utils/json-output.js +11 -10
- package/{scripts → utils}/locale-optimizer.js +61 -60
- package/utils/logger.js +4 -4
- package/utils/safe-json.js +3 -3
- package/utils/secure-backup.js +8 -7
- package/utils/setup-enforcer.js +63 -98
- package/main/i18ntk-go.js +0 -283
- package/main/i18ntk-java.js +0 -380
- package/main/i18ntk-js.js +0 -512
- package/main/i18ntk-manage.js +0 -1694
- package/main/i18ntk-php.js +0 -462
- package/main/i18ntk-py.js +0 -379
- package/main/i18ntk-settings.js +0 -23
- package/main/manage/index-fixed.js +0 -1447
- package/scripts/build-lite.js +0 -279
- package/scripts/deprecate-versions.js +0 -317
- package/scripts/export-translations.js +0 -84
- package/scripts/fix-all-i18n.js +0 -236
- package/scripts/fix-and-purify-i18n.js +0 -233
- package/scripts/fix-locale-control-chars.js +0 -110
- package/scripts/lint-locales.js +0 -80
- package/scripts/prepublish-dev.js +0 -221
- package/scripts/prepublish.js +0 -362
- package/scripts/security-check.js +0 -117
- package/scripts/sync-translations.js +0 -151
- package/scripts/sync-ui-locales.js +0 -20
- package/scripts/validate-all-translations.js +0 -195
- package/scripts/verify-deprecations.js +0 -157
- package/scripts/verify-translations.js +0 -63
- package/utils/security-fixed.js +0 -609
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# i18ntk v2.
|
|
1
|
+
# i18ntk v2.3.0
|
|
2
2
|
|
|
3
|
-
Zero-dependency
|
|
3
|
+
Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, and translation completion.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -9,52 +9,76 @@ Zero-dependency i18n toolkit for initialization, scanning, analysis, validation,
|
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](https://www.npmjs.com/package/i18ntk)
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[](https://socket.dev/npm/package/i18ntk/overview/2.3.0)
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Upgrade Notice
|
|
15
|
+
|
|
16
|
+
Versions earlier than `2.3.0` may contain known stability and security issues.
|
|
17
|
+
They are considered unsupported for production use. Upgrade to `2.3.0` or newer.
|
|
18
|
+
|
|
19
|
+
## What i18ntk Does
|
|
15
20
|
|
|
16
21
|
- Zero runtime dependencies
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
22
|
+
- Interactive and non-interactive project setup
|
|
23
|
+
- Translation completeness analysis and usage tracking
|
|
24
|
+
- Validation, sizing, and summary reporting
|
|
25
|
+
- Missing-key completion and fixer workflows
|
|
26
|
+
- Runtime translation helpers for application code
|
|
27
|
+
- Support for JS/TS, React, Vue, Angular, and generic projects
|
|
28
|
+
|
|
29
|
+
## Getting Started
|
|
30
|
+
|
|
31
|
+
1. Install the package.
|
|
32
|
+
2. Run `i18ntk` or `i18ntk --command=init` to initialize the project.
|
|
33
|
+
3. Confirm the source language and locale directories.
|
|
34
|
+
4. Run `i18ntk --command=analyze` or `i18ntk --command=validate` to inspect translation coverage.
|
|
35
|
+
5. Use `i18ntk --command=complete` to fill missing keys when needed.
|
|
36
|
+
|
|
37
|
+
The full onboarding flow is documented in [docs/getting-started.md](docs/getting-started.md).
|
|
21
38
|
|
|
22
39
|
## Install
|
|
23
40
|
|
|
24
41
|
```bash
|
|
25
|
-
# global
|
|
42
|
+
# global CLI use
|
|
26
43
|
npm install -g i18ntk
|
|
27
44
|
|
|
28
|
-
# local
|
|
45
|
+
# local project use
|
|
29
46
|
npm install --save-dev i18ntk
|
|
30
47
|
|
|
31
|
-
# one-off
|
|
48
|
+
# one-off execution
|
|
32
49
|
npx i18ntk --help
|
|
33
50
|
```
|
|
34
51
|
|
|
35
|
-
##
|
|
52
|
+
## Setup
|
|
53
|
+
|
|
54
|
+
The toolkit stores project configuration in `.i18ntk-config` at the project root.
|
|
55
|
+
|
|
56
|
+
Recommended setup flow:
|
|
36
57
|
|
|
37
58
|
```bash
|
|
38
|
-
|
|
59
|
+
i18ntk
|
|
60
|
+
# or
|
|
39
61
|
i18ntk --command=init
|
|
62
|
+
```
|
|
40
63
|
|
|
41
|
-
|
|
42
|
-
i18ntk --command=analyze
|
|
64
|
+
During setup, you can define:
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
- source directory
|
|
67
|
+
- source language
|
|
68
|
+
- UI language
|
|
69
|
+
- framework preference
|
|
70
|
+
- output directory
|
|
71
|
+
- backup behavior
|
|
46
72
|
|
|
47
|
-
|
|
48
|
-
i18ntk --command=complete
|
|
49
|
-
```
|
|
73
|
+
If you run in CI or a non-interactive shell, use:
|
|
50
74
|
|
|
51
|
-
|
|
75
|
+
```bash
|
|
76
|
+
i18ntk --command=init --no-prompt
|
|
77
|
+
```
|
|
52
78
|
|
|
53
|
-
|
|
79
|
+
## Daily Use
|
|
54
80
|
|
|
55
81
|
```bash
|
|
56
|
-
i18ntk
|
|
57
|
-
i18ntk --command=init
|
|
58
82
|
i18ntk --command=analyze
|
|
59
83
|
i18ntk --command=validate
|
|
60
84
|
i18ntk --command=usage
|
|
@@ -62,10 +86,9 @@ i18ntk --command=scanner
|
|
|
62
86
|
i18ntk --command=sizing
|
|
63
87
|
i18ntk --command=complete
|
|
64
88
|
i18ntk --command=summary
|
|
65
|
-
i18ntk --command=debug
|
|
66
89
|
```
|
|
67
90
|
|
|
68
|
-
Standalone
|
|
91
|
+
Standalone commands are also available:
|
|
69
92
|
|
|
70
93
|
```bash
|
|
71
94
|
i18ntk-init
|
|
@@ -92,15 +115,43 @@ i18ntk-backup
|
|
|
92
115
|
- `--dry-run`
|
|
93
116
|
- `--help`
|
|
94
117
|
|
|
95
|
-
|
|
118
|
+
Example:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
i18ntk --command=analyze --source-dir=./src --i18n-dir=./locales --output-dir=./i18ntk-reports
|
|
122
|
+
```
|
|
96
123
|
|
|
97
|
-
|
|
124
|
+
## Runtime API
|
|
98
125
|
|
|
99
|
-
|
|
126
|
+
Use `i18ntk/runtime` when your application needs to read locale JSON files at runtime.
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const runtime = require('i18ntk/runtime');
|
|
130
|
+
|
|
131
|
+
runtime.initRuntime({
|
|
132
|
+
baseDir: './locales',
|
|
133
|
+
language: 'en',
|
|
134
|
+
fallbackLanguage: 'en',
|
|
135
|
+
keySeparator: '.',
|
|
136
|
+
preload: true
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
console.log(runtime.t('common.hello'));
|
|
140
|
+
runtime.setLanguage('fr');
|
|
141
|
+
console.log(runtime.getLanguage());
|
|
142
|
+
console.log(runtime.getAvailableLanguages());
|
|
143
|
+
runtime.refresh('fr');
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For a deeper walkthrough, see [docs/runtime.md](docs/runtime.md).
|
|
147
|
+
|
|
148
|
+
## Configuration
|
|
149
|
+
|
|
150
|
+
Example `.i18ntk-config`:
|
|
100
151
|
|
|
101
152
|
```json
|
|
102
153
|
{
|
|
103
|
-
"version": "2.
|
|
154
|
+
"version": "2.3.0",
|
|
104
155
|
"sourceDir": "./locales",
|
|
105
156
|
"i18nDir": "./locales",
|
|
106
157
|
"outputDir": "./i18ntk-reports",
|
|
@@ -112,34 +163,20 @@ Example:
|
|
|
112
163
|
}
|
|
113
164
|
```
|
|
114
165
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
```ts
|
|
118
|
-
import { initRuntime, t, setLanguage, getLanguage } from 'i18ntk/runtime';
|
|
119
|
-
|
|
120
|
-
initRuntime({
|
|
121
|
-
baseDir: './locales',
|
|
122
|
-
language: 'en',
|
|
123
|
-
fallbackLanguage: 'en',
|
|
124
|
-
preload: true
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
console.log(t('common.hello'));
|
|
128
|
-
setLanguage('fr');
|
|
129
|
-
console.log(getLanguage());
|
|
130
|
-
```
|
|
166
|
+
See [docs/api/CONFIGURATION.md](docs/api/CONFIGURATION.md) for the full configuration model.
|
|
131
167
|
|
|
132
|
-
##
|
|
168
|
+
## Docs
|
|
133
169
|
|
|
134
170
|
- [Documentation Index](docs/README.md)
|
|
171
|
+
- [Getting Started](docs/getting-started.md)
|
|
135
172
|
- [API Reference](docs/api/API_REFERENCE.md)
|
|
136
173
|
- [Configuration Guide](docs/api/CONFIGURATION.md)
|
|
137
174
|
- [Runtime API Guide](docs/runtime.md)
|
|
138
175
|
- [Scanner Guide](docs/scanner-guide.md)
|
|
139
176
|
- [Environment Variables](docs/environment-variables.md)
|
|
140
|
-
- [Migration Guide v2.
|
|
141
|
-
- [
|
|
177
|
+
- [Migration Guide v2.3.0](docs/migration-guide-v2.3.0.md)
|
|
178
|
+
- [Optimization Prompt](docs/development/package-optimization-prompt.md)
|
|
142
179
|
|
|
143
180
|
## License
|
|
144
181
|
|
|
145
|
-
MIT
|
|
182
|
+
MIT
|
package/main/i18ntk-analyze.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* I18NTK TRANSLATION ANALYSIS SCRIPT
|
|
4
4
|
*
|
|
@@ -27,7 +27,7 @@ const SetupEnforcer = require('../utils/setup-enforcer');
|
|
|
27
27
|
}
|
|
28
28
|
})();
|
|
29
29
|
|
|
30
|
-
loadTranslations(
|
|
30
|
+
loadTranslations('en', path.resolve(__dirname, '..', '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, '..', '
|
|
70
|
+
loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
|
|
71
71
|
|
|
72
72
|
this.sourceDir = this.config.sourceDir;
|
|
73
73
|
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
@@ -142,44 +142,44 @@ class I18nAnalyzer {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// Get all available languages
|
|
146
|
-
isValidLanguageCode(code) {
|
|
147
|
-
if (!code || typeof code !== 'string') return false;
|
|
148
|
-
return /^[a-z]{2}(?:-[A-Za-z0-9]{2,8})*$/i.test(code.trim());
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
isExcludedLanguageDirectory(name) {
|
|
152
|
-
if (!name || typeof name !== 'string') return true;
|
|
153
|
-
const lowered = name.toLowerCase();
|
|
154
|
-
return lowered.startsWith('backup-') || lowered === 'backup' || lowered === 'reports' || lowered === 'i18ntk-reports';
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
getAvailableLanguages() {
|
|
158
|
-
try {
|
|
159
|
-
const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
|
|
160
|
-
if (!items) {
|
|
161
|
-
console.error('Error reading source directory: Unable to access directory');
|
|
145
|
+
// Get all available languages
|
|
146
|
+
isValidLanguageCode(code) {
|
|
147
|
+
if (!code || typeof code !== 'string') return false;
|
|
148
|
+
return /^[a-z]{2}(?:-[A-Za-z0-9]{2,8})*$/i.test(code.trim());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
isExcludedLanguageDirectory(name) {
|
|
152
|
+
if (!name || typeof name !== 'string') return true;
|
|
153
|
+
const lowered = name.toLowerCase();
|
|
154
|
+
return lowered.startsWith('backup-') || lowered === 'backup' || lowered === 'reports' || lowered === 'i18ntk-reports';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getAvailableLanguages() {
|
|
158
|
+
try {
|
|
159
|
+
const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
|
|
160
|
+
if (!items) {
|
|
161
|
+
console.error('Error reading source directory: Unable to access directory');
|
|
162
162
|
return [];
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
const languages = [];
|
|
166
166
|
|
|
167
167
|
// Check for directory-based structure
|
|
168
|
-
const directories = items
|
|
169
|
-
.filter(item => item.isDirectory())
|
|
170
|
-
.map(item => item.name)
|
|
171
|
-
.filter(name =>
|
|
172
|
-
name !== 'node_modules' &&
|
|
173
|
-
!name.startsWith('.') &&
|
|
174
|
-
name !== this.config.sourceLanguage &&
|
|
175
|
-
!this.isExcludedLanguageDirectory(name) &&
|
|
176
|
-
this.isValidLanguageCode(name)
|
|
177
|
-
)
|
|
178
|
-
.filter(name => {
|
|
179
|
-
const dirPath = path.join(this.sourceDir, name);
|
|
180
|
-
const dirItems = SecurityUtils.safeReaddirSync(dirPath, process.cwd(), { withFileTypes: true }) || [];
|
|
181
|
-
return dirItems.some(item => item.isFile() && item.name.endsWith('.json'));
|
|
182
|
-
});
|
|
168
|
+
const directories = items
|
|
169
|
+
.filter(item => item.isDirectory())
|
|
170
|
+
.map(item => item.name)
|
|
171
|
+
.filter(name =>
|
|
172
|
+
name !== 'node_modules' &&
|
|
173
|
+
!name.startsWith('.') &&
|
|
174
|
+
name !== this.config.sourceLanguage &&
|
|
175
|
+
!this.isExcludedLanguageDirectory(name) &&
|
|
176
|
+
this.isValidLanguageCode(name)
|
|
177
|
+
)
|
|
178
|
+
.filter(name => {
|
|
179
|
+
const dirPath = path.join(this.sourceDir, name);
|
|
180
|
+
const dirItems = SecurityUtils.safeReaddirSync(dirPath, process.cwd(), { withFileTypes: true }) || [];
|
|
181
|
+
return dirItems.some(item => item.isFile() && item.name.endsWith('.json'));
|
|
182
|
+
});
|
|
183
183
|
|
|
184
184
|
// Check for monolith files (language.json files)
|
|
185
185
|
const files = items
|
|
@@ -190,17 +190,17 @@ class I18nAnalyzer {
|
|
|
190
190
|
languages.push(...directories);
|
|
191
191
|
|
|
192
192
|
// Add monolith files as languages (without .json extension)
|
|
193
|
-
const monolithLanguages = files
|
|
194
|
-
.map(file => file.replace('.json', ''))
|
|
195
|
-
.filter(lang =>
|
|
196
|
-
!languages.includes(lang) &&
|
|
197
|
-
lang !== this.config.sourceLanguage &&
|
|
198
|
-
!this.isExcludedLanguageDirectory(lang) &&
|
|
199
|
-
this.isValidLanguageCode(lang)
|
|
200
|
-
);
|
|
201
|
-
languages.push(...monolithLanguages);
|
|
202
|
-
|
|
203
|
-
return [...new Set(languages)].sort();
|
|
193
|
+
const monolithLanguages = files
|
|
194
|
+
.map(file => file.replace('.json', ''))
|
|
195
|
+
.filter(lang =>
|
|
196
|
+
!languages.includes(lang) &&
|
|
197
|
+
lang !== this.config.sourceLanguage &&
|
|
198
|
+
!this.isExcludedLanguageDirectory(lang) &&
|
|
199
|
+
this.isValidLanguageCode(lang)
|
|
200
|
+
);
|
|
201
|
+
languages.push(...monolithLanguages);
|
|
202
|
+
|
|
203
|
+
return [...new Set(languages)].sort();
|
|
204
204
|
} catch (error) {
|
|
205
205
|
console.error('Error reading source directory:', error.message);
|
|
206
206
|
return [];
|
|
@@ -712,7 +712,7 @@ try {
|
|
|
712
712
|
throw new Error(t('analyze.failedToWriteReportFile') || 'Failed to write report file securely');
|
|
713
713
|
}
|
|
714
714
|
|
|
715
|
-
return reportPath;
|
|
715
|
+
return reportPath;
|
|
716
716
|
|
|
717
717
|
} catch (error) {
|
|
718
718
|
console.error(`Failed to save report for ${language}:`, error.message);
|
|
@@ -779,21 +779,21 @@ try {
|
|
|
779
779
|
console.log(t('analyze.analyzing', { language }) || `\n🔄 Analyzing ${language}...`);
|
|
780
780
|
}
|
|
781
781
|
|
|
782
|
-
const analysis = this.analyzeLanguage(language);
|
|
783
|
-
const report = this.generateLanguageReport(analysis);
|
|
784
|
-
|
|
785
|
-
// Save report
|
|
786
|
-
const reportPath = await this.saveReport(language, report);
|
|
787
|
-
const processedCount = results.length + 1;
|
|
788
|
-
|
|
789
|
-
if (!args.json) {
|
|
790
|
-
console.log(t('analyze.completed', { language }) || `✅ Analysis completed for ${language}`);
|
|
791
|
-
console.log(t('analyze.progress', {
|
|
792
|
-
translated: processedCount,
|
|
793
|
-
total: languages.length
|
|
794
|
-
}) || ` Progress: ${processedCount}/${languages.length} languages processed`);
|
|
795
|
-
console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
|
|
796
|
-
}
|
|
782
|
+
const analysis = this.analyzeLanguage(language);
|
|
783
|
+
const report = this.generateLanguageReport(analysis);
|
|
784
|
+
|
|
785
|
+
// Save report
|
|
786
|
+
const reportPath = await this.saveReport(language, report);
|
|
787
|
+
const processedCount = results.length + 1;
|
|
788
|
+
|
|
789
|
+
if (!args.json) {
|
|
790
|
+
console.log(t('analyze.completed', { language }) || `✅ Analysis completed for ${language}`);
|
|
791
|
+
console.log(t('analyze.progress', {
|
|
792
|
+
translated: processedCount,
|
|
793
|
+
total: languages.length
|
|
794
|
+
}) || ` Progress: ${processedCount}/${languages.length} languages processed`);
|
|
795
|
+
console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
|
|
796
|
+
}
|
|
797
797
|
|
|
798
798
|
results.push({
|
|
799
799
|
language,
|
|
@@ -889,7 +889,7 @@ try {
|
|
|
889
889
|
this.config = { ...baseConfig, ...this.config };
|
|
890
890
|
|
|
891
891
|
const uiLanguage = this.config.uiLanguage || 'en';
|
|
892
|
-
loadTranslations(uiLanguage, path.resolve(__dirname, '..', '
|
|
892
|
+
loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
|
|
893
893
|
|
|
894
894
|
this.sourceDir = this.config.sourceDir;
|
|
895
895
|
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
-
const fs = require('fs
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const configManager = require('../utils/config-manager');
|
|
9
|
-
const { logger } = require('../utils/logger');
|
|
10
|
-
const { colors } = require('../utils/logger');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const fsp = fs.promises;
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const configManager = require('../utils/config-manager');
|
|
9
|
+
const { logger } = require('../utils/logger');
|
|
10
|
+
const { colors } = require('../utils/logger');
|
|
11
11
|
const SecurityUtils = require('../utils/security');
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -16,10 +16,10 @@ const SecurityUtils = require('../utils/security');
|
|
|
16
16
|
* Class-based implementation of backup functionality for use with CommandRouter
|
|
17
17
|
*/
|
|
18
18
|
class I18nBackup {
|
|
19
|
-
constructor(config = {}) {
|
|
20
|
-
this.config = config;
|
|
21
|
-
this.backupDir = path.join(process.cwd(), '
|
|
22
|
-
this.maxBackups = config.backup?.maxBackups ||
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.backupDir = path.join(process.cwd(), 'i18ntk-backups');
|
|
22
|
+
this.maxBackups = Math.min(Math.max(parseInt(config.backup?.maxBackups, 10) || 1, 1), 3);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -93,7 +93,7 @@ Options:
|
|
|
93
93
|
|
|
94
94
|
// Create backup directory if it doesn't exist
|
|
95
95
|
try {
|
|
96
|
-
await
|
|
96
|
+
await fsp.mkdir(outputDir, { recursive: true });
|
|
97
97
|
logger.debug(`Created backup directory: ${outputDir}`);
|
|
98
98
|
} catch (err) {
|
|
99
99
|
if (err.code !== 'EEXIST') {
|
|
@@ -106,7 +106,7 @@ Options:
|
|
|
106
106
|
// Validate directory
|
|
107
107
|
const sourceDir = path.resolve(dir);
|
|
108
108
|
try {
|
|
109
|
-
const stats = await
|
|
109
|
+
const stats = await fsp.stat(sourceDir);
|
|
110
110
|
if (!stats.isDirectory()) {
|
|
111
111
|
throw new Error(`Path exists but is not a directory: ${sourceDir}`);
|
|
112
112
|
}
|
|
@@ -121,7 +121,7 @@ Options:
|
|
|
121
121
|
logger.info('\nCreating backup...');
|
|
122
122
|
|
|
123
123
|
// Read all files in the directory
|
|
124
|
-
const files = (await
|
|
124
|
+
const files = (await fsp.readdir(sourceDir, { withFileTypes: true }))
|
|
125
125
|
.filter(dirent => dirent.isFile() && dirent.name.endsWith('.json'))
|
|
126
126
|
.map(dirent => dirent.name);
|
|
127
127
|
|
|
@@ -135,7 +135,7 @@ Options:
|
|
|
135
135
|
for (const file of files) {
|
|
136
136
|
const filePath = path.join(sourceDir, file);
|
|
137
137
|
try {
|
|
138
|
-
const content = JSON.parse(await
|
|
138
|
+
const content = JSON.parse(await fsp.readFile(filePath, 'utf8'));
|
|
139
139
|
translations[file] = content;
|
|
140
140
|
} catch (error) {
|
|
141
141
|
logger.error(`Could not read file ${file}: ${error.message}`);
|
|
@@ -143,8 +143,8 @@ Options:
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// Create the backup
|
|
146
|
-
await
|
|
147
|
-
const stats = await
|
|
146
|
+
await fsp.writeFile(backupPath, JSON.stringify(translations, null, 2));
|
|
147
|
+
const stats = await fsp.stat(backupPath);
|
|
148
148
|
|
|
149
149
|
logger.success('Backup created successfully');
|
|
150
150
|
logger.info(` Location: ${backupPath}`);
|
|
@@ -218,7 +218,7 @@ Options:
|
|
|
218
218
|
try {
|
|
219
219
|
// Ensure backup directory exists
|
|
220
220
|
try {
|
|
221
|
-
await
|
|
221
|
+
await fsp.access(this.backupDir);
|
|
222
222
|
} catch (err) {
|
|
223
223
|
if (err.code === 'ENOENT') {
|
|
224
224
|
logger.warn('No backups found. The backup directory does not exist yet.');
|
|
@@ -228,14 +228,14 @@ Options:
|
|
|
228
228
|
return { success: true, backups: [] };
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
const files = await
|
|
231
|
+
const files = await fsp.readdir(this.backupDir);
|
|
232
232
|
const backups = [];
|
|
233
233
|
|
|
234
234
|
for (const file of files) {
|
|
235
235
|
if (file.startsWith('backup-') && file.endsWith('.json')) {
|
|
236
236
|
try {
|
|
237
237
|
const filePath = path.join(this.backupDir, file);
|
|
238
|
-
const stats = await
|
|
238
|
+
const stats = await fsp.stat(filePath);
|
|
239
239
|
backups.push({
|
|
240
240
|
name: file,
|
|
241
241
|
path: filePath,
|
|
@@ -298,14 +298,14 @@ Options:
|
|
|
298
298
|
logger.info('\nVerifying backup...');
|
|
299
299
|
|
|
300
300
|
try {
|
|
301
|
-
const data = await
|
|
301
|
+
const data = await fsp.readFile(backupPath, 'utf8');
|
|
302
302
|
const content = JSON.parse(data);
|
|
303
303
|
|
|
304
304
|
if (typeof content === 'object' && content !== null) {
|
|
305
305
|
const fileCount = Object.keys(content).length;
|
|
306
306
|
logger.success('Backup is valid');
|
|
307
307
|
logger.info(` Contains ${fileCount} translation files`);
|
|
308
|
-
logger.info(` Last modified: ${(await
|
|
308
|
+
logger.info(` Last modified: ${(await fsp.stat(backupPath)).mtime.toLocaleString()}`);
|
|
309
309
|
|
|
310
310
|
return {
|
|
311
311
|
success: true,
|
|
@@ -334,14 +334,14 @@ Options:
|
|
|
334
334
|
logger.info('\nCleaning up old backups...');
|
|
335
335
|
|
|
336
336
|
try {
|
|
337
|
-
const files = await
|
|
337
|
+
const files = await fsp.readdir(this.backupDir);
|
|
338
338
|
const backupFiles = files
|
|
339
339
|
.filter(file => file.startsWith('backup-') && file.endsWith('.json'))
|
|
340
340
|
.map(file => ({
|
|
341
341
|
name: file,
|
|
342
|
-
path: path.join(this.backupDir, file),
|
|
343
|
-
time: fs.statSync(path.join(this.backupDir, file)).mtime.getTime()
|
|
344
|
-
}))
|
|
342
|
+
path: path.join(this.backupDir, file),
|
|
343
|
+
time: fs.statSync(path.join(this.backupDir, file)).mtime.getTime()
|
|
344
|
+
}))
|
|
345
345
|
.sort((a, b) => b.time - a.time);
|
|
346
346
|
|
|
347
347
|
// Keep only the most recent 'keep' files
|
|
@@ -355,7 +355,7 @@ Options:
|
|
|
355
355
|
// Delete old backups
|
|
356
356
|
for (const file of toDelete) {
|
|
357
357
|
try {
|
|
358
|
-
await
|
|
358
|
+
await fsp.unlink(file.path);
|
|
359
359
|
logger.info(` - Deleted: ${file.name}`);
|
|
360
360
|
} catch (err) {
|
|
361
361
|
logger.error(` - Failed to delete ${file.name}: ${err.message}`);
|
|
@@ -374,12 +374,10 @@ Options:
|
|
|
374
374
|
} catch (error) {
|
|
375
375
|
logger.error('Error cleaning up backups:');
|
|
376
376
|
logger.error(` ${error.message}`);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
}
|
|
377
|
+
logger.debug(error.stack || error.message);
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
383
381
|
|
|
384
382
|
async cleanupOldBackups(outputDir) {
|
|
385
383
|
try {
|
|
@@ -408,13 +406,11 @@ Options:
|
|
|
408
406
|
}
|
|
409
407
|
}
|
|
410
408
|
|
|
411
|
-
handleError(error) {
|
|
412
|
-
logger.error('Backup operation failed:');
|
|
413
|
-
logger.error(` ${error.message}`);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
}
|
|
409
|
+
handleError(error) {
|
|
410
|
+
logger.error('Backup operation failed:');
|
|
411
|
+
logger.error(` ${error.message}`);
|
|
412
|
+
logger.debug(error.stack || error.message);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
419
415
|
|
|
420
|
-
module.exports = I18nBackup;
|
|
416
|
+
module.exports = I18nBackup;
|