i18ntk 2.5.1 → 3.0.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/CHANGELOG.md +385 -0
- package/README.md +56 -47
- package/main/i18ntk-analyze.js +4 -4
- package/main/i18ntk-scanner.js +14 -12
- package/main/i18ntk-translate.js +502 -0
- package/main/i18ntk-validate.js +25 -18
- package/main/manage/commands/AnalyzeCommand.js +7 -4
- package/main/manage/commands/CommandRouter.js +7 -1
- package/main/manage/commands/FixerCommand.js +11 -1
- package/main/manage/commands/ScannerCommand.js +12 -10
- package/main/manage/commands/TranslateCommand.js +242 -0
- package/main/manage/commands/ValidateCommand.js +21 -17
- package/main/manage/index.js +17 -12
- package/package.json +13 -3
- package/runtime/enhanced.js +64 -10
- package/runtime/i18ntk.d.ts +10 -6
- package/runtime/index.js +45 -22
- package/ui-locales/de.json +3 -0
- package/ui-locales/en.json +3 -0
- package/ui-locales/es.json +3 -0
- package/ui-locales/fr.json +3 -0
- package/ui-locales/ja.json +3 -0
- package/ui-locales/ru.json +3 -1
- package/ui-locales/zh.json +3 -0
- package/utils/admin-auth.js +4 -1
- package/utils/config-helper.js +43 -37
- package/utils/config-manager.js +59 -49
- package/utils/config.js +13 -4
- package/utils/env-manager.js +3 -1
- package/utils/i18n-helper.js +41 -13
- package/utils/init-helper.js +23 -21
- package/utils/secure-errors.js +10 -6
- package/utils/security.js +30 -4
- package/utils/setup-enforcer.js +22 -33
- package/utils/translate/api.js +168 -0
- package/utils/translate/cli.js +91 -0
- package/utils/translate/placeholder.js +93 -0
- package/utils/translate/report.js +90 -0
- package/utils/translate/traverse.js +148 -0
- package/utils/watch-locales.js +12 -5
|
@@ -175,9 +175,9 @@ class ScannerCommand {
|
|
|
175
175
|
if (pyproject.includes('Flask')) return 'flask';
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
// Check for Python files
|
|
179
|
-
const
|
|
180
|
-
|
|
178
|
+
// Check for Python files using safeReaddirSync
|
|
179
|
+
const pythonItems = SecurityUtils.safeReaddirSync(projectRoot, projectRoot, { withFileTypes: true }) || [];
|
|
180
|
+
const hasPythonFiles = pythonItems.some(item => item.isFile && item.name && item.name.endsWith('.py'));
|
|
181
181
|
if (hasPythonFiles) return 'python';
|
|
182
182
|
} catch (error) {
|
|
183
183
|
// Continue to JS frameworks
|
|
@@ -414,20 +414,22 @@ class ScannerCommand {
|
|
|
414
414
|
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.html', '.svelte', '.py', '.pyx', '.pyi'];
|
|
415
415
|
|
|
416
416
|
const scanRecursive = (currentDir) => {
|
|
417
|
-
const items =
|
|
417
|
+
const items = SecurityUtils.safeReaddirSync(currentDir, path.dirname(currentDir), { withFileTypes: true });
|
|
418
|
+
if (!items) return;
|
|
418
419
|
|
|
419
420
|
for (const item of items) {
|
|
420
|
-
const fullPath = path.join(currentDir, item);
|
|
421
|
-
const stat =
|
|
421
|
+
const fullPath = path.join(currentDir, item.name);
|
|
422
|
+
const stat = SecurityUtils.safeStatSync(fullPath, currentDir);
|
|
423
|
+
if (!stat) continue;
|
|
422
424
|
|
|
423
425
|
if (stat.isDirectory()) {
|
|
424
|
-
if (!item.startsWith('.') && !this.shouldExcludeFile(fullPath, exclusions)) {
|
|
426
|
+
if (!item.name.startsWith('.') && !this.shouldExcludeFile(fullPath, exclusions)) {
|
|
425
427
|
scanRecursive(fullPath);
|
|
426
428
|
}
|
|
427
429
|
} else if (stat.isFile()) {
|
|
428
|
-
const ext = path.extname(item);
|
|
430
|
+
const ext = path.extname(item.name);
|
|
429
431
|
if (extensions.includes(ext) && !this.shouldExcludeFile(fullPath, exclusions)) {
|
|
430
|
-
if (!includeTests && (item.includes('.test.') || item.includes('.spec.'))) {
|
|
432
|
+
if (!includeTests && (item.name.includes('.test.') || item.name.includes('.spec.'))) {
|
|
431
433
|
continue;
|
|
432
434
|
}
|
|
433
435
|
|
|
@@ -449,7 +451,7 @@ class ScannerCommand {
|
|
|
449
451
|
|
|
450
452
|
async generateReport(results, outputDir) {
|
|
451
453
|
if (!SecurityUtils.safeExistsSync(outputDir, path.dirname(outputDir))) {
|
|
452
|
-
|
|
454
|
+
SecurityUtils.safeMkdirSync(outputDir, process.cwd(), { recursive: true });
|
|
453
455
|
}
|
|
454
456
|
|
|
455
457
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* I18NTK TRANSLATE COMMAND
|
|
5
|
+
*
|
|
6
|
+
* Interactive menu-driven auto-translation using Google Translate.
|
|
7
|
+
* Wraps i18ntk-translate.js behind a user-friendly menu flow.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { getUnifiedConfig } = require('../../../utils/config-helper');
|
|
13
|
+
const { loadTranslations } = require('../../../utils/i18n-helper');
|
|
14
|
+
const SetupEnforcer = require('../../../utils/setup-enforcer');
|
|
15
|
+
|
|
16
|
+
class TranslateCommand {
|
|
17
|
+
constructor(config = {}, ui = null) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.ui = ui;
|
|
20
|
+
this.prompt = null;
|
|
21
|
+
this.isNonInteractiveMode = false;
|
|
22
|
+
this.safeClose = null;
|
|
23
|
+
this.sourceDir = null;
|
|
24
|
+
this.sourceLang = null;
|
|
25
|
+
this.targetLang = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setRuntimeDependencies(prompt, isNonInteractiveMode, safeClose) {
|
|
29
|
+
this.prompt = prompt;
|
|
30
|
+
this.isNonInteractiveMode = isNonInteractiveMode;
|
|
31
|
+
this.safeClose = safeClose;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async execute(options = {}) {
|
|
35
|
+
try {
|
|
36
|
+
await SetupEnforcer.checkSetupCompleteAsync();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Setup check failed:', error.message);
|
|
39
|
+
return { success: false, error: 'Setup required' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
loadTranslations('en', path.resolve(__dirname, '..', '..', '..', 'ui-locales'));
|
|
43
|
+
|
|
44
|
+
const config = this.config || {};
|
|
45
|
+
const unified = getUnifiedConfig(config);
|
|
46
|
+
|
|
47
|
+
const defaultSourceDir = unified.sourceDir || unified.i18nDir || path.resolve(process.cwd(), 'locales', 'en');
|
|
48
|
+
this.sourceLang = unified.sourceLanguage || 'en';
|
|
49
|
+
|
|
50
|
+
console.log('\n============================================================');
|
|
51
|
+
console.log(' \u{1F310} AUTO TRANSLATE (BETA)');
|
|
52
|
+
console.log('============================================================');
|
|
53
|
+
|
|
54
|
+
if (this.isNonInteractiveMode) {
|
|
55
|
+
this.sourceDir = defaultSourceDir;
|
|
56
|
+
if (!fs.existsSync(this.sourceDir)) {
|
|
57
|
+
console.error(`Source locale directory not found: ${this.sourceDir}`);
|
|
58
|
+
return { success: false, error: 'Source directory not found' };
|
|
59
|
+
}
|
|
60
|
+
const jsonFiles = fs.readdirSync(this.sourceDir).filter(f => f.endsWith('.json')).sort();
|
|
61
|
+
return await this.nonInteractiveFlow(jsonFiles);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { ask } = require('../../../utils/cli');
|
|
65
|
+
|
|
66
|
+
// Step 1: Choose source directory
|
|
67
|
+
this.sourceDir = await this.promptSourceDir(ask, defaultSourceDir);
|
|
68
|
+
if (!this.sourceDir) return { success: false, error: 'No source directory selected' };
|
|
69
|
+
|
|
70
|
+
// Step 2: Choose source language
|
|
71
|
+
this.sourceLang = await this.promptSourceLang(ask);
|
|
72
|
+
if (!this.sourceLang) return { success: false, error: 'No source language selected' };
|
|
73
|
+
|
|
74
|
+
const jsonFiles = fs.readdirSync(this.sourceDir)
|
|
75
|
+
.filter(f => f.endsWith('.json'))
|
|
76
|
+
.sort();
|
|
77
|
+
|
|
78
|
+
if (jsonFiles.length === 0) {
|
|
79
|
+
console.error(`No JSON files found in: ${this.sourceDir}`);
|
|
80
|
+
return { success: false, error: 'No source files found' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return await this.interactiveFlow(jsonFiles, ask);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async promptSourceDir(ask, defaultDir) {
|
|
87
|
+
while (true) {
|
|
88
|
+
console.log(`\n Source directory [default: ${defaultDir}]`);
|
|
89
|
+
console.log(' Press Enter for default, or type a custom path.');
|
|
90
|
+
const input = await ask(' > ');
|
|
91
|
+
|
|
92
|
+
if (!input.trim()) {
|
|
93
|
+
if (!fs.existsSync(defaultDir)) {
|
|
94
|
+
console.log(` Default directory not found: ${defaultDir}`);
|
|
95
|
+
console.log(' Please enter an existing directory with JSON locale files.');
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
console.log(` Using default: ${defaultDir}`);
|
|
99
|
+
return defaultDir;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const resolved = path.resolve(process.cwd(), input.trim());
|
|
103
|
+
if (!fs.existsSync(resolved)) {
|
|
104
|
+
console.log(` Directory not found: ${resolved}`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!fs.statSync(resolved).isDirectory()) {
|
|
108
|
+
console.log(` Not a directory: ${resolved}`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
return resolved;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async promptSourceLang(ask) {
|
|
116
|
+
while (true) {
|
|
117
|
+
console.log(`\n Source language code [default: ${this.sourceLang}]`);
|
|
118
|
+
const input = await ask(' > ');
|
|
119
|
+
|
|
120
|
+
if (!input.trim()) {
|
|
121
|
+
return this.sourceLang;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const lang = input.trim().toLowerCase();
|
|
125
|
+
if (lang.length >= 2) {
|
|
126
|
+
return lang;
|
|
127
|
+
}
|
|
128
|
+
console.log(' Invalid language code. Use 2+ characters (e.g. en, de, fr).');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async interactiveFlow(jsonFiles, ask) {
|
|
133
|
+
|
|
134
|
+
console.log('\n Target language code(s)');
|
|
135
|
+
console.log(' Enter one or more comma/space-separated codes');
|
|
136
|
+
console.log(' (e.g. de, es, fr or de es fr or de):');
|
|
137
|
+
const langInput = await ask(' > ');
|
|
138
|
+
|
|
139
|
+
const targetLangs = langInput
|
|
140
|
+
.trim()
|
|
141
|
+
.split(/[,;\s]+/)
|
|
142
|
+
.map(s => s.toLowerCase().trim())
|
|
143
|
+
.filter(s => s.length >= 2);
|
|
144
|
+
|
|
145
|
+
if (targetLangs.length === 0) {
|
|
146
|
+
console.log(' No valid language codes entered. Aborting.');
|
|
147
|
+
return { success: false, error: 'Invalid language code' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(`\n Target languages: ${targetLangs.join(', ')}`);
|
|
151
|
+
|
|
152
|
+
console.log(`\n Which file(s) to translate?`);
|
|
153
|
+
console.log(` a) All files (${jsonFiles.join(', ')})`);
|
|
154
|
+
jsonFiles.forEach((f, i) => {
|
|
155
|
+
console.log(` ${i + 1}) ${f}`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const fileChoice = await ask('\n Choice [a/1-9]: ');
|
|
159
|
+
let sourceFiles;
|
|
160
|
+
|
|
161
|
+
if (fileChoice.toLowerCase() === 'a') {
|
|
162
|
+
sourceFiles = jsonFiles.map(f => path.join(this.sourceDir, f));
|
|
163
|
+
} else {
|
|
164
|
+
const idx = parseInt(fileChoice, 10) - 1;
|
|
165
|
+
if (isNaN(idx) || idx < 0 || idx >= jsonFiles.length) {
|
|
166
|
+
console.log(' Invalid choice. Aborting.');
|
|
167
|
+
return { success: false, error: 'Invalid file choice' };
|
|
168
|
+
}
|
|
169
|
+
sourceFiles = [path.join(this.sourceDir, jsonFiles[idx])];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Dry-run for first language only (all languages use same source so same keys)
|
|
173
|
+
const firstLang = targetLangs[0];
|
|
174
|
+
console.log(`\n Dry-run preview for "${firstLang}"...\n`);
|
|
175
|
+
await this.runTranslate(sourceFiles, firstLang, { dryRun: true });
|
|
176
|
+
|
|
177
|
+
console.log('\n Proceed with actual translation?');
|
|
178
|
+
const answer = await ask(' [y]es / [n]o: ');
|
|
179
|
+
if (!/^y|yes$/i.test(answer.trim())) {
|
|
180
|
+
console.log(' Translation cancelled.');
|
|
181
|
+
return { success: true, cancelled: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let results = [];
|
|
185
|
+
for (const lang of targetLangs) {
|
|
186
|
+
console.log(`\n Translating to "${lang}"...\n`);
|
|
187
|
+
try {
|
|
188
|
+
await this.runTranslate(sourceFiles, lang, { dryRun: false });
|
|
189
|
+
results.push({ lang, ok: true });
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.error(` Failed for "${lang}": ${e.message}`);
|
|
192
|
+
results.push({ lang, ok: false, error: e.message });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log('\n Summary:');
|
|
197
|
+
for (const r of results) {
|
|
198
|
+
console.log(` ${r.ok ? '\u{2705}' : '\u{274C}'} ${r.lang}${r.error ? ' (' + r.error + ')' : ''}`);
|
|
199
|
+
}
|
|
200
|
+
console.log('\n Translation complete!');
|
|
201
|
+
return { success: true, results };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async nonInteractiveFlow(jsonFiles) {
|
|
205
|
+
console.log('\n Non-interactive mode. Use direct CLI instead:');
|
|
206
|
+
console.log(' i18ntk-translate <source> <lang> [options]');
|
|
207
|
+
return { success: false, error: 'Non-interactive mode not supported from menu' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async runTranslate(sourceFiles, targetLang, opts = {}) {
|
|
211
|
+
const { spawn } = require('child_process');
|
|
212
|
+
|
|
213
|
+
for (const src of sourceFiles) {
|
|
214
|
+
const args = [
|
|
215
|
+
path.resolve(__dirname, '..', '..', 'i18ntk-translate.js'),
|
|
216
|
+
src,
|
|
217
|
+
targetLang,
|
|
218
|
+
'--no-confirm',
|
|
219
|
+
'--skip-placeholders',
|
|
220
|
+
'--report-stdout'
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
if (opts.dryRun) {
|
|
224
|
+
args.push('--dry-run');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await new Promise((resolve, reject) => {
|
|
228
|
+
const proc = spawn('node', args, {
|
|
229
|
+
stdio: 'inherit',
|
|
230
|
+
cwd: process.cwd()
|
|
231
|
+
});
|
|
232
|
+
proc.on('close', (code) => {
|
|
233
|
+
if (code === 0) resolve();
|
|
234
|
+
else reject(new Error(`Exit code ${code}`));
|
|
235
|
+
});
|
|
236
|
+
proc.on('error', reject);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = TranslateCommand;
|
|
@@ -94,8 +94,8 @@ class ValidateCommand {
|
|
|
94
94
|
} else {
|
|
95
95
|
console.warn(t('config.dirFallbackWarning', { dir: this.sourceDir, fallback: this.sourceLanguageDir }) ||
|
|
96
96
|
`Warning: Directory ${this.sourceDir} not found. Using ${this.sourceLanguageDir}.`);
|
|
97
|
-
if (!SecurityUtils.safeExistsSync(this.sourceLanguageDir)) {
|
|
98
|
-
|
|
97
|
+
if (!SecurityUtils.safeExistsSync(this.sourceLanguageDir, process.cwd())) {
|
|
98
|
+
SecurityUtils.safeMkdirSync(this.sourceLanguageDir, process.cwd(), { recursive: true });
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -185,13 +185,15 @@ class ValidateCommand {
|
|
|
185
185
|
throw new Error(`Source directory not found: ${this.sourceDir}`);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
188
|
+
const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
|
|
189
|
+
if (!items) {
|
|
190
|
+
throw new Error(`Source directory not found: ${this.sourceDir}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const languages = items
|
|
194
|
+
.filter(item => item.isDirectory())
|
|
195
|
+
.filter(item => item.name !== this.config.sourceLanguage && !this.isExcludedLanguageDirectory(item.name))
|
|
196
|
+
.map(item => item.name);
|
|
195
197
|
|
|
196
198
|
return languages;
|
|
197
199
|
} catch (error) {
|
|
@@ -209,11 +211,13 @@ class ValidateCommand {
|
|
|
209
211
|
return [];
|
|
210
212
|
}
|
|
211
213
|
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
const items = SecurityUtils.safeReaddirSync(languageDir, process.cwd(), { withFileTypes: true });
|
|
215
|
+
if (!items) return [];
|
|
216
|
+
|
|
217
|
+
const files = items
|
|
218
|
+
.filter(item => item.isFile() && item.name.endsWith('.json') &&
|
|
219
|
+
!this.config.excludeFiles.includes(item.name))
|
|
220
|
+
.map(item => item.name);
|
|
217
221
|
|
|
218
222
|
return files;
|
|
219
223
|
} catch (error) {
|
|
@@ -663,10 +667,10 @@ class ValidateCommand {
|
|
|
663
667
|
|
|
664
668
|
// Delete old validation report if it exists
|
|
665
669
|
const reportPath = path.join(process.cwd(), 'validation-report.txt');
|
|
666
|
-
SecurityUtils.validatePath(reportPath);
|
|
670
|
+
const validatedPath = SecurityUtils.validatePath(reportPath, process.cwd());
|
|
667
671
|
|
|
668
|
-
if (SecurityUtils.safeExistsSync(
|
|
669
|
-
|
|
672
|
+
if (validatedPath && SecurityUtils.safeExistsSync(validatedPath, process.cwd())) {
|
|
673
|
+
SecurityUtils.safeUnlinkSync(validatedPath, process.cwd());
|
|
670
674
|
console.log(t('validate.deletedOldReport'));
|
|
671
675
|
|
|
672
676
|
SecurityUtils.logSecurityEvent(t('validate.fileDeleted'), 'info', {
|
package/main/manage/index.js
CHANGED
|
@@ -765,10 +765,11 @@ class I18nManager {
|
|
|
765
765
|
|
|
766
766
|
function findFiles(dir, results = []) {
|
|
767
767
|
try {
|
|
768
|
-
const items =
|
|
768
|
+
const items = SecurityUtils.safeReaddirSync(dir, cwd, { withFileTypes: true });
|
|
769
|
+
if (!items) return results;
|
|
769
770
|
|
|
770
771
|
for (const item of items) {
|
|
771
|
-
const fullPath = path.join(dir, item);
|
|
772
|
+
const fullPath = path.join(dir, item.name);
|
|
772
773
|
const relativePath = path.relative(cwd, fullPath);
|
|
773
774
|
|
|
774
775
|
if (shouldIgnore(relativePath)) {
|
|
@@ -776,14 +777,12 @@ class I18nManager {
|
|
|
776
777
|
}
|
|
777
778
|
|
|
778
779
|
try {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
if (stat.isDirectory()) {
|
|
780
|
+
if (item.isDirectory()) {
|
|
782
781
|
findFiles(fullPath, results);
|
|
783
|
-
} else if (
|
|
782
|
+
} else if (item.isFile()) {
|
|
784
783
|
// Check if file matches any of our patterns
|
|
785
784
|
for (const pattern of patterns) {
|
|
786
|
-
if (matchesPattern(item, pattern)) {
|
|
785
|
+
if (matchesPattern(item.name, pattern)) {
|
|
787
786
|
results.push(relativePath);
|
|
788
787
|
break;
|
|
789
788
|
}
|
|
@@ -879,6 +878,7 @@ class I18nManager {
|
|
|
879
878
|
console.log(`11. ${t('menu.options.help')}`);
|
|
880
879
|
console.log(`12. ${t('menu.options.language')}`);
|
|
881
880
|
console.log(`13. ${t('menu.options.scanner')}`);
|
|
881
|
+
console.log(`14. ${t('menu.options.translate')}`);
|
|
882
882
|
console.log(`0. ${t('menu.options.exit')}`);
|
|
883
883
|
|
|
884
884
|
console.log('\n' + t('menu.nonInteractiveModeWarning'));
|
|
@@ -904,6 +904,7 @@ class I18nManager {
|
|
|
904
904
|
console.log(`11. ${t('menu.options.help')}`);
|
|
905
905
|
console.log(`12. ${t('menu.options.language')}`);
|
|
906
906
|
console.log(`13. ${t('menu.options.scanner')}`);
|
|
907
|
+
console.log(`14. ${t('menu.options.translate')}`);
|
|
907
908
|
console.log(`0. ${t('menu.options.exit')}`);
|
|
908
909
|
|
|
909
910
|
const choice = await this.prompt('\n' + t('menu.selectOptionPrompt'));
|
|
@@ -1008,11 +1009,15 @@ class I18nManager {
|
|
|
1008
1009
|
case '12':
|
|
1009
1010
|
await this.showLanguageMenu();
|
|
1010
1011
|
break;
|
|
1011
|
-
case '13':
|
|
1012
|
-
await this.executeCommand('scanner', {fromMenu: true});
|
|
1013
|
-
await this.showInteractiveMenu();
|
|
1014
|
-
return;
|
|
1015
|
-
case '
|
|
1012
|
+
case '13':
|
|
1013
|
+
await this.executeCommand('scanner', {fromMenu: true});
|
|
1014
|
+
await this.showInteractiveMenu();
|
|
1015
|
+
return;
|
|
1016
|
+
case '14':
|
|
1017
|
+
await this.executeCommand('translate', {fromMenu: true});
|
|
1018
|
+
await this.showInteractiveMenu();
|
|
1019
|
+
return;
|
|
1020
|
+
case '0':
|
|
1016
1021
|
console.log(t('menu.goodbye'));
|
|
1017
1022
|
this.safeClose();
|
|
1018
1023
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18ntk",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Zero-dependency internationalization toolkit for setup, scanning, analysis, validation,
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"i18n",
|
|
7
7
|
"internationalization",
|
|
8
8
|
"localization",
|
|
9
9
|
"translation",
|
|
10
|
+
"auto-translation",
|
|
11
|
+
"google-translate",
|
|
10
12
|
"l10n",
|
|
11
13
|
"multilingual",
|
|
12
14
|
"i18next",
|
|
@@ -76,12 +78,14 @@
|
|
|
76
78
|
"i18ntk-doctor": "main/i18ntk-doctor.js",
|
|
77
79
|
"i18ntk-fixer": "main/i18ntk-fixer.js",
|
|
78
80
|
"i18ntk-scanner": "main/i18ntk-scanner.js",
|
|
79
|
-
"i18ntk-backup": "main/i18ntk-backup.js"
|
|
81
|
+
"i18ntk-backup": "main/i18ntk-backup.js",
|
|
82
|
+
"i18ntk-translate": "main/i18ntk-translate.js"
|
|
80
83
|
},
|
|
81
84
|
"files": [
|
|
82
85
|
"main/i18ntk-analyze.js",
|
|
83
86
|
"main/i18ntk-backup-class.js",
|
|
84
87
|
"main/i18ntk-backup.js",
|
|
88
|
+
"main/i18ntk-translate.js",
|
|
85
89
|
"main/i18ntk-complete.js",
|
|
86
90
|
"main/i18ntk-doctor.js",
|
|
87
91
|
"main/i18ntk-fixer.js",
|
|
@@ -114,6 +118,11 @@
|
|
|
114
118
|
"utils/extractors/regex.js",
|
|
115
119
|
"utils/format-manager.js",
|
|
116
120
|
"utils/formats/json.js",
|
|
121
|
+
"utils/translate/placeholder.js",
|
|
122
|
+
"utils/translate/api.js",
|
|
123
|
+
"utils/translate/traverse.js",
|
|
124
|
+
"utils/translate/report.js",
|
|
125
|
+
"utils/translate/cli.js",
|
|
117
126
|
"utils/framework-detector.js",
|
|
118
127
|
"utils/i18n-helper.js",
|
|
119
128
|
"utils/init-helper.js",
|
|
@@ -132,6 +141,7 @@
|
|
|
132
141
|
"utils/watch-locales.js",
|
|
133
142
|
"LICENSE",
|
|
134
143
|
"README.md",
|
|
144
|
+
"CHANGELOG.md",
|
|
135
145
|
"CODE_OF_CONDUCT.md",
|
|
136
146
|
"CONTRIBUTING.md",
|
|
137
147
|
"FUNDING.md",
|
package/runtime/enhanced.js
CHANGED
|
@@ -24,6 +24,33 @@ const IV_LENGTH = 16;
|
|
|
24
24
|
const AUTH_TAG_LENGTH = 16;
|
|
25
25
|
const SALT_LENGTH = 32;
|
|
26
26
|
|
|
27
|
+
// Track active instances to ensure cleanup is registered only once
|
|
28
|
+
let activeInstances = new Set();
|
|
29
|
+
let processHandlersRegistered = false;
|
|
30
|
+
|
|
31
|
+
function registerProcessHandlers() {
|
|
32
|
+
if (processHandlersRegistered) return;
|
|
33
|
+
processHandlersRegistered = true;
|
|
34
|
+
|
|
35
|
+
process.on('exit', () => {
|
|
36
|
+
for (const instance of activeInstances) {
|
|
37
|
+
try { instance.cleanup(); } catch (_) { /* best-effort */ }
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
process.on('SIGINT', () => {
|
|
41
|
+
for (const instance of activeInstances) {
|
|
42
|
+
try { instance.cleanup(); } catch (_) { /* best-effort */ }
|
|
43
|
+
}
|
|
44
|
+
process.exit(0);
|
|
45
|
+
});
|
|
46
|
+
process.on('uncaughtException', () => {
|
|
47
|
+
for (const instance of activeInstances) {
|
|
48
|
+
try { instance.cleanup(); } catch (_) { /* best-effort */ }
|
|
49
|
+
}
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
class I18nEnhancedRuntime extends EventEmitter {
|
|
28
55
|
constructor() {
|
|
29
56
|
super();
|
|
@@ -84,14 +111,14 @@ class I18nEnhancedRuntime extends EventEmitter {
|
|
|
84
111
|
() => this.checkMemoryUsage(),
|
|
85
112
|
30000 // Check every 30 seconds
|
|
86
113
|
);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (process && process.on) {
|
|
90
|
-
process.on('exit', () => this.cleanup());
|
|
91
|
-
process.on('SIGINT', () => this.cleanup());
|
|
92
|
-
process.on('uncaughtException', () => this.cleanup());
|
|
114
|
+
if (typeof this.memoryCheckInterval.unref === 'function') {
|
|
115
|
+
this.memoryCheckInterval.unref();
|
|
93
116
|
}
|
|
94
117
|
|
|
118
|
+
// Register this instance for process-wide cleanup
|
|
119
|
+
activeInstances.add(this);
|
|
120
|
+
registerProcessHandlers();
|
|
121
|
+
|
|
95
122
|
// Add default translations namespace
|
|
96
123
|
this.addNamespace('default', {
|
|
97
124
|
en: {
|
|
@@ -178,12 +205,32 @@ class I18nEnhancedRuntime extends EventEmitter {
|
|
|
178
205
|
}
|
|
179
206
|
|
|
180
207
|
async decryptData(encryptedData, key = this.encryptionKey) {
|
|
181
|
-
if (!key)
|
|
208
|
+
if (!key) {
|
|
209
|
+
throw new EncryptionError('Encryption key not set', {
|
|
210
|
+
operation: 'decrypt',
|
|
211
|
+
keyType: typeof key
|
|
212
|
+
});
|
|
213
|
+
}
|
|
182
214
|
|
|
183
215
|
try {
|
|
184
|
-
|
|
185
|
-
|
|
216
|
+
let data;
|
|
217
|
+
try {
|
|
218
|
+
data = JSON.parse(encryptedData);
|
|
219
|
+
} catch (parseError) {
|
|
220
|
+
throw new EncryptionError('Failed to parse encrypted data', {
|
|
221
|
+
operation: 'decrypt',
|
|
222
|
+
error: parseError.message
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!data || !data.iv || !data.authTag || !data.encrypted) {
|
|
227
|
+
throw new EncryptionError('Invalid encrypted data format', {
|
|
228
|
+
operation: 'decrypt',
|
|
229
|
+
missingFields: ['iv', 'authTag', 'encrypted'].filter(f => !(f in (data || {})))
|
|
230
|
+
});
|
|
231
|
+
}
|
|
186
232
|
|
|
233
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(key, 'hex'), Buffer.from(data.iv, 'hex'));
|
|
187
234
|
decipher.setAuthTag(Buffer.from(data.authTag, 'hex'));
|
|
188
235
|
|
|
189
236
|
let decrypted = decipher.update(data.encrypted, 'hex', 'utf8');
|
|
@@ -191,7 +238,12 @@ class I18nEnhancedRuntime extends EventEmitter {
|
|
|
191
238
|
|
|
192
239
|
return decrypted;
|
|
193
240
|
} catch (error) {
|
|
194
|
-
|
|
241
|
+
if (error instanceof SecureError) throw error;
|
|
242
|
+
|
|
243
|
+
throw new EncryptionError('Decryption failed', {
|
|
244
|
+
operation: 'decrypt',
|
|
245
|
+
errorId: crypto.randomBytes(4).toString('hex')
|
|
246
|
+
});
|
|
195
247
|
}
|
|
196
248
|
}
|
|
197
249
|
|
|
@@ -619,6 +671,8 @@ class I18nEnhancedRuntime extends EventEmitter {
|
|
|
619
671
|
if (this.config.encryption) {
|
|
620
672
|
this.config.encryption.salt = null;
|
|
621
673
|
}
|
|
674
|
+
|
|
675
|
+
activeInstances.delete(this);
|
|
622
676
|
}
|
|
623
677
|
|
|
624
678
|
// Add or update a cache entry
|
package/runtime/i18ntk.d.ts
CHANGED
|
@@ -447,12 +447,12 @@ export interface I18nRuntime {
|
|
|
447
447
|
*/
|
|
448
448
|
export interface BasicI18nRuntime {
|
|
449
449
|
/**
|
|
450
|
-
* Translate a key with parameters
|
|
450
|
+
* Translate a key with parameters (synchronous)
|
|
451
451
|
*/
|
|
452
452
|
translate(key: string, params?: TranslationParams): string;
|
|
453
453
|
|
|
454
454
|
/**
|
|
455
|
-
* Alias for translate function
|
|
455
|
+
* Alias for translate function (synchronous)
|
|
456
456
|
*/
|
|
457
457
|
t(key: string, params?: TranslationParams): string;
|
|
458
458
|
|
|
@@ -467,7 +467,7 @@ export interface BasicI18nRuntime {
|
|
|
467
467
|
getLanguage(): string;
|
|
468
468
|
|
|
469
469
|
/**
|
|
470
|
-
* Get available languages
|
|
470
|
+
* Get available languages (synchronous)
|
|
471
471
|
*/
|
|
472
472
|
getAvailableLanguages(): string[];
|
|
473
473
|
|
|
@@ -478,16 +478,20 @@ export interface BasicI18nRuntime {
|
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
/**
|
|
481
|
-
*
|
|
481
|
+
* Initialize the enhanced i18ntk runtime (async, returns full I18nRuntime)
|
|
482
482
|
*/
|
|
483
483
|
export declare function initI18nRuntime(config: I18nConfig): Promise<I18nRuntime>;
|
|
484
484
|
|
|
485
485
|
/**
|
|
486
|
-
*
|
|
486
|
+
* Initialize the basic lightweight runtime (synchronous)
|
|
487
|
+
* This is the default export from 'i18ntk/runtime'
|
|
487
488
|
*/
|
|
488
|
-
export declare function initRuntime(
|
|
489
|
+
export declare function initRuntime(options: {
|
|
489
490
|
baseDir: string;
|
|
490
491
|
language?: string;
|
|
492
|
+
fallbackLanguage?: string;
|
|
493
|
+
keySeparator?: string;
|
|
494
|
+
preload?: boolean;
|
|
491
495
|
}): BasicI18nRuntime;
|
|
492
496
|
|
|
493
497
|
/**
|