i18ntk 4.3.2 → 4.4.1

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.
@@ -356,25 +356,38 @@ async function cleanupOldBackups(backupDirPath) {
356
356
  }
357
357
 
358
358
  async function handleCreate(args) {
359
- const dir = args._[1] || path.join(__dirname, '..', 'locales');
360
- const outputDir = args.output || backupDir;
359
+ const rawSourceDir = args._[1] || path.join(__dirname, '..', 'locales');
360
+ const rawOutputDir = args.output || backupDir;
361
+
362
+ // Validate both paths against project root (cwd) for security
363
+ const sourceDir = path.resolve(rawSourceDir);
364
+ if (!SecurityUtils.validatePath(sourceDir, process.cwd())) {
365
+ throw new Error(`Source directory is outside the allowed project boundary.`);
366
+ }
367
+
368
+ const outputDir = path.resolve(rawOutputDir);
369
+ const validatedOutputDir = SecurityUtils.validatePath(outputDir, process.cwd());
370
+ if (!validatedOutputDir) {
371
+ throw new Error(`Output directory is outside the allowed project boundary.`);
372
+ }
373
+
361
374
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
362
375
  const backupName = `backup-${timestamp}.json`;
363
- const backupPath = path.join(outputDir, backupName);
376
+ const backupPath = path.join(validatedOutputDir, backupName);
364
377
  const isIncremental = args.incremental !== 'false' && args.incremental !== false;
365
378
 
366
- logger.debug(`Source directory: ${dir}`);
379
+ logger.debug(`Source directory: ${sourceDir}`);
367
380
  logger.debug(`Backup will be saved to: ${backupPath}`);
368
381
 
369
382
  try {
370
- await fsp.mkdir(outputDir, { recursive: true });
371
- logger.debug(`Created backup directory: ${outputDir}`);
383
+ SecurityUtils.safeMkdirSync(validatedOutputDir, process.cwd());
384
+ logger.debug(`Created backup directory: ${validatedOutputDir}`);
372
385
  } catch (err) {
373
386
  if (err.code !== 'EEXIST') {
374
387
  logger.error(`Failed to create backup directory: ${err.message}`);
375
388
  throw err;
376
389
  }
377
- logger.debug(`Using existing backup directory: ${outputDir}`);
390
+ logger.debug(`Using existing backup directory: ${validatedOutputDir}`);
378
391
  }
379
392
 
380
393
  const sourceDir = path.resolve(dir);
@@ -463,7 +476,7 @@ async function handleCreate(args) {
463
476
  backupData[file] = content;
464
477
  }
465
478
 
466
- await fsp.writeFile(backupPath, JSON.stringify(backupData, null, 2));
479
+ SecurityUtils.safeWriteFileSync(backupPath, JSON.stringify(backupData, null, 2), validatedOutputDir);
467
480
  const stats = await fsp.stat(backupPath);
468
481
 
469
482
  logger.success('Backup created successfully');
@@ -483,9 +496,11 @@ async function handleRestore(args) {
483
496
  }
484
497
 
485
498
  const backupPath = path.resolve(process.cwd(), backupFile);
486
- const outputDir = args.output
487
- ? path.resolve(process.cwd(), args.output)
488
- : path.join(process.cwd(), 'restored');
499
+ const rawOutputDir = args.output || path.join(process.cwd(), 'restored');
500
+ const validatedOutputDir = SecurityUtils.validatePath(rawOutputDir, process.cwd());
501
+ if (!validatedOutputDir) {
502
+ throw new Error(`Restore output directory is outside the allowed project boundary.`);
503
+ }
489
504
 
490
505
  if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
491
506
  throw new Error(`Backup file not found: ${backupPath}`);
@@ -494,36 +509,38 @@ async function handleRestore(args) {
494
509
  logger.info('\nRestoring backup...');
495
510
 
496
511
  try {
497
- const backupData = JSON.parse(await fsp.readFile(backupPath, 'utf8'));
512
+ const rawContent = SecurityUtils.safeReadFileSync(backupPath, process.cwd(), 'utf8');
513
+ if (!rawContent) throw new Error(`Could not read backup file: ${backupPath}`);
514
+ const backupData = JSON.parse(rawContent);
498
515
  const isIncremental = backupData._meta && backupData._meta.type === 'incremental';
499
516
 
500
517
  if (isIncremental) {
501
518
  const chain = await buildRestoreChain(backupPath, backupData);
502
- await fsp.mkdir(outputDir, { recursive: true });
519
+ SecurityUtils.safeMkdirSync(validatedOutputDir, process.cwd());
503
520
 
504
- const restoredFiles = new Set();
505
- for (const entry of chain) {
506
- for (const [file, content] of Object.entries(entry.data)) {
507
- if (restoreBackupEntry(outputDir, file, content)) {
508
- restoredFiles.add(file);
509
- }
510
- }
511
- }
521
+ const restoredFiles = new Set();
522
+ for (const entry of chain) {
523
+ for (const [file, content] of Object.entries(entry.data)) {
524
+ if (restoreBackupEntry(validatedOutputDir, file, content)) {
525
+ restoredFiles.add(file);
526
+ }
527
+ }
528
+ }
512
529
 
513
530
  logger.success('Incremental backup restored successfully');
514
- logger.info(` ${restoredFiles.size} files restored across ${chain.length} backup(s) to: ${outputDir}`);
531
+ logger.info(` ${restoredFiles.size} files restored across ${chain.length} backup(s) to: ${validatedOutputDir}`);
515
532
  } else {
516
- await fsp.mkdir(outputDir, { recursive: true });
533
+ SecurityUtils.safeMkdirSync(validatedOutputDir, process.cwd());
517
534
 
518
- let count = 0;
519
- for (const [file, content] of Object.entries(backupData)) {
520
- if (restoreBackupEntry(outputDir, file, content)) {
521
- count++;
522
- }
523
- }
535
+ let count = 0;
536
+ for (const [file, content] of Object.entries(backupData)) {
537
+ if (restoreBackupEntry(validatedOutputDir, file, content)) {
538
+ count++;
539
+ }
540
+ }
524
541
 
525
542
  logger.success('Backup restored successfully');
526
- logger.info(` Restored ${count} files to: ${outputDir}`);
543
+ logger.info(` Restored ${count} files to: ${validatedOutputDir}`);
527
544
  }
528
545
  } catch (error) {
529
546
  handleError(error);
@@ -532,37 +549,43 @@ async function handleRestore(args) {
532
549
 
533
550
  async function handleList() {
534
551
  try {
535
- // Ensure backup directory exists
536
- try {
537
- await fsp.access(backupDir);
538
- } catch (err) {
539
- if (err.code === 'ENOENT') {
540
- logger.warn('No backups found. The backup directory does not exist yet.');
541
- } else {
542
- logger.error(`Error accessing backup directory: ${err.message}`);
543
- }
552
+ const validatedBackupDir = SecurityUtils.validatePath(backupDir, process.cwd());
553
+ if (!validatedBackupDir) {
554
+ logger.error('Backup directory is outside allowed project boundary.');
555
+ return;
556
+ }
557
+
558
+ if (!SecurityUtils.safeExistsSync(validatedBackupDir, process.cwd())) {
559
+ logger.warn('No backups found. The backup directory does not exist yet.');
560
+ return;
561
+ }
562
+
563
+ const files = SecurityUtils.safeReaddirSync(validatedBackupDir, process.cwd());
564
+ if (!files || files.length === 0) {
565
+ logger.warn('No valid backup files found in the backup directory.');
544
566
  return;
545
567
  }
546
568
 
547
- const files = await fsp.readdir(backupDir);
548
569
  const backups = [];
549
-
570
+
550
571
  for (const file of files) {
551
- if (file.startsWith('backup-') && file.endsWith('.json')) {
572
+ if (Array.isArray(files) && file.startsWith('backup-') && file.endsWith('.json')) {
552
573
  try {
553
- const filePath = path.join(backupDir, file);
554
- const stats = await fsp.stat(filePath);
555
- backups.push({
556
- name: file,
557
- path: filePath,
558
- size: stats.size,
559
- createdAt: stats.mtime
560
- });
574
+ const filePath = path.join(validatedBackupDir, file);
575
+ const stats = SecurityUtils.safeStatSync(filePath, process.cwd());
576
+ if (stats) {
577
+ backups.push({
578
+ name: file,
579
+ path: filePath,
580
+ size: stats.size,
581
+ createdAt: stats.mtime
582
+ });
583
+ }
561
584
  } catch (err) {
562
585
  logger.warn(`Skipping invalid backup file ${file}: ${err.message}`);
563
586
  }
564
587
  }
565
- }
588
+ }
566
589
 
567
590
  if (backups.length === 0) {
568
591
  logger.warn('No valid backup files found in the backup directory.');
@@ -610,7 +633,9 @@ async function handleVerify(args) {
610
633
  logger.info('\nVerifying backup...');
611
634
 
612
635
  try {
613
- const data = JSON.parse(await fsp.readFile(backupPath, 'utf8'));
636
+ const rawContent = SecurityUtils.safeReadFileSync(backupPath, process.cwd(), 'utf8');
637
+ if (!rawContent) throw new Error(`Could not read backup file: ${backupPath}`);
638
+ const data = JSON.parse(rawContent);
614
639
 
615
640
  if (data._meta && data._meta.hashes) {
616
641
  logger.info(' Performing hash chain verification...');
@@ -13,9 +13,9 @@
13
13
 
14
14
 
15
15
  const path = require('path');
16
+ const fs = require('fs');
16
17
  const SecurityUtils = require('../utils/security');
17
- const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../utils/config-helper');
18
- const { loadTranslations, t } = require('../utils/i18n-helper');
18
+ const { loadTranslations, t } = require('../utils/i18n-helper');
19
19
  const { getGlobalReadline, closeGlobalReadline } = require('../utils/cli');
20
20
  const SetupEnforcer = require('../utils/setup-enforcer');
21
21
 
@@ -559,12 +559,23 @@ class I18nCompletionTool {
559
559
  await this.initialize();
560
560
 
561
561
  if (args.sourceDir) {
562
- this.config.sourceDir = args.sourceDir;
563
- this.sourceDir = path.resolve(this.config.sourceDir);
562
+ const resolvedSourceDir = path.resolve(args.sourceDir);
563
+ const validatedSourceDir = SecurityUtils.validatePath(resolvedSourceDir, process.cwd());
564
+ if (!validatedSourceDir) {
565
+ console.error(t("complete.invalidSourceDir", { sourceDir: args.sourceDir }));
566
+ console.error(`Path validation failed — source directory is outside the allowed project boundary.`);
567
+ process.exit(1);
568
+ }
569
+ this.config.sourceDir = resolvedSourceDir;
570
+ this.sourceDir = validatedSourceDir;
564
571
  }
565
572
 
566
573
  if (args.sourceLanguage) {
567
- this.config.sourceLanguage = args.sourceLanguage;
574
+ this.config.sourceLanguage = SecurityUtils.sanitizeInput(args.sourceLanguage, {
575
+ allowedChars: /^[a-zA-Z0-9\-_]+$/,
576
+ maxLength: 10,
577
+ removeHTML: true
578
+ });
568
579
  }
569
580
 
570
581
  this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
@@ -33,6 +33,11 @@ const SetupEnforcer = require('../utils/setup-enforcer');
33
33
  }
34
34
  })();
35
35
 
36
+ console.error('Setup check failed:', error.message);
37
+ process.exit(1);
38
+ }
39
+ })();
40
+
36
41
  loadTranslations();
37
42
 
38
43
  class I18nTextScanner {
@@ -65,7 +65,7 @@ const {
65
65
  } = require('../utils/translate/protection');
66
66
  const { translateBatch, DEFAULT_CONCURRENCY, clampProviderConcurrency } = require('../utils/translate/api');
67
67
  const { collectLeaves, getLeaf, setLeaf, deepClone } = require('../utils/translate/traverse');
68
- const { generateReport, writeReport, formatSummaryLine } = require('../utils/translate/report');
68
+ const { generateReport, writeReport, writeResidualReport, formatSummaryLine } = require('../utils/translate/report');
69
69
  const { analyzeEnglishContent } = require('../utils/validation-risk');
70
70
  const {
71
71
  confirmGlobalChoice,
@@ -996,7 +996,7 @@ async function processFile(sourcePath, targetLang, args) {
996
996
  }
997
997
 
998
998
  if (residualUntranslated.length > 0) {
999
- console.warn(`[${fileName}] Warning: ${residualUntranslated.length} values still look untranslated after retry. Rerun Auto Translate to capture leftovers.`);
999
+ console.warn(`[${fileName}] Warning: ${residualUntranslated.length} values still look untranslated after retry. A resume report will capture these keys.`);
1000
1000
  }
1001
1001
 
1002
1002
  console.log(`[${fileName}] Writing output.`);
@@ -1092,7 +1092,15 @@ async function run(args) {
1092
1092
 
1093
1093
  if (allResidualUntranslated.length > 0) {
1094
1094
  console.warn(`WARNING: ${allResidualUntranslated.length} values still look untranslated after Auto Translate.`);
1095
- console.warn('Rerun Auto Translate to capture leftovers, or review the values listed in the report.');
1095
+ const residualReportPath = writeResidualReport(allResidualUntranslated, {
1096
+ sourceFile: sourceFiles.length === 1 ? sourceFiles[0] : `${sourceFiles.length} files`,
1097
+ targetLang: args.targetLang,
1098
+ });
1099
+ if (residualReportPath) {
1100
+ console.warn(`Auto Translate resume report written: ${residualReportPath}`);
1101
+ } else {
1102
+ console.warn('Auto Translate could not write the resume report. Review the values listed in the report output.');
1103
+ }
1096
1104
  }
1097
1105
 
1098
1106
  if (allSkippedKeys.length > 0 || allResidualUntranslated.length > 0 || args.reportFile || args.reportStdout) {