i18ntk 2.3.7 → 2.4.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 +9 -6
- package/main/i18ntk-backup-class.js +35 -423
- package/main/manage/commands/BackupCommand.js +62 -62
- package/main/manage/services/SetupService.js +444 -462
- package/package.json +12 -9
- package/utils/config-manager.js +84 -30
- package/utils/config.js +15 -14
- package/utils/i18n-helper.js +35 -20
- package/utils/logger.js +233 -64
- package/utils/npm-version-warning.js +12 -141
- package/utils/security.js +233 -150
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18ntk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "🚀 The fastest internationalization toolkit with 97% performance boost! Zero-dependency, enterprise-grade internationalization for React, Vue, Angular, Python, Java, PHP & more. Features PIN protection, auto framework detection, 7+ UI languages, and comprehensive translation management. Perfect for startups to enterprises.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"i18n",
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
"languages:select": "node settings/settings-cli.js",
|
|
214
214
|
"languages:list": "node settings/settings-cli.js --list-languages",
|
|
215
215
|
"languages:status": "node settings/settings-cli.js --language-status",
|
|
216
|
-
"lint:locales": "node scripts/lint-locales.js"
|
|
216
|
+
"lint:locales": "node scripts/lint-locales.js"
|
|
217
217
|
},
|
|
218
218
|
"engines": {
|
|
219
219
|
"node": ">=16.0.0",
|
|
@@ -224,26 +224,29 @@
|
|
|
224
224
|
},
|
|
225
225
|
"preferGlobal": true,
|
|
226
226
|
"versionInfo": {
|
|
227
|
-
"version": "2.
|
|
228
|
-
"releaseDate": "
|
|
229
|
-
"lastUpdated": "
|
|
227
|
+
"version": "2.4.0",
|
|
228
|
+
"releaseDate": "16/04/2026",
|
|
229
|
+
"lastUpdated": "16/04/2026",
|
|
230
230
|
"maintainer": "Vlad Noskov",
|
|
231
231
|
"changelog": "./CHANGELOG.md",
|
|
232
232
|
"documentation": "./README.md",
|
|
233
233
|
"apiReference": "./docs/api/API_REFERENCE.md",
|
|
234
234
|
"majorChanges": [
|
|
235
|
+
"LOGGING: Introduced centralized structured logger with silent-by-default production behavior and DEBUG_MODE/JSON_LOG toggles.",
|
|
236
|
+
"SECURITY: Added internal path whitelist detection to prevent false-positive traversal warnings for package/project internals.",
|
|
237
|
+
"I18N: Added missing-key warning TTL cache to eliminate repeated translation-key spam during builds.",
|
|
235
238
|
"HOTFIX: Removed deprecated package-path fallback that caused production build warnings for non-exported subpaths.",
|
|
236
239
|
"CRITICAL FIX: Resolved sizing and usage-analysis regressions in v2 command flow.",
|
|
237
240
|
"PACKAGING: Reduced publish footprint by removing internal development scripts and legacy fixed-file artifacts.",
|
|
238
241
|
"SECURITY: Hardened release checks and added explicit support guidance to update from pre-2.3.5 versions.",
|
|
239
242
|
"CONFIG: Added cross-process file locking for .i18ntk-config writes to prevent production rename races.",
|
|
240
243
|
"CONFIG: Made autosave runtime-safe with non-throwing save failures and I18NTK_DISABLE_AUTOSAVE support.",
|
|
241
|
-
"SECURITY: Hardened reset/backup path handling and
|
|
242
|
-
"CLI:
|
|
244
|
+
"SECURITY: Hardened reset/backup path handling and disabled manager backup-route execution in current builds.",
|
|
245
|
+
"CLI: Removed npm registry startup update checks to eliminate outbound network calls in scanner-restricted environments.",
|
|
243
246
|
"I18N: Completed internal UI locale parity and actionable untranslated-key cleanup across supported languages."
|
|
244
247
|
],
|
|
245
248
|
"breakingChanges": [],
|
|
246
|
-
"nextVersion": "2.4.
|
|
249
|
+
"nextVersion": "2.4.1",
|
|
247
250
|
"supportedNodeVersions": ">=16.0.0",
|
|
248
251
|
"supportedFrameworks": {
|
|
249
252
|
"react-i18next": ">=11.0.0",
|
|
@@ -265,7 +268,7 @@
|
|
|
265
268
|
"spring-boot": ">=2.5.0",
|
|
266
269
|
"laravel": ">=8.0.0"
|
|
267
270
|
},
|
|
268
|
-
"supportPolicy": "Versions earlier than 2.
|
|
271
|
+
"supportPolicy": "Versions earlier than 2.4.0 may be unstable or insecure. Upgrade to 2.4.0 or newer."
|
|
269
272
|
},
|
|
270
273
|
"_comment": "This package is zero-dependency and uses only native Node.js modules"
|
|
271
274
|
}
|
package/utils/config-manager.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const crypto = require('crypto');
|
|
5
|
-
const SecurityUtils = require('./security');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const SecurityUtils = require('./security');
|
|
6
|
+
const { logger } = require('./logger');
|
|
6
7
|
|
|
7
8
|
// Determine package directory and user project root
|
|
8
9
|
const packageDir = path.resolve(__dirname, '..');
|
|
@@ -19,7 +20,55 @@ const CONFIG_LOCK_PATH = `${PROJECT_CONFIG_PATH}.lock`;
|
|
|
19
20
|
const CONFIG_LOCK_TIMEOUT_MS = 5000;
|
|
20
21
|
const CONFIG_LOCK_STALE_MS = 15000;
|
|
21
22
|
const CONFIG_LOCK_RETRY_MS = 50;
|
|
22
|
-
let autosaveDisabledWarned = false;
|
|
23
|
+
let autosaveDisabledWarned = false;
|
|
24
|
+
let defaultConfigNoticeShown = false;
|
|
25
|
+
let configFallbackNoticeShown = false;
|
|
26
|
+
|
|
27
|
+
function logInfo(message, details) {
|
|
28
|
+
logger.info(message, details);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function logWarn(message, details) {
|
|
32
|
+
logger.warn(message, details);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function logError(message, details) {
|
|
36
|
+
logger.error(message, details);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function notifyDefaultConfig(reason, error) {
|
|
40
|
+
if (defaultConfigNoticeShown) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
defaultConfigNoticeShown = true;
|
|
44
|
+
|
|
45
|
+
logger.info('Using default configuration (reason: configuration error)', { reason });
|
|
46
|
+
if (logger.isDebugMode() && error) {
|
|
47
|
+
logger.debug(`Default configuration reason details: ${error.message}`, {
|
|
48
|
+
stack: error.stack
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function notifyConfigFallback(error) {
|
|
54
|
+
if (!configFallbackNoticeShown) {
|
|
55
|
+
configFallbackNoticeShown = true;
|
|
56
|
+
logger.info('Using default configuration (reason: configuration error)', {
|
|
57
|
+
reason: 'configuration error'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
logger.recordFirstError('config:load-fallback', {
|
|
62
|
+
message: error && error.message ? error.message : 'Unknown configuration error',
|
|
63
|
+
stack: error && error.stack ? error.stack : null
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (logger.isDebugMode() && error) {
|
|
67
|
+
logger.debug(`Configuration load fallback details: ${error.message}`, {
|
|
68
|
+
stack: error.stack
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
23
72
|
|
|
24
73
|
// Setup tracking file
|
|
25
74
|
const SETUP_COMPLETED_FILE = path.join(PROJECT_SETTINGS_DIR, 'setup.json');
|
|
@@ -381,7 +430,7 @@ function tryReadJson(filePath) {
|
|
|
381
430
|
|
|
382
431
|
const data = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
|
|
383
432
|
if (!data || data.trim() === '') {
|
|
384
|
-
|
|
433
|
+
logWarn(`[i18ntk] Warning: Empty or invalid JSON file at ${filePath}`);
|
|
385
434
|
return null;
|
|
386
435
|
}
|
|
387
436
|
|
|
@@ -390,18 +439,18 @@ function tryReadJson(filePath) {
|
|
|
390
439
|
return parsed;
|
|
391
440
|
}
|
|
392
441
|
|
|
393
|
-
|
|
442
|
+
logError(`[i18ntk] Error parsing JSON from ${filePath}: Invalid JSON content`);
|
|
394
443
|
// Create a backup of the corrupted file
|
|
395
444
|
const backupPath = `${filePath}.corrupted-${Date.now()}.bak`;
|
|
396
445
|
try {
|
|
397
446
|
SecurityUtils.safeWriteFileSync(backupPath, data, path.dirname(backupPath), 'utf8');
|
|
398
|
-
|
|
447
|
+
logWarn(`[i18ntk] Created backup of corrupted config at ${backupPath}`);
|
|
399
448
|
} catch (backupError) {
|
|
400
|
-
|
|
449
|
+
logError(`[i18ntk] Failed to create backup of corrupted config: ${backupError.message}`);
|
|
401
450
|
}
|
|
402
451
|
return null;
|
|
403
452
|
} catch (error) {
|
|
404
|
-
|
|
453
|
+
logError(`[i18ntk] Error reading config file at ${filePath}: ${error.message}`);
|
|
405
454
|
return null;
|
|
406
455
|
}
|
|
407
456
|
}
|
|
@@ -420,11 +469,11 @@ async function migrateLegacyIfNeeded(baseCfg) {
|
|
|
420
469
|
// Best-effort removal of legacy file to prevent future use
|
|
421
470
|
try { fs.unlinkSync(LEGACY_CONFIG_PATH); } catch (_) {}
|
|
422
471
|
// Deprecation notice
|
|
423
|
-
|
|
472
|
+
logWarn('[i18ntk] Deprecated config location detected (~/.i18ntk). Configuration was migrated to project .i18ntk-config.');
|
|
424
473
|
return merged;
|
|
425
474
|
} catch (_) {
|
|
426
475
|
// If write fails, fall back to in-memory config without deleting legacy
|
|
427
|
-
|
|
476
|
+
logWarn('[i18ntk] Deprecated config location detected (~/.i18ntk). Using migrated settings in memory; failed to persist to .i18ntk-config.');
|
|
428
477
|
return merged;
|
|
429
478
|
}
|
|
430
479
|
}
|
|
@@ -445,7 +494,7 @@ function loadConfig() {
|
|
|
445
494
|
|
|
446
495
|
// Prevent concurrent loading
|
|
447
496
|
if (configLoadInProgress) {
|
|
448
|
-
|
|
497
|
+
logWarn('[i18ntk] Configuration loading already in progress, returning defaults');
|
|
449
498
|
resetRecursionGuard();
|
|
450
499
|
return clone(DEFAULT_CONFIG);
|
|
451
500
|
}
|
|
@@ -472,7 +521,7 @@ function loadConfig() {
|
|
|
472
521
|
// Attempt to migrate to project settings
|
|
473
522
|
// Ignore migration errors; we still return merged cfg in memory
|
|
474
523
|
// eslint-disable-next-line no-unused-vars
|
|
475
|
-
|
|
524
|
+
logWarn('[i18ntk] Detected legacy config at ~/.i18ntk. Migrating to project settings directory...');
|
|
476
525
|
const _ = (async () => { await migrateLegacyIfNeeded(DEFAULT_CONFIG); })();
|
|
477
526
|
}
|
|
478
527
|
}
|
|
@@ -481,7 +530,8 @@ function loadConfig() {
|
|
|
481
530
|
currentConfig = cfg;
|
|
482
531
|
return currentConfig;
|
|
483
532
|
} catch (error) {
|
|
484
|
-
|
|
533
|
+
logError('[i18ntk] Error in loadConfig', { error: error.message });
|
|
534
|
+
notifyConfigFallback(error);
|
|
485
535
|
currentConfig = clone(DEFAULT_CONFIG);
|
|
486
536
|
return currentConfig;
|
|
487
537
|
} finally {
|
|
@@ -498,7 +548,7 @@ async function saveConfig(cfg = currentConfig) {
|
|
|
498
548
|
currentConfig = cfg;
|
|
499
549
|
if (!autosaveDisabledWarned) {
|
|
500
550
|
autosaveDisabledWarned = true;
|
|
501
|
-
|
|
551
|
+
logWarn('[i18ntk] Autosave disabled by I18NTK_DISABLE_AUTOSAVE. Keeping configuration in memory only.');
|
|
502
552
|
}
|
|
503
553
|
return false;
|
|
504
554
|
}
|
|
@@ -540,7 +590,7 @@ async function saveConfig(cfg = currentConfig) {
|
|
|
540
590
|
currentConfig = cfg;
|
|
541
591
|
return true;
|
|
542
592
|
} catch (error) {
|
|
543
|
-
|
|
593
|
+
logError('[i18ntk] Error saving configuration', { error: error.message });
|
|
544
594
|
return false;
|
|
545
595
|
} finally {
|
|
546
596
|
if (releaseLock) {
|
|
@@ -593,7 +643,7 @@ function getConfig() {
|
|
|
593
643
|
|
|
594
644
|
// Check for legacy config for migration
|
|
595
645
|
if (SecurityUtils.safeExistsSync(LEGACY_CONFIG_PATH)) {
|
|
596
|
-
|
|
646
|
+
logInfo('Migrating legacy configuration');
|
|
597
647
|
const legacyRaw = SecurityUtils.safeReadFileSync(LEGACY_CONFIG_PATH, path.dirname(LEGACY_CONFIG_PATH), 'utf8');
|
|
598
648
|
const legacyConfig = SecurityUtils.safeParseJSON(legacyRaw);
|
|
599
649
|
if (!legacyConfig || typeof legacyConfig !== 'object') {
|
|
@@ -601,7 +651,7 @@ function getConfig() {
|
|
|
601
651
|
}
|
|
602
652
|
const migratedConfig = { ...DEFAULT_CONFIG, ...legacyConfig };
|
|
603
653
|
saveConfig(migratedConfig).catch((err) => {
|
|
604
|
-
|
|
654
|
+
logWarn('[i18ntk] Warning: failed to persist migrated configuration', { error: err.message });
|
|
605
655
|
});
|
|
606
656
|
currentConfig = migratedConfig;
|
|
607
657
|
|
|
@@ -619,18 +669,18 @@ function getConfig() {
|
|
|
619
669
|
}
|
|
620
670
|
|
|
621
671
|
// Use package defaults for new installation
|
|
622
|
-
|
|
623
|
-
saveConfig(DEFAULT_CONFIG).catch((err) => {
|
|
624
|
-
|
|
625
|
-
});
|
|
626
|
-
currentConfig = DEFAULT_CONFIG;
|
|
627
|
-
return resolvePaths(DEFAULT_CONFIG);
|
|
628
|
-
|
|
629
|
-
} catch (error) {
|
|
630
|
-
|
|
631
|
-
currentConfig = DEFAULT_CONFIG;
|
|
632
|
-
return resolvePaths(DEFAULT_CONFIG);
|
|
633
|
-
}
|
|
672
|
+
notifyDefaultConfig('default initialization');
|
|
673
|
+
saveConfig(DEFAULT_CONFIG).catch((err) => {
|
|
674
|
+
logWarn('[i18ntk] Warning: failed to persist default configuration', { error: err.message });
|
|
675
|
+
});
|
|
676
|
+
currentConfig = DEFAULT_CONFIG;
|
|
677
|
+
return resolvePaths(DEFAULT_CONFIG);
|
|
678
|
+
|
|
679
|
+
} catch (error) {
|
|
680
|
+
notifyConfigFallback(error);
|
|
681
|
+
currentConfig = DEFAULT_CONFIG;
|
|
682
|
+
return resolvePaths(DEFAULT_CONFIG);
|
|
683
|
+
}
|
|
634
684
|
}
|
|
635
685
|
|
|
636
686
|
async function setConfig(cfg) {
|
|
@@ -693,3 +743,7 @@ module.exports = {
|
|
|
693
743
|
toRelative,
|
|
694
744
|
normalizePathValue,
|
|
695
745
|
}
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
|
package/utils/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const SecurityUtils = require('./security');
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const SecurityUtils = require('./security');
|
|
4
|
+
const { logger } = require('./logger');
|
|
4
5
|
|
|
5
6
|
const settingsManager = require('../settings/settings-manager');
|
|
6
7
|
const CONFIG_FILE = 'i18ntk-config.json';
|
|
@@ -66,11 +67,11 @@ function loadConfig(cwd = settingsManager.configDir) {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
throw new Error('Invalid configuration format');
|
|
69
|
-
} catch (error) {
|
|
70
|
-
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.debug(`Error loading config: ${error.message}`, { stack: error.stack });
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
77
|
* Saves configuration to file
|
|
@@ -96,11 +97,11 @@ function saveConfig(config, cwd = settingsManager.configDir) {
|
|
|
96
97
|
);
|
|
97
98
|
|
|
98
99
|
return true;
|
|
99
|
-
} catch (error) {
|
|
100
|
-
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger.error(`Error saving config: ${error.message}`);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
104
105
|
|
|
105
106
|
/**
|
|
106
107
|
* Ensures default values for configuration
|
|
@@ -127,4 +128,4 @@ module.exports = {
|
|
|
127
128
|
saveConfig,
|
|
128
129
|
ensureConfigDefaults,
|
|
129
130
|
validatePath // Exported for testing
|
|
130
|
-
};
|
|
131
|
+
};
|
package/utils/i18n-helper.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// utils/i18n-helper.js
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { logger } = require('./logger');
|
|
4
5
|
|
|
5
6
|
// Lazy load SecurityUtils to prevent circular dependencies
|
|
6
7
|
let securityUtils;
|
|
@@ -200,10 +201,21 @@ function findLocaleFilesAllDirs(lang, preferredDir) {
|
|
|
200
201
|
return files;
|
|
201
202
|
}
|
|
202
203
|
|
|
203
|
-
let translations = {};
|
|
204
|
-
let currentLanguage = 'en';
|
|
205
|
-
let isInitialized = false;
|
|
206
|
-
const
|
|
204
|
+
let translations = {};
|
|
205
|
+
let currentLanguage = 'en';
|
|
206
|
+
let isInitialized = false;
|
|
207
|
+
const missingKeyCache = new Map();
|
|
208
|
+
const missingKeyTtlMs = 5 * 60 * 1000;
|
|
209
|
+
|
|
210
|
+
function shouldReportMissingKey(key) {
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
const expiresAt = missingKeyCache.get(key) || 0;
|
|
213
|
+
if (expiresAt > now) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
missingKeyCache.set(key, now + missingKeyTtlMs);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
207
219
|
|
|
208
220
|
function loadTranslations(language) {
|
|
209
221
|
const cfg = safeRequireConfig();
|
|
@@ -286,9 +298,11 @@ function loadTranslations(language) {
|
|
|
286
298
|
currentLanguage = 'en';
|
|
287
299
|
isInitialized = true;
|
|
288
300
|
|
|
289
|
-
if (loadErrors.length > 0) {
|
|
290
|
-
|
|
291
|
-
|
|
301
|
+
if (loadErrors.length > 0) {
|
|
302
|
+
logger.warn('No valid UI locale files found. Using built-in English strings.', {
|
|
303
|
+
errorCount: loadErrors.length
|
|
304
|
+
});
|
|
305
|
+
}
|
|
292
306
|
|
|
293
307
|
return translations;
|
|
294
308
|
}
|
|
@@ -337,23 +351,24 @@ function t(key, params = {}) {
|
|
|
337
351
|
}
|
|
338
352
|
}
|
|
339
353
|
|
|
340
|
-
if (typeof value === 'undefined') {
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
354
|
+
if (typeof value === 'undefined') {
|
|
355
|
+
if (shouldReportMissingKey(key)) {
|
|
356
|
+
logger.logMissingTranslationKey(key, 'Configuration error');
|
|
357
|
+
}
|
|
358
|
+
return key;
|
|
359
|
+
}
|
|
347
360
|
|
|
348
361
|
// If we found a string, interpolate parameters
|
|
349
362
|
if (typeof value === 'string') {
|
|
350
363
|
return interpolateParams(value, params);
|
|
351
364
|
}
|
|
352
365
|
|
|
353
|
-
// Return the key if the final value is not a string
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
366
|
+
// Return the key if the final value is not a string
|
|
367
|
+
if (shouldReportMissingKey(`${key}:non-string`)) {
|
|
368
|
+
logger.warn(`Translation key does not resolve to a string: ${key}`);
|
|
369
|
+
}
|
|
370
|
+
return key;
|
|
371
|
+
}
|
|
357
372
|
|
|
358
373
|
/**
|
|
359
374
|
* Interpolate parameters into a translation string
|