i18ntk 2.5.0 → 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.
@@ -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
  /**
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
- return JSON.parse(stripBOMAndComments(raw));
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
- for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
97
- const full = path.join(d, entry.name);
98
- if (entry.isDirectory()) {
99
- stack.push(full);
100
- } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
101
- results.push(full);
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
- if (SecurityUtils.safeExistsSync(langDir) && fs.statSync(langDir).isDirectory()) {
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 if (SecurityUtils.safeExistsSync(langFile) && fs.statSync(langFile).isFile()) {
125
- try {
126
- const data = readJsonSafe(langFile);
127
- if (data && typeof data === 'object') deepMerge(merged, data);
128
- } catch (_) { /* ignore */ }
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
- for (const entry of fs.readdirSync(state.baseDir, { withFileTypes: true })) {
215
- if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
216
- langs.add(entry.name.replace(/\.json$/i, ''));
217
- } else if (entry.isDirectory()) {
218
- // language folder convention
219
- const lang = entry.name;
220
- const idx = path.join(state.baseDir, lang, `${lang}.json`);
221
- if (SecurityUtils.safeExistsSync(idx)) langs.add(lang);
222
- else langs.add(lang); // be permissive
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
  }
@@ -188,14 +188,60 @@ class AdminAuth {
188
188
  return crypto.timingSafeEqual(left, right);
189
189
  }
190
190
 
191
+ hasUsablePinConfig(config) {
192
+ return Boolean(
193
+ config &&
194
+ config.enabled === true &&
195
+ typeof config.pinHash === 'string' &&
196
+ config.pinHash.length > 0 &&
197
+ typeof config.salt === 'string' &&
198
+ config.salt.length > 0
199
+ );
200
+ }
201
+
202
+ logMissingPinConfig(context) {
203
+ SecurityUtils.logSecurityEvent(
204
+ 'admin_auth_config_invalid',
205
+ 'warning',
206
+ { message: `Admin PIN is required but not usable during ${context}` }
207
+ );
208
+ }
209
+
210
+ getSessionExpiryTime(session) {
211
+ if (!session || typeof session !== 'object') {
212
+ return NaN;
213
+ }
214
+
215
+ if (Number.isFinite(session.expiresAt)) {
216
+ return session.expiresAt;
217
+ }
218
+
219
+ if (typeof session.expiresAt === 'string') {
220
+ const parsedExpiresAt = Date.parse(session.expiresAt);
221
+ if (Number.isFinite(parsedExpiresAt)) {
222
+ return parsedExpiresAt;
223
+ }
224
+ }
225
+
226
+ if (typeof session.expires === 'string') {
227
+ const parsedExpires = Date.parse(session.expires);
228
+ if (Number.isFinite(parsedExpires)) {
229
+ return parsedExpires;
230
+ }
231
+ }
232
+
233
+ return NaN;
234
+ }
235
+
191
236
  /**
192
237
  * Verify PIN
193
238
  */
194
239
  async verifyPin(pin) {
195
240
  try {
196
241
  const config = await this.loadConfig();
197
- if (!config || !config.enabled) {
198
- return true; // No authentication required if not enabled
242
+ if (!this.hasUsablePinConfig(config)) {
243
+ this.logMissingPinConfig('PIN verification');
244
+ return false;
199
245
  }
200
246
 
201
247
  // Check for lockout
@@ -256,7 +302,7 @@ class AdminAuth {
256
302
  */
257
303
  async isPinConfigured() {
258
304
  const config = await this.loadConfig();
259
- return config && config.enabled && config.pinHash;
305
+ return this.hasUsablePinConfig(config);
260
306
  }
261
307
 
262
308
  /**
@@ -270,7 +316,10 @@ class AdminAuth {
270
316
  }
271
317
 
272
318
  const config = await this.loadConfig();
273
- return config && config.enabled;
319
+ if (!this.hasUsablePinConfig(config)) {
320
+ this.logMissingPinConfig('auth-required check');
321
+ }
322
+ return true;
274
323
  }
275
324
 
276
325
  /**
@@ -283,12 +332,6 @@ class AdminAuth {
283
332
  return false;
284
333
  }
285
334
 
286
- // Check if admin PIN is actually configured
287
- const config = await this.loadConfig();
288
- if (!config || !config.enabled || !config.pinHash) {
289
- return false; // Don't require PIN if admin PIN is not configured
290
- }
291
-
292
335
  // Check if PIN protection is enabled
293
336
  const pinProtection = globalSettings.security?.pinProtection;
294
337
  if (!pinProtection || !pinProtection.enabled) {
@@ -297,7 +340,16 @@ class AdminAuth {
297
340
 
298
341
  // Check if this specific script requires protection
299
342
  const protectedScripts = pinProtection.protectedScripts || {};
300
- return protectedScripts[scriptName] !== false; // Default to true if not explicitly set
343
+ if (protectedScripts[scriptName] === false) {
344
+ return false;
345
+ }
346
+
347
+ const config = await this.loadConfig();
348
+ if (!this.hasUsablePinConfig(config)) {
349
+ this.logMissingPinConfig(`script auth-required check for ${scriptName}`);
350
+ }
351
+
352
+ return true; // Default to true if not explicitly set
301
353
  }
302
354
 
303
355
  /**
@@ -322,7 +374,10 @@ class AdminAuth {
322
374
  process.exit(0);
323
375
  });
324
376
  process.on('uncaughtException', (error) => {
325
- SecurityUtils.logSecurityEvent('uncaught_exception', 'error', error.message);
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
+ });
326
381
  cleanup();
327
382
  process.exit(1);
328
383
  });
@@ -336,11 +391,14 @@ class AdminAuth {
336
391
  sessionId = this.generateSessionId();
337
392
  }
338
393
 
394
+ const now = Date.now();
395
+ const expiresAt = now + this.sessionTimeout;
339
396
  const session = {
340
397
  id: sessionId,
341
- created: new Date().toISOString(),
342
- lastActivity: new Date().toISOString(),
343
- expires: new Date(Date.now() + this.sessionTimeout).toISOString()
398
+ created: new Date(now).toISOString(),
399
+ lastActivity: new Date(now).toISOString(),
400
+ expires: new Date(expiresAt).toISOString(),
401
+ expiresAt
344
402
  };
345
403
 
346
404
  this.activeSessions.set(sessionId, session);
@@ -373,10 +431,10 @@ class AdminAuth {
373
431
  return false;
374
432
  }
375
433
 
376
- const now = new Date();
377
- const expires = new Date(session.expires);
434
+ const now = Date.now();
435
+ const expiresAt = this.getSessionExpiryTime(session);
378
436
 
379
- if (now > expires) {
437
+ if (!Number.isFinite(expiresAt) || now > expiresAt) {
380
438
  this.activeSessions.delete(sessionId);
381
439
  this.clearCurrentSession();
382
440
  SecurityUtils.logSecurityEvent(
@@ -388,8 +446,10 @@ class AdminAuth {
388
446
  }
389
447
 
390
448
  // Update last activity
391
- session.lastActivity = now.toISOString();
392
- session.expires = new Date(now.getTime() + this.sessionTimeout).toISOString();
449
+ const nextExpiresAt = now + this.sessionTimeout;
450
+ session.lastActivity = new Date(now).toISOString();
451
+ session.expires = new Date(nextExpiresAt).toISOString();
452
+ session.expiresAt = nextExpiresAt;
393
453
  this.activeSessions.set(sessionId, session);
394
454
 
395
455
  return true;
@@ -571,9 +631,13 @@ class AdminAuth {
571
631
  let cleaned = 0;
572
632
 
573
633
  for (const [sessionId, session] of this.activeSessions.entries()) {
574
- const expiresAt = session.expiresAt || new Date(session.expires).getTime();
634
+ const expiresAt = this.getSessionExpiryTime(session);
575
635
  if (!Number.isFinite(expiresAt) || now > expiresAt) {
576
636
  this.activeSessions.delete(sessionId);
637
+ if (this.currentSession && this.currentSession.id === sessionId) {
638
+ this.currentSession = null;
639
+ this.sessionStartTime = null;
640
+ }
577
641
  cleaned++;
578
642
  }
579
643
  }
@@ -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
- cfg = SecurityUtils.safeExistsSync(configFile) ? JSON.parse(SecurityUtils.safeReadFileSync(configFile, settingsDir, 'utf8')) : {};
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 isHardcodedPath = cfg.projectRoot && cfg.projectRoot.includes('i18n-management-toolkit-main');
64
- projectRoot = isHardcodedPath ? process.cwd() : path.resolve(cfg.projectRoot || '.');
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 (isHardcodedPath) {
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
- fs.mkdirSync(dirPath, { recursive: true });
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