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.
Files changed (40) hide show
  1. package/CHANGELOG.md +385 -0
  2. package/README.md +56 -47
  3. package/main/i18ntk-analyze.js +4 -4
  4. package/main/i18ntk-scanner.js +14 -12
  5. package/main/i18ntk-translate.js +502 -0
  6. package/main/i18ntk-validate.js +25 -18
  7. package/main/manage/commands/AnalyzeCommand.js +7 -4
  8. package/main/manage/commands/CommandRouter.js +7 -1
  9. package/main/manage/commands/FixerCommand.js +11 -1
  10. package/main/manage/commands/ScannerCommand.js +12 -10
  11. package/main/manage/commands/TranslateCommand.js +242 -0
  12. package/main/manage/commands/ValidateCommand.js +21 -17
  13. package/main/manage/index.js +17 -12
  14. package/package.json +13 -3
  15. package/runtime/enhanced.js +64 -10
  16. package/runtime/i18ntk.d.ts +10 -6
  17. package/runtime/index.js +45 -22
  18. package/ui-locales/de.json +3 -0
  19. package/ui-locales/en.json +3 -0
  20. package/ui-locales/es.json +3 -0
  21. package/ui-locales/fr.json +3 -0
  22. package/ui-locales/ja.json +3 -0
  23. package/ui-locales/ru.json +3 -1
  24. package/ui-locales/zh.json +3 -0
  25. package/utils/admin-auth.js +4 -1
  26. package/utils/config-helper.js +43 -37
  27. package/utils/config-manager.js +59 -49
  28. package/utils/config.js +13 -4
  29. package/utils/env-manager.js +3 -1
  30. package/utils/i18n-helper.js +41 -13
  31. package/utils/init-helper.js +23 -21
  32. package/utils/secure-errors.js +10 -6
  33. package/utils/security.js +30 -4
  34. package/utils/setup-enforcer.js +22 -33
  35. package/utils/translate/api.js +168 -0
  36. package/utils/translate/cli.js +91 -0
  37. package/utils/translate/placeholder.js +93 -0
  38. package/utils/translate/report.js +90 -0
  39. package/utils/translate/traverse.js +148 -0
  40. 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 hasPythonFiles = fs.readdirSync(projectRoot, { recursive: true })
180
- .some(file => file.endsWith && file.endsWith('.py'));
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 = fs.readdirSync(currentDir);
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 = fs.statSync(fullPath);
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
- fs.mkdirSync(outputDir, { recursive: true });
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
- fs.mkdirSync(this.sourceLanguageDir, { recursive: true });
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 languages = fs.readdirSync(this.sourceDir)
189
- .filter(item => {
190
- const itemPath = path.join(this.sourceDir, item);
191
- return fs.statSync(itemPath).isDirectory() &&
192
- item !== this.config.sourceLanguage &&
193
- !this.isExcludedLanguageDirectory(item);
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 files = fs.readdirSync(languageDir)
213
- .filter(file => {
214
- return file.endsWith('.json') &&
215
- !this.config.excludeFiles.includes(file);
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(reportPath)) {
669
- fs.unlinkSync(reportPath);
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', {
@@ -765,10 +765,11 @@ class I18nManager {
765
765
 
766
766
  function findFiles(dir, results = []) {
767
767
  try {
768
- const items = fs.readdirSync(dir);
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
- const stat = fs.statSync(fullPath);
780
-
781
- if (stat.isDirectory()) {
780
+ if (item.isDirectory()) {
782
781
  findFiles(fullPath, results);
783
- } else if (stat.isFile()) {
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 '0':
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": "2.5.1",
4
- "description": "Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, fixing, reporting, and runtime translation loading.",
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",
@@ -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
- // Ensure cleanup on process exit
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) throw new Error('Encryption key not set');
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
- const data = JSON.parse(encryptedData);
185
- const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(key, 'hex'), Buffer.from(data.iv, 'hex'));
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
- throw new Error(`Decryption failed: ${error.message}`);
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
@@ -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
- * Main initialization function
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
- * Basic initialization function (backward compatibility)
486
+ * Initialize the basic lightweight runtime (synchronous)
487
+ * This is the default export from 'i18ntk/runtime'
487
488
  */
488
- export declare function initRuntime(config: {
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
  /**