i18ntk 2.5.1 → 2.6.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 +366 -0
- package/README.md +21 -45
- package/main/i18ntk-analyze.js +4 -4
- package/main/i18ntk-scanner.js +14 -12
- package/main/i18ntk-validate.js +25 -18
- package/main/manage/commands/AnalyzeCommand.js +7 -4
- package/main/manage/commands/FixerCommand.js +11 -1
- package/main/manage/commands/ScannerCommand.js +12 -10
- package/main/manage/commands/ValidateCommand.js +21 -17
- package/main/manage/index.js +6 -7
- package/package.json +2 -1
- package/runtime/enhanced.js +64 -10
- package/runtime/i18ntk.d.ts +10 -6
- package/runtime/index.js +45 -22
- 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/watch-locales.js +12 -5
|
@@ -373,7 +373,17 @@ class FixerCommand {
|
|
|
373
373
|
if (backupDirs.length <= keepCount) return;
|
|
374
374
|
|
|
375
375
|
for (const staleDir of backupDirs.slice(keepCount)) {
|
|
376
|
-
|
|
376
|
+
try {
|
|
377
|
+
SecurityUtils.safeUnlinkSync(staleDir.path, process.cwd());
|
|
378
|
+
} catch (_) {
|
|
379
|
+
// Directory not empty, use recursive removal
|
|
380
|
+
try {
|
|
381
|
+
const { rmSync } = require('fs');
|
|
382
|
+
rmSync(staleDir.path, { recursive: true, force: true });
|
|
383
|
+
} catch (_) {
|
|
384
|
+
// Best-effort cleanup
|
|
385
|
+
}
|
|
386
|
+
}
|
|
377
387
|
}
|
|
378
388
|
} catch (error) {
|
|
379
389
|
console.warn(`Failed to clean old backups: ${error.message}`);
|
|
@@ -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, '-');
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18ntk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, fixing, reporting, and runtime translation loading.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"i18n",
|
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
"utils/watch-locales.js",
|
|
133
133
|
"LICENSE",
|
|
134
134
|
"README.md",
|
|
135
|
+
"CHANGELOG.md",
|
|
135
136
|
"CODE_OF_CONDUCT.md",
|
|
136
137
|
"CONTRIBUTING.md",
|
|
137
138
|
"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
|
/**
|
package/runtime/index.js
CHANGED
|
@@ -30,7 +30,18 @@ function stripBOMAndComments(s) {
|
|
|
30
30
|
|
|
31
31
|
function readJsonSafe(file) {
|
|
32
32
|
const raw = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8');
|
|
33
|
-
|
|
33
|
+
if (raw === null || raw === undefined) {
|
|
34
|
+
throw new Error(`Unable to read JSON file: ${file}`);
|
|
35
|
+
}
|
|
36
|
+
const cleaned = stripBOMAndComments(raw);
|
|
37
|
+
if (!cleaned) {
|
|
38
|
+
throw new Error(`Empty JSON file: ${file}`);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(cleaned);
|
|
42
|
+
} catch (parseError) {
|
|
43
|
+
throw new Error(`Invalid JSON in file ${file}: ${parseError.message}`);
|
|
44
|
+
}
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
function deepMerge(target, source) {
|
|
@@ -93,13 +104,17 @@ function listJsonFilesRecursively(dir) {
|
|
|
93
104
|
while (stack.length) {
|
|
94
105
|
const d = stack.pop();
|
|
95
106
|
if (!SecurityUtils.safeExistsSync(d)) continue;
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
try {
|
|
108
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
109
|
+
const full = path.join(d, entry.name);
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
stack.push(full);
|
|
112
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
|
|
113
|
+
results.push(full);
|
|
114
|
+
}
|
|
102
115
|
}
|
|
116
|
+
} catch (_) {
|
|
117
|
+
// Skip directories we cannot read
|
|
103
118
|
}
|
|
104
119
|
}
|
|
105
120
|
return results;
|
|
@@ -111,7 +126,8 @@ function readLanguageFromBase(baseDir, lang) {
|
|
|
111
126
|
const langDir = path.join(baseDir, lang);
|
|
112
127
|
|
|
113
128
|
// Prefer folder if exists, otherwise single file
|
|
114
|
-
|
|
129
|
+
const langDirStat = SecurityUtils.safeStatSync(langDir, path.dirname(langDir));
|
|
130
|
+
if (langDirStat && langDirStat.isDirectory()) {
|
|
115
131
|
const files = listJsonFilesRecursively(langDir);
|
|
116
132
|
for (const file of files) {
|
|
117
133
|
try {
|
|
@@ -121,11 +137,14 @@ function readLanguageFromBase(baseDir, lang) {
|
|
|
121
137
|
// Skip unreadable/invalid files
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
|
-
} else
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
} else {
|
|
141
|
+
const langFileStat = SecurityUtils.safeStatSync(langFile, path.dirname(langFile));
|
|
142
|
+
if (langFileStat && langFileStat.isFile()) {
|
|
143
|
+
try {
|
|
144
|
+
const data = readJsonSafe(langFile);
|
|
145
|
+
if (data && typeof data === 'object') deepMerge(merged, data);
|
|
146
|
+
} catch (_) { /* ignore */ }
|
|
147
|
+
}
|
|
129
148
|
}
|
|
130
149
|
|
|
131
150
|
return merged;
|
|
@@ -211,16 +230,20 @@ function getAvailableLanguages() {
|
|
|
211
230
|
const langs = new Set();
|
|
212
231
|
if (!state.baseDir) state.baseDir = resolveBaseDir();
|
|
213
232
|
if (!SecurityUtils.safeExistsSync(state.baseDir)) return ['en'];
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
233
|
+
try {
|
|
234
|
+
for (const entry of fs.readdirSync(state.baseDir, { withFileTypes: true })) {
|
|
235
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
|
|
236
|
+
langs.add(entry.name.replace(/\.json$/i, ''));
|
|
237
|
+
} else if (entry.isDirectory()) {
|
|
238
|
+
const lang = entry.name;
|
|
239
|
+
const idx = path.join(state.baseDir, lang, `${lang}.json`);
|
|
240
|
+
if (SecurityUtils.safeExistsSync(idx)) langs.add(lang);
|
|
241
|
+
else langs.add(lang); // be permissive
|
|
242
|
+
}
|
|
223
243
|
}
|
|
244
|
+
} catch (_) {
|
|
245
|
+
// Unreadable directory
|
|
246
|
+
return ['en'];
|
|
224
247
|
}
|
|
225
248
|
return Array.from(langs.size ? langs : new Set(['en']));
|
|
226
249
|
}
|
package/utils/admin-auth.js
CHANGED
|
@@ -374,7 +374,10 @@ class AdminAuth {
|
|
|
374
374
|
process.exit(0);
|
|
375
375
|
});
|
|
376
376
|
process.on('uncaughtException', (error) => {
|
|
377
|
-
SecurityUtils.logSecurityEvent('uncaught_exception', 'error',
|
|
377
|
+
SecurityUtils.logSecurityEvent('uncaught_exception', 'error', {
|
|
378
|
+
message: error && error.message ? error.message : 'Unknown uncaught exception',
|
|
379
|
+
stack: error && error.stack ? String(error.stack).split('\n').slice(0, 3).join('\n') : undefined
|
|
380
|
+
});
|
|
378
381
|
cleanup();
|
|
379
382
|
process.exit(1);
|
|
380
383
|
});
|
package/utils/config-helper.js
CHANGED
|
@@ -51,7 +51,9 @@ async function getUnifiedConfig(scriptName, cliArgs = {}) {
|
|
|
51
51
|
}
|
|
52
52
|
settingsDir = safeConfigDir;
|
|
53
53
|
const configFile = path.join(settingsDir, 'i18ntk-config.json');
|
|
54
|
-
|
|
54
|
+
const rawConfig = SecurityUtils.safeReadFileSync(configFile, settingsDir, 'utf8');
|
|
55
|
+
cfg = rawConfig ? SecurityUtils.safeParseJSON(rawConfig) : {};
|
|
56
|
+
if (!cfg || typeof cfg !== 'object') cfg = {};
|
|
55
57
|
projectRoot = settingsDir;
|
|
56
58
|
cfg.projectRoot = projectRoot;
|
|
57
59
|
cfg.sourceDir = path.resolve(projectRoot, toStr(cfg.sourceDir) || './locales');
|
|
@@ -60,11 +62,15 @@ async function getUnifiedConfig(scriptName, cliArgs = {}) {
|
|
|
60
62
|
} else {
|
|
61
63
|
cfg = configManager.getConfig();
|
|
62
64
|
// Use current working directory instead of hardcoded path
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
+
const isSuspiciousPath = cfg.projectRoot && (
|
|
66
|
+
cfg.projectRoot.includes('i18n-management-toolkit-main') ||
|
|
67
|
+
cfg.projectRoot.includes('i18ntk-') ||
|
|
68
|
+
!SecurityUtils.safeExistsSync(cfg.projectRoot, path.dirname(cfg.projectRoot || '.'))
|
|
69
|
+
);
|
|
70
|
+
projectRoot = isSuspiciousPath ? process.cwd() : path.resolve(cfg.projectRoot || '.');
|
|
65
71
|
|
|
66
72
|
// Update config with dynamic project root
|
|
67
|
-
if (
|
|
73
|
+
if (isSuspiciousPath) {
|
|
68
74
|
cfg.projectRoot = '.';
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -110,7 +116,7 @@ async function getUnifiedConfig(scriptName, cliArgs = {}) {
|
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
// Auto-fix i18nDir if missing but sourceDir exists
|
|
113
|
-
if (!SecurityUtils.safeExistsSync(cfg.i18nDir) && SecurityUtils.safeExistsSync(cfg.sourceDir)) {
|
|
119
|
+
if (!SecurityUtils.safeExistsSync(cfg.i18nDir, projectRoot) && SecurityUtils.safeExistsSync(cfg.sourceDir, projectRoot)) {
|
|
114
120
|
await configManager.updateConfig({ i18nDir: configManager.toRelative(cfg.sourceDir) });
|
|
115
121
|
cfg.i18nDir = cfg.sourceDir;
|
|
116
122
|
}
|
|
@@ -414,8 +420,8 @@ function ensureDirectory(dirPath) {
|
|
|
414
420
|
// Silently handle undefined or invalid paths to prevent security errors
|
|
415
421
|
return;
|
|
416
422
|
}
|
|
417
|
-
if (!SecurityUtils.safeExistsSync(dirPath)) {
|
|
418
|
-
|
|
423
|
+
if (!SecurityUtils.safeExistsSync(dirPath, process.cwd())) {
|
|
424
|
+
SecurityUtils.safeMkdirSync(dirPath, process.cwd(), { recursive: true });
|
|
419
425
|
}
|
|
420
426
|
}
|
|
421
427
|
|
|
@@ -499,49 +505,49 @@ async function initializeSourceFiles(sourceDir, sourceLang) {
|
|
|
499
505
|
ensureDirectory(sourceDir);
|
|
500
506
|
|
|
501
507
|
// Write the default source language file
|
|
502
|
-
SecurityUtils.safeWriteFileSync(sourceFile, JSON.stringify(defaultContent, null, 2));
|
|
508
|
+
SecurityUtils.safeWriteFileSync(sourceFile, JSON.stringify(defaultContent, null, 2), sourceDir, 'utf8');
|
|
503
509
|
|
|
504
510
|
// Create directories for supported languages
|
|
505
511
|
const supportedLanguages = ['es', 'fr', 'de', 'ja', 'ru', 'zh', 'pt'];
|
|
506
512
|
|
|
507
513
|
supportedLanguages.forEach(lang => {
|
|
508
514
|
const langFile = path.join(sourceDir, `${lang}.json`);
|
|
509
|
-
if (!SecurityUtils.safeExistsSync(langFile)) {
|
|
515
|
+
if (!SecurityUtils.safeExistsSync(langFile, sourceDir)) {
|
|
510
516
|
// Create empty object structure for each language
|
|
511
517
|
const emptyStructure = {
|
|
512
518
|
app: {},
|
|
513
519
|
common: {},
|
|
514
520
|
navigation: {}
|
|
515
521
|
};
|
|
516
|
-
SecurityUtils.safeWriteFileSync(langFile, JSON.stringify(emptyStructure, null, 2));
|
|
522
|
+
SecurityUtils.safeWriteFileSync(langFile, JSON.stringify(emptyStructure, null, 2), sourceDir, 'utf8');
|
|
517
523
|
}
|
|
518
524
|
});
|
|
519
525
|
|
|
520
|
-
// Create v2 project config if it doesn't exist
|
|
521
|
-
const configFile = '.i18ntk-config';
|
|
522
|
-
if (!SecurityUtils.safeExistsSync(configFile)) {
|
|
523
|
-
const version = (() => {
|
|
524
|
-
try {
|
|
525
|
-
return require('../package.json').version;
|
|
526
|
-
} catch {
|
|
527
|
-
return '2.0.0';
|
|
528
|
-
}
|
|
529
|
-
})();
|
|
530
|
-
const defaultConfig = {
|
|
531
|
-
version,
|
|
532
|
-
sourceDir: sourceDir,
|
|
533
|
-
outputDir: "./i18ntk-reports",
|
|
534
|
-
defaultLanguage: sourceLang,
|
|
535
|
-
supportedLanguages: [sourceLang, 'es', 'fr', 'de', 'ja', 'ru', 'zh', 'pt'],
|
|
536
|
-
setup: {
|
|
537
|
-
completed: true,
|
|
538
|
-
completedAt: new Date().toISOString(),
|
|
539
|
-
version,
|
|
540
|
-
setupId: `setup_${Date.now()}`
|
|
541
|
-
},
|
|
542
|
-
security: {
|
|
543
|
-
adminPinEnabled: true,
|
|
544
|
-
sessionTimeout: 1800000,
|
|
526
|
+
// Create v2 project config if it doesn't exist
|
|
527
|
+
const configFile = '.i18ntk-config';
|
|
528
|
+
if (!SecurityUtils.safeExistsSync(configFile, process.cwd())) {
|
|
529
|
+
const version = (() => {
|
|
530
|
+
try {
|
|
531
|
+
return require('../package.json').version;
|
|
532
|
+
} catch {
|
|
533
|
+
return '2.0.0';
|
|
534
|
+
}
|
|
535
|
+
})();
|
|
536
|
+
const defaultConfig = {
|
|
537
|
+
version,
|
|
538
|
+
sourceDir: sourceDir,
|
|
539
|
+
outputDir: "./i18ntk-reports",
|
|
540
|
+
defaultLanguage: sourceLang,
|
|
541
|
+
supportedLanguages: [sourceLang, 'es', 'fr', 'de', 'ja', 'ru', 'zh', 'pt'],
|
|
542
|
+
setup: {
|
|
543
|
+
completed: true,
|
|
544
|
+
completedAt: new Date().toISOString(),
|
|
545
|
+
version,
|
|
546
|
+
setupId: `setup_${Date.now()}`
|
|
547
|
+
},
|
|
548
|
+
security: {
|
|
549
|
+
adminPinEnabled: true,
|
|
550
|
+
sessionTimeout: 1800000,
|
|
545
551
|
maxFailedAttempts: 3
|
|
546
552
|
},
|
|
547
553
|
performance: {
|
|
@@ -550,8 +556,8 @@ async function initializeSourceFiles(sourceDir, sourceLang) {
|
|
|
550
556
|
batchSize: 1000
|
|
551
557
|
}
|
|
552
558
|
};
|
|
553
|
-
SecurityUtils.safeWriteFileSync(configFile, JSON.stringify(defaultConfig, null, 2));
|
|
554
|
-
}
|
|
559
|
+
SecurityUtils.safeWriteFileSync(configFile, JSON.stringify(defaultConfig, null, 2), process.cwd(), 'utf8');
|
|
560
|
+
}
|
|
555
561
|
}
|
|
556
562
|
|
|
557
563
|
|