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.
@@ -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
- fs.rmSync(staleDir.path, { recursive: true, force: true });
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 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, '-');
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18ntk",
3
- "version": "2.5.1",
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",
@@ -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
  }
@@ -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', 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
+ });
378
381
  cleanup();
379
382
  process.exit(1);
380
383
  });
@@ -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