i18ntk 2.2.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 +83 -50
- package/main/i18ntk-backup-class.js +37 -41
- package/main/i18ntk-backup.js +28 -30
- package/main/i18ntk-doctor.js +7 -6
- package/main/i18ntk-init.js +44 -8
- package/main/i18ntk-sizing.js +7 -8
- package/main/i18ntk-usage.js +17 -5
- package/main/i18ntk-validate.js +72 -22
- package/main/manage/commands/AnalyzeCommand.js +12 -14
- package/main/manage/commands/CommandRouter.js +15 -12
- package/main/manage/commands/FixerCommand.js +92 -36
- package/main/manage/commands/ValidateCommand.js +78 -27
- package/main/manage/index.js +158 -148
- package/main/manage/managers/DebugMenu.js +6 -6
- package/main/manage/managers/InteractiveMenu.js +6 -6
- package/main/manage/managers/LanguageMenu.js +5 -4
- 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 +17 -5
- package/package.json +6 -6
- package/settings/settings-cli.js +2 -2
- package/settings/settings-manager.js +984 -968
- package/utils/config-helper.js +27 -16
- package/utils/config-manager.js +8 -7
- package/utils/init-helper.js +3 -2
- package/utils/json-output.js +11 -10
- 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/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,57 +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
15
|
|
|
16
|
-
Versions earlier than `2.
|
|
17
|
-
They are considered unsupported for production use. Upgrade to `2.
|
|
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
18
|
|
|
19
|
-
##
|
|
19
|
+
## What i18ntk Does
|
|
20
20
|
|
|
21
21
|
- Zero runtime dependencies
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
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).
|
|
26
38
|
|
|
27
39
|
## Install
|
|
28
40
|
|
|
29
41
|
```bash
|
|
30
|
-
# global
|
|
42
|
+
# global CLI use
|
|
31
43
|
npm install -g i18ntk
|
|
32
44
|
|
|
33
|
-
# local
|
|
45
|
+
# local project use
|
|
34
46
|
npm install --save-dev i18ntk
|
|
35
47
|
|
|
36
|
-
# one-off
|
|
48
|
+
# one-off execution
|
|
37
49
|
npx i18ntk --help
|
|
38
50
|
```
|
|
39
51
|
|
|
40
|
-
##
|
|
52
|
+
## Setup
|
|
53
|
+
|
|
54
|
+
The toolkit stores project configuration in `.i18ntk-config` at the project root.
|
|
55
|
+
|
|
56
|
+
Recommended setup flow:
|
|
41
57
|
|
|
42
58
|
```bash
|
|
43
|
-
|
|
59
|
+
i18ntk
|
|
60
|
+
# or
|
|
44
61
|
i18ntk --command=init
|
|
62
|
+
```
|
|
45
63
|
|
|
46
|
-
|
|
47
|
-
i18ntk --command=analyze
|
|
64
|
+
During setup, you can define:
|
|
48
65
|
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
- source directory
|
|
67
|
+
- source language
|
|
68
|
+
- UI language
|
|
69
|
+
- framework preference
|
|
70
|
+
- output directory
|
|
71
|
+
- backup behavior
|
|
51
72
|
|
|
52
|
-
|
|
53
|
-
i18ntk --command=complete
|
|
54
|
-
```
|
|
73
|
+
If you run in CI or a non-interactive shell, use:
|
|
55
74
|
|
|
56
|
-
|
|
75
|
+
```bash
|
|
76
|
+
i18ntk --command=init --no-prompt
|
|
77
|
+
```
|
|
57
78
|
|
|
58
|
-
|
|
79
|
+
## Daily Use
|
|
59
80
|
|
|
60
81
|
```bash
|
|
61
|
-
i18ntk
|
|
62
|
-
i18ntk --command=init
|
|
63
82
|
i18ntk --command=analyze
|
|
64
83
|
i18ntk --command=validate
|
|
65
84
|
i18ntk --command=usage
|
|
@@ -67,10 +86,9 @@ i18ntk --command=scanner
|
|
|
67
86
|
i18ntk --command=sizing
|
|
68
87
|
i18ntk --command=complete
|
|
69
88
|
i18ntk --command=summary
|
|
70
|
-
i18ntk --command=debug
|
|
71
89
|
```
|
|
72
90
|
|
|
73
|
-
Standalone
|
|
91
|
+
Standalone commands are also available:
|
|
74
92
|
|
|
75
93
|
```bash
|
|
76
94
|
i18ntk-init
|
|
@@ -97,15 +115,43 @@ i18ntk-backup
|
|
|
97
115
|
- `--dry-run`
|
|
98
116
|
- `--help`
|
|
99
117
|
|
|
100
|
-
|
|
118
|
+
Example:
|
|
101
119
|
|
|
102
|
-
|
|
120
|
+
```bash
|
|
121
|
+
i18ntk --command=analyze --source-dir=./src --i18n-dir=./locales --output-dir=./i18ntk-reports
|
|
122
|
+
```
|
|
103
123
|
|
|
104
|
-
|
|
124
|
+
## Runtime API
|
|
125
|
+
|
|
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`:
|
|
105
151
|
|
|
106
152
|
```json
|
|
107
153
|
{
|
|
108
|
-
"version": "2.
|
|
154
|
+
"version": "2.3.0",
|
|
109
155
|
"sourceDir": "./locales",
|
|
110
156
|
"i18nDir": "./locales",
|
|
111
157
|
"outputDir": "./i18ntk-reports",
|
|
@@ -117,32 +163,19 @@ Example:
|
|
|
117
163
|
}
|
|
118
164
|
```
|
|
119
165
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
```ts
|
|
123
|
-
import { initRuntime, t, setLanguage, getLanguage } from 'i18ntk/runtime';
|
|
124
|
-
|
|
125
|
-
initRuntime({
|
|
126
|
-
baseDir: './locales',
|
|
127
|
-
language: 'en',
|
|
128
|
-
fallbackLanguage: 'en',
|
|
129
|
-
preload: true
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
console.log(t('common.hello'));
|
|
133
|
-
setLanguage('fr');
|
|
134
|
-
console.log(getLanguage());
|
|
135
|
-
```
|
|
166
|
+
See [docs/api/CONFIGURATION.md](docs/api/CONFIGURATION.md) for the full configuration model.
|
|
136
167
|
|
|
137
|
-
##
|
|
168
|
+
## Docs
|
|
138
169
|
|
|
139
170
|
- [Documentation Index](docs/README.md)
|
|
171
|
+
- [Getting Started](docs/getting-started.md)
|
|
140
172
|
- [API Reference](docs/api/API_REFERENCE.md)
|
|
141
173
|
- [Configuration Guide](docs/api/CONFIGURATION.md)
|
|
142
174
|
- [Runtime API Guide](docs/runtime.md)
|
|
143
175
|
- [Scanner Guide](docs/scanner-guide.md)
|
|
144
176
|
- [Environment Variables](docs/environment-variables.md)
|
|
145
|
-
- [Migration Guide v2.
|
|
177
|
+
- [Migration Guide v2.3.0](docs/migration-guide-v2.3.0.md)
|
|
178
|
+
- [Optimization Prompt](docs/development/package-optimization-prompt.md)
|
|
146
179
|
|
|
147
180
|
## License
|
|
148
181
|
|
|
@@ -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;
|
package/main/i18ntk-backup.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
-
const fs = require('fs
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const fsp = fs.promises;
|
|
6
7
|
const path = require('path');
|
|
7
8
|
|
|
8
9
|
// Simple CLI argument parser
|
|
@@ -36,16 +37,15 @@ function parseArgs(args) {
|
|
|
36
37
|
|
|
37
38
|
return result;
|
|
38
39
|
}
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const {
|
|
42
|
-
const { colors } = require('../utils/logger');
|
|
40
|
+
const configManager = require('../utils/config-manager');
|
|
41
|
+
const { logger } = require('../utils/logger');
|
|
42
|
+
const { colors } = require('../utils/logger');
|
|
43
43
|
const prompt = require('../utils/prompt');
|
|
44
44
|
|
|
45
45
|
// Backup configuration
|
|
46
46
|
const config = configManager.getConfig();
|
|
47
|
-
const backupDir = path.join(process.cwd(), '
|
|
48
|
-
const maxBackups = config.backup?.maxBackups ||
|
|
47
|
+
const backupDir = path.join(process.cwd(), 'i18ntk-backups');
|
|
48
|
+
const maxBackups = Math.min(Math.max(parseInt(config.backup?.maxBackups, 10) || 1, 1), 3);
|
|
49
49
|
|
|
50
50
|
// Main function to handle commands
|
|
51
51
|
async function main() {
|
|
@@ -121,7 +121,7 @@ async function handleCreate(args) {
|
|
|
121
121
|
|
|
122
122
|
// Create backup directory if it doesn't exist
|
|
123
123
|
try {
|
|
124
|
-
await
|
|
124
|
+
await fsp.mkdir(outputDir, { recursive: true });
|
|
125
125
|
logger.debug(`Created backup directory: ${outputDir}`);
|
|
126
126
|
} catch (err) {
|
|
127
127
|
if (err.code !== 'EEXIST') {
|
|
@@ -134,7 +134,7 @@ async function handleCreate(args) {
|
|
|
134
134
|
// Validate directory
|
|
135
135
|
const sourceDir = path.resolve(dir);
|
|
136
136
|
try {
|
|
137
|
-
const stats = await
|
|
137
|
+
const stats = await fsp.stat(sourceDir);
|
|
138
138
|
if (!stats.isDirectory()) {
|
|
139
139
|
throw new Error(`Path exists but is not a directory: ${sourceDir}`);
|
|
140
140
|
}
|
|
@@ -149,7 +149,7 @@ async function handleCreate(args) {
|
|
|
149
149
|
logger.info('\nCreating backup...');
|
|
150
150
|
|
|
151
151
|
// Read all files in the directory
|
|
152
|
-
const files = (await
|
|
152
|
+
const files = (await fsp.readdir(sourceDir, { withFileTypes: true }))
|
|
153
153
|
.filter(dirent => dirent.isFile() && dirent.name.endsWith('.json'))
|
|
154
154
|
.map(dirent => dirent.name);
|
|
155
155
|
|
|
@@ -163,7 +163,7 @@ async function handleCreate(args) {
|
|
|
163
163
|
for (const file of files) {
|
|
164
164
|
const filePath = path.join(sourceDir, file);
|
|
165
165
|
try {
|
|
166
|
-
const content = JSON.parse(await
|
|
166
|
+
const content = JSON.parse(await fsp.readFile(filePath, 'utf8'));
|
|
167
167
|
translations[file] = content;
|
|
168
168
|
} catch (error) {
|
|
169
169
|
logger.error(`Could not read file ${file}: ${error.message}`);
|
|
@@ -171,8 +171,8 @@ async function handleCreate(args) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
// Create the backup
|
|
174
|
-
await
|
|
175
|
-
const stats = await
|
|
174
|
+
await fsp.writeFile(backupPath, JSON.stringify(translations, null, 2));
|
|
175
|
+
const stats = await fsp.stat(backupPath);
|
|
176
176
|
|
|
177
177
|
logger.success('Backup created successfully');
|
|
178
178
|
logger.info(` Location: ${backupPath}`);
|
|
@@ -203,12 +203,12 @@ async function handleRestore(args) {
|
|
|
203
203
|
|
|
204
204
|
try {
|
|
205
205
|
// Read the backup file
|
|
206
|
-
const backupData = await
|
|
206
|
+
const backupData = await fsp.readFile(backupPath, 'utf8');
|
|
207
207
|
const translations = JSON.parse(backupData);
|
|
208
208
|
|
|
209
209
|
// Create output directory if it doesn't exist
|
|
210
210
|
try {
|
|
211
|
-
await
|
|
211
|
+
await fsp.mkdir(outputDir, { recursive: true });
|
|
212
212
|
} catch (err) {
|
|
213
213
|
if (err.code !== 'EEXIST') throw err;
|
|
214
214
|
}
|
|
@@ -216,7 +216,7 @@ async function handleRestore(args) {
|
|
|
216
216
|
// Write the restored files
|
|
217
217
|
for (const [file, content] of Object.entries(translations)) {
|
|
218
218
|
const filePath = path.join(outputDir, file);
|
|
219
|
-
await
|
|
219
|
+
await fsp.writeFile(filePath, JSON.stringify(content, null, 2));
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
logger.success('Backup restored successfully');
|
|
@@ -230,7 +230,7 @@ async function handleList() {
|
|
|
230
230
|
try {
|
|
231
231
|
// Ensure backup directory exists
|
|
232
232
|
try {
|
|
233
|
-
await
|
|
233
|
+
await fsp.access(backupDir);
|
|
234
234
|
} catch (err) {
|
|
235
235
|
if (err.code === 'ENOENT') {
|
|
236
236
|
logger.warn('No backups found. The backup directory does not exist yet.');
|
|
@@ -240,14 +240,14 @@ async function handleList() {
|
|
|
240
240
|
return;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
const files = await
|
|
243
|
+
const files = await fsp.readdir(backupDir);
|
|
244
244
|
const backups = [];
|
|
245
245
|
|
|
246
246
|
for (const file of files) {
|
|
247
247
|
if (file.startsWith('backup-') && file.endsWith('.json')) {
|
|
248
248
|
try {
|
|
249
249
|
const filePath = path.join(backupDir, file);
|
|
250
|
-
|
|
250
|
+
const stats = await fsp.stat(filePath);
|
|
251
251
|
backups.push({
|
|
252
252
|
name: file,
|
|
253
253
|
path: filePath,
|
|
@@ -307,14 +307,14 @@ async function handleVerify(args) {
|
|
|
307
307
|
logger.info('\nVerifying backup...');
|
|
308
308
|
|
|
309
309
|
try {
|
|
310
|
-
const data = await
|
|
310
|
+
const data = await fsp.readFile(backupPath, 'utf8');
|
|
311
311
|
const content = JSON.parse(data);
|
|
312
312
|
|
|
313
313
|
if (typeof content === 'object' && content !== null) {
|
|
314
314
|
const fileCount = Object.keys(content).length;
|
|
315
315
|
logger.success('Backup is valid');
|
|
316
316
|
logger.info(` Contains ${fileCount} translation files`);
|
|
317
|
-
logger.info(` Last modified: ${(await
|
|
317
|
+
logger.info(` Last modified: ${(await fsp.stat(backupPath)).mtime.toLocaleString()}`);
|
|
318
318
|
} else {
|
|
319
319
|
throw new Error('Invalid backup format');
|
|
320
320
|
}
|
|
@@ -331,13 +331,13 @@ async function handleCleanup(args) {
|
|
|
331
331
|
logger.info('\nCleaning up old backups...');
|
|
332
332
|
|
|
333
333
|
try {
|
|
334
|
-
const files = await
|
|
334
|
+
const files = await fsp.readdir(backupDir);
|
|
335
335
|
const backupFiles = files
|
|
336
336
|
.filter(file => file.startsWith('backup-') && file.endsWith('.json'))
|
|
337
337
|
.map(file => ({
|
|
338
338
|
name: file,
|
|
339
339
|
path: path.join(backupDir, file),
|
|
340
|
-
time: fs.statSync(path.join(backupDir, file)).mtime.getTime()
|
|
340
|
+
time: fs.statSync(path.join(backupDir, file)).mtime.getTime()
|
|
341
341
|
}))
|
|
342
342
|
.sort((a, b) => b.time - a.time);
|
|
343
343
|
|
|
@@ -352,7 +352,7 @@ async function handleCleanup(args) {
|
|
|
352
352
|
// Delete old backups
|
|
353
353
|
for (const file of toDelete) {
|
|
354
354
|
try {
|
|
355
|
-
await
|
|
355
|
+
await fsp.unlink(file.path);
|
|
356
356
|
logger.info(` - Deleted: ${file.name}`);
|
|
357
357
|
} catch (err) {
|
|
358
358
|
logger.error(` - Failed to delete ${file.name}: ${err.message}`);
|
|
@@ -365,12 +365,10 @@ async function handleCleanup(args) {
|
|
|
365
365
|
} catch (error) {
|
|
366
366
|
logger.error('Error cleaning up backups:');
|
|
367
367
|
logger.error(` ${error.message}`);
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
}
|
|
368
|
+
logger.debug(error.stack || error.message);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
374
372
|
|
|
375
373
|
// Start the application
|
|
376
374
|
// Handle unhandled promise rejections
|
package/main/i18ntk-doctor.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const SecurityUtils = require('../utils/security');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const
|
|
2
|
+
const SecurityUtils = require('../utils/security');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const packageJson = require('../package.json');
|
|
6
|
+
const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../utils/config-helper');
|
|
6
7
|
const SetupEnforcer = require('../utils/setup-enforcer');
|
|
7
8
|
|
|
8
9
|
// Ensure setup is complete before running (only for standalone execution)
|
|
@@ -97,7 +98,7 @@ class I18nDoctor {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
const pkgVersion =
|
|
101
|
+
const pkgVersion = packageJson.version;
|
|
101
102
|
if (config.version && config.version !== pkgVersion) {
|
|
102
103
|
issues.push(`Config version mismatch: ${config.version} != ${pkgVersion}`);
|
|
103
104
|
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
@@ -181,4 +182,4 @@ if (require.main === module) {
|
|
|
181
182
|
doctor.run();
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
module.exports = I18nDoctor;
|
|
185
|
+
module.exports = I18nDoctor;
|
package/main/i18ntk-init.js
CHANGED
|
@@ -754,7 +754,7 @@ class I18nInitializer {
|
|
|
754
754
|
}
|
|
755
755
|
|
|
756
756
|
// Interactive admin PIN setup
|
|
757
|
-
async promptAdminPinSetup() {
|
|
757
|
+
async promptAdminPinSetup() {
|
|
758
758
|
const { ask, askHidden, flushStdout } = require('../utils/cli');
|
|
759
759
|
|
|
760
760
|
console.log('\n' + t('init.adminPinSetupOptional'));
|
|
@@ -806,8 +806,31 @@ class I18nInitializer {
|
|
|
806
806
|
}
|
|
807
807
|
} else {
|
|
808
808
|
console.log(t('init.skippingAdminPinSetup'));
|
|
809
|
-
}
|
|
810
|
-
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async promptBackupConfiguration(skipPrompt = false) {
|
|
813
|
+
const defaultBackupConfig = { enabled: false, maxBackups: 1, location: './i18ntk-backups' };
|
|
814
|
+
if (skipPrompt || !isInteractive()) {
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const { ask } = require('../utils/cli');
|
|
819
|
+
console.log('\nBackup Settings');
|
|
820
|
+
console.log('Backups are disabled by default to avoid backup recursion and repo pollution.');
|
|
821
|
+
const enableAnswer = await ask('Enable automatic backups? (y/N): ');
|
|
822
|
+
const enabled = ['y', 'yes'].includes(String(enableAnswer || '').trim().toLowerCase());
|
|
823
|
+
|
|
824
|
+
if (!enabled) {
|
|
825
|
+
return defaultBackupConfig;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const keepAnswer = await ask('How many backups should be kept automatically (1-3, default 1): ');
|
|
829
|
+
const parsedKeep = parseInt(String(keepAnswer || '').trim(), 10);
|
|
830
|
+
const maxBackups = Number.isInteger(parsedKeep) ? Math.min(Math.max(parsedKeep, 1), 3) : 1;
|
|
831
|
+
|
|
832
|
+
return { enabled: true, maxBackups, location: './i18ntk-backups' };
|
|
833
|
+
}
|
|
811
834
|
|
|
812
835
|
// Interactive language selection
|
|
813
836
|
async selectLanguages(skipPrompt = false) {
|
|
@@ -921,11 +944,24 @@ class I18nInitializer {
|
|
|
921
944
|
// Prompt for admin PIN setup if not already configured
|
|
922
945
|
const securitySettings = configManager.getConfig().security || {};
|
|
923
946
|
|
|
924
|
-
if (!securitySettings.adminPinEnabled && securitySettings.adminPinPromptOnInit !== false && !args.noPrompt) {
|
|
925
|
-
const { flushStdout } = require('../utils/cli');
|
|
926
|
-
await flushStdout();
|
|
927
|
-
await this.promptAdminPinSetup();
|
|
928
|
-
}
|
|
947
|
+
if (!securitySettings.adminPinEnabled && securitySettings.adminPinPromptOnInit !== false && !args.noPrompt) {
|
|
948
|
+
const { flushStdout } = require('../utils/cli');
|
|
949
|
+
await flushStdout();
|
|
950
|
+
await this.promptAdminPinSetup();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const backupSettings = await this.promptBackupConfiguration(args.noPrompt);
|
|
954
|
+
if (backupSettings) {
|
|
955
|
+
await configManager.updateConfig({
|
|
956
|
+
backup: {
|
|
957
|
+
...(this.config.backup || {}),
|
|
958
|
+
...backupSettings
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
this.config.backup = { ...(this.config.backup || {}), ...backupSettings };
|
|
962
|
+
} else if (!this.config.backup) {
|
|
963
|
+
this.config.backup = { enabled: false, maxBackups: 1, location: './i18ntk-backups' };
|
|
964
|
+
}
|
|
929
965
|
|
|
930
966
|
// Get target languages - use args.languages if provided
|
|
931
967
|
let targetLanguages = args.languages || await this.selectLanguages(args.noPrompt);
|