i18nsmith 0.2.1 → 0.3.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.
Files changed (53) hide show
  1. package/dist/commands/audit.d.ts.map +1 -1
  2. package/dist/commands/backup.d.ts.map +1 -1
  3. package/dist/commands/check.d.ts +25 -0
  4. package/dist/commands/check.d.ts.map +1 -1
  5. package/dist/commands/config.d.ts.map +1 -1
  6. package/dist/commands/debug-patterns.d.ts.map +1 -1
  7. package/dist/commands/diagnose.d.ts.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/install-hooks.d.ts.map +1 -1
  10. package/dist/commands/preflight.d.ts.map +1 -1
  11. package/dist/commands/rename.d.ts.map +1 -1
  12. package/dist/commands/review.d.ts.map +1 -1
  13. package/dist/commands/scaffold-adapter.d.ts.map +1 -1
  14. package/dist/commands/scan.d.ts.map +1 -1
  15. package/dist/commands/sync.d.ts +1 -1
  16. package/dist/commands/sync.d.ts.map +1 -1
  17. package/dist/commands/transform.d.ts.map +1 -1
  18. package/dist/commands/translate/index.d.ts.map +1 -1
  19. package/dist/index.js +2536 -107783
  20. package/dist/rename-suspicious.test.d.ts +2 -0
  21. package/dist/rename-suspicious.test.d.ts.map +1 -0
  22. package/dist/utils/diff-utils.d.ts +5 -0
  23. package/dist/utils/diff-utils.d.ts.map +1 -1
  24. package/dist/utils/errors.d.ts +8 -0
  25. package/dist/utils/errors.d.ts.map +1 -0
  26. package/dist/utils/locale-audit.d.ts +39 -0
  27. package/dist/utils/locale-audit.d.ts.map +1 -0
  28. package/dist/utils/preview.d.ts.map +1 -1
  29. package/package.json +5 -5
  30. package/src/commands/audit.ts +18 -209
  31. package/src/commands/backup.ts +67 -63
  32. package/src/commands/check.ts +119 -68
  33. package/src/commands/config.ts +117 -95
  34. package/src/commands/debug-patterns.ts +25 -22
  35. package/src/commands/diagnose.ts +29 -26
  36. package/src/commands/init.ts +84 -79
  37. package/src/commands/install-hooks.ts +18 -15
  38. package/src/commands/preflight.ts +21 -13
  39. package/src/commands/rename.ts +86 -81
  40. package/src/commands/review.ts +81 -78
  41. package/src/commands/scaffold-adapter.ts +8 -4
  42. package/src/commands/scan.ts +61 -58
  43. package/src/commands/sync.ts +640 -203
  44. package/src/commands/transform.ts +46 -18
  45. package/src/commands/translate/index.ts +7 -4
  46. package/src/e2e.test.ts +34 -14
  47. package/src/integration.test.ts +86 -0
  48. package/src/rename-suspicious.test.ts +124 -0
  49. package/src/utils/diff-utils.ts +6 -0
  50. package/src/utils/errors.ts +34 -0
  51. package/src/utils/locale-audit.ts +219 -0
  52. package/src/utils/preview.test.ts +43 -0
  53. package/src/utils/preview.ts +2 -8
@@ -6,6 +6,7 @@ import chalk from 'chalk';
6
6
  import { diagnoseWorkspace, I18nConfig, TranslationConfig, ensureGitignore } from '@i18nsmith/core';
7
7
  import { scaffoldTranslationContext, scaffoldI18next } from '../utils/scaffold.js';
8
8
  import { hasDependency, readPackageJson } from '../utils/pkg.js';
9
+ import { CliError, withErrorHandling } from '../utils/errors.js';
9
10
 
10
11
  /**
11
12
  * Parse a comma-separated list of glob patterns, respecting brace expansions.
@@ -184,7 +185,8 @@ async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promis
184
185
 
185
186
  console.log(chalk.blue('\nRun "i18nsmith check" to verify your setup.'));
186
187
  } catch (error) {
187
- console.error(chalk.red('Failed to write configuration file:'), error);
188
+ const message = error instanceof Error ? error.message : String(error);
189
+ throw new CliError(`Failed to write configuration file: ${message}`);
188
190
  }
189
191
  }
190
192
 
@@ -194,14 +196,15 @@ export function registerInit(program: Command) {
194
196
  .description('Initialize i18nsmith configuration')
195
197
  .option('--merge', 'Merge with existing locales/runtimes when detected', false)
196
198
  .option('-y, --yes', 'Skip prompts and use defaults (non-interactive mode)', false)
197
- .action(async (commandOptions: InitCommandOptions) => {
198
- console.log(chalk.blue('Initializing i18nsmith configuration...'));
199
-
200
- // Non-interactive mode with sensible defaults
201
- if (commandOptions.yes) {
202
- await runNonInteractiveInit(commandOptions);
203
- return;
204
- }
199
+ .action(
200
+ withErrorHandling(async (commandOptions: InitCommandOptions) => {
201
+ console.log(chalk.blue('Initializing i18nsmith configuration...'));
202
+
203
+ // Non-interactive mode with sensible defaults
204
+ if (commandOptions.yes) {
205
+ await runNonInteractiveInit(commandOptions);
206
+ return;
207
+ }
205
208
 
206
209
  const answers = await inquirer.prompt<InitAnswers>([
207
210
  {
@@ -380,85 +383,87 @@ export function registerInit(program: Command) {
380
383
  seedTargetLocales: answers.seedTargetLocales,
381
384
  };
382
385
 
383
- const workspaceRoot = process.cwd();
384
- const mergeDecision = await maybePromptMergeStrategy(config, workspaceRoot, Boolean(commandOptions.merge));
385
- if (mergeDecision?.aborted) {
386
- console.log(chalk.yellow('Aborting init to avoid overwriting existing i18n assets. Re-run with --merge to bypass.'));
387
- return;
388
- }
389
-
390
- const configPath = path.join(workspaceRoot, 'i18n.config.json');
391
-
392
- try {
393
- await fs.writeFile(configPath, JSON.stringify(config, null, 2));
394
- console.log(chalk.green(`\nConfiguration created at ${configPath}`));
395
-
396
- // Ensure .gitignore has i18nsmith artifacts
397
- const gitignoreResult = await ensureGitignore(workspaceRoot);
398
- if (gitignoreResult.updated) {
399
- console.log(chalk.green(`Updated .gitignore with i18nsmith artifacts`));
386
+ const workspaceRoot = process.cwd();
387
+ const mergeDecision = await maybePromptMergeStrategy(config, workspaceRoot, Boolean(commandOptions.merge));
388
+ if (mergeDecision?.aborted) {
389
+ console.log(chalk.yellow('Aborting init to avoid overwriting existing i18n assets. Re-run with --merge to bypass.'));
390
+ return;
400
391
  }
401
392
 
402
- if (answers.scaffoldAdapter && answers.scaffoldAdapterPath) {
403
- try {
404
- await scaffoldTranslationContext(answers.scaffoldAdapterPath, answers.sourceLanguage, {
405
- localesDir: answers.localesDir,
406
- });
407
- console.log(chalk.green(`Translation context scaffolded at ${answers.scaffoldAdapterPath}`));
408
- } catch (error) {
409
- console.warn(chalk.yellow(`Skipping adapter scaffold: ${(error as Error).message}`));
393
+ const configPath = path.join(workspaceRoot, 'i18n.config.json');
394
+
395
+ try {
396
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
397
+ console.log(chalk.green(`\nConfiguration created at ${configPath}`));
398
+
399
+ // Ensure .gitignore has i18nsmith artifacts
400
+ const gitignoreResult = await ensureGitignore(workspaceRoot);
401
+ if (gitignoreResult.updated) {
402
+ console.log(chalk.green(`Updated .gitignore with i18nsmith artifacts`));
410
403
  }
411
- }
412
404
 
413
- if (
414
- answers.adapterPreset === 'react-i18next' &&
415
- answers.scaffoldReactRuntime &&
416
- answers.reactI18nPath &&
417
- answers.reactProviderPath
418
- ) {
419
- try {
420
- await scaffoldI18next(
421
- answers.reactI18nPath,
422
- answers.reactProviderPath,
423
- answers.sourceLanguage,
424
- answers.localesDir
425
- );
426
- console.log(chalk.green('react-i18next runtime scaffolded:'));
427
- console.log(chalk.green(` • ${answers.reactI18nPath}`));
428
- console.log(chalk.green(` • ${answers.reactProviderPath}`));
429
- console.log(chalk.blue('\nWrap your app with the provider (e.g. Next.js providers.tsx):'));
430
- console.log(
431
- chalk.cyan(
432
- `import { I18nProvider } from '${answers.reactProviderPath.replace(/\\/g, '/').replace(/\.tsx?$/, '')}';\n<I18nProvider>{children}</I18nProvider>`
433
- )
434
- );
435
- } catch (error) {
436
- console.warn(chalk.yellow(`Skipping i18next scaffold: ${(error as Error).message}`));
405
+ if (answers.scaffoldAdapter && answers.scaffoldAdapterPath) {
406
+ try {
407
+ await scaffoldTranslationContext(answers.scaffoldAdapterPath, answers.sourceLanguage, {
408
+ localesDir: answers.localesDir,
409
+ });
410
+ console.log(chalk.green(`Translation context scaffolded at ${answers.scaffoldAdapterPath}`));
411
+ } catch (error) {
412
+ console.warn(chalk.yellow(`Skipping adapter scaffold: ${(error as Error).message}`));
413
+ }
437
414
  }
438
- }
439
415
 
440
- if (answers.adapterPreset === 'react-i18next') {
441
- const pkg = await readPackageJson();
442
- const missingDeps = ['react-i18next', 'i18next'].filter((dep) => !hasDependency(pkg, dep));
443
- if (missingDeps.length) {
444
- console.log(chalk.yellow('\nDependencies missing for react-i18next adapter:'));
445
- missingDeps.forEach((dep) => console.log(chalk.yellow(` • ${dep}`)));
446
- console.log(chalk.blue('Install them with:'));
447
- console.log(chalk.cyan(' pnpm add react-i18next i18next'));
416
+ if (
417
+ answers.adapterPreset === 'react-i18next' &&
418
+ answers.scaffoldReactRuntime &&
419
+ answers.reactI18nPath &&
420
+ answers.reactProviderPath
421
+ ) {
422
+ try {
423
+ await scaffoldI18next(
424
+ answers.reactI18nPath,
425
+ answers.reactProviderPath,
426
+ answers.sourceLanguage,
427
+ answers.localesDir
428
+ );
429
+ console.log(chalk.green('react-i18next runtime scaffolded:'));
430
+ console.log(chalk.green(` • ${answers.reactI18nPath}`));
431
+ console.log(chalk.green(` • ${answers.reactProviderPath}`));
432
+ console.log(chalk.blue('\nWrap your app with the provider (e.g. Next.js providers.tsx):'));
433
+ console.log(
434
+ chalk.cyan(
435
+ `import { I18nProvider } from '${answers.reactProviderPath.replace(/\\/g, '/').replace(/\.tsx?$/, '')}';\n<I18nProvider>{children}</I18nProvider>`
436
+ )
437
+ );
438
+ } catch (error) {
439
+ console.warn(chalk.yellow(`Skipping i18next scaffold: ${(error as Error).message}`));
440
+ }
448
441
  }
449
- }
450
442
 
451
- if (mergeDecision?.strategy) {
452
- console.log(
453
- chalk.blue(
454
- `Merge strategy selected: ${mergeDecision.strategy}. Use this when running i18nsmith sync or diagnose to reconcile locales.`
455
- )
456
- );
443
+ if (answers.adapterPreset === 'react-i18next') {
444
+ const pkg = await readPackageJson();
445
+ const missingDeps = ['react-i18next', 'i18next'].filter((dep) => !hasDependency(pkg, dep));
446
+ if (missingDeps.length) {
447
+ console.log(chalk.yellow('\nDependencies missing for react-i18next adapter:'));
448
+ missingDeps.forEach((dep) => console.log(chalk.yellow(` • ${dep}`)));
449
+ console.log(chalk.blue('Install them with:'));
450
+ console.log(chalk.cyan(' pnpm add react-i18next i18next'));
451
+ }
452
+ }
453
+
454
+ if (mergeDecision?.strategy) {
455
+ console.log(
456
+ chalk.blue(
457
+ `Merge strategy selected: ${mergeDecision.strategy}. Use this when running i18nsmith sync or diagnose to reconcile locales.`
458
+ )
459
+ );
460
+ }
461
+ } catch (error) {
462
+ const message = error instanceof Error ? error.message : String(error);
463
+ throw new CliError(`Failed to write configuration file: ${message}`);
457
464
  }
458
- } catch (error) {
459
- console.error(chalk.red('Failed to write configuration file:'), error);
460
- }
461
- });
465
+ })
466
+ );
462
467
  }
463
468
 
464
469
  type MergeStrategy = 'keep-source' | 'overwrite' | 'interactive';
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { Command } from 'commander';
4
4
  import { detectPackageManager } from '../utils/package-manager.js';
5
+ import { withErrorHandling } from '../utils/errors.js';
5
6
 
6
7
  interface InstallHooksOptions {
7
8
  yes?: boolean;
@@ -42,25 +43,27 @@ export function registerInstallHooks(program: Command) {
42
43
  .option('-y, --yes', 'Skip confirmations')
43
44
  .option('--force', 'Force re-install husky even if present')
44
45
  .option('--skip', 'Skip husky installation (just create hooks)')
45
- .action(async (opts: InstallHooksOptions) => {
46
- const cwd = opts.cwd ?? process.cwd();
47
- const pm = await detectPackageManager();
48
- const hasHusky = await huskyInstalled(cwd);
46
+ .action(
47
+ withErrorHandling(async (opts: InstallHooksOptions) => {
48
+ const cwd = opts.cwd ?? process.cwd();
49
+ const pm = await detectPackageManager();
50
+ const hasHusky = await huskyInstalled(cwd);
49
51
 
50
- if (!hasHusky && !opts.skip) {
51
- await ensureHusky(cwd, pm, !!opts.force);
52
- console.log('Add "prepare": "husky install" to package.json scripts if missing.');
53
- }
52
+ if (!hasHusky && !opts.skip) {
53
+ await ensureHusky(cwd, pm, !!opts.force);
54
+ console.log('Add "prepare": "husky install" to package.json scripts if missing.');
55
+ }
54
56
 
55
- const preCommitContent = `#!/bin/sh\n. \"$(dirname "$0")/_/husky.sh\"\n[ -n \"$I18NSMITH_SKIP_HOOKS\" ] && exit 0\nNOCOLOR=1 npx i18nsmith check --fail-on conflicts || exit 1\n`;
56
- const prePushContent = `#!/bin/sh\n. \"$(dirname "$0")/_/husky.sh\"\n[ -n \"$I18NSMITH_SKIP_HOOKS\" ] && exit 0\nNOCOLOR=1 npx i18nsmith sync --dry-run --check || exit 1\n`;
57
+ const preCommitContent = `#!/bin/sh\n. "$(dirname "$0")/_/husky.sh"\n[ -n "$I18NSMITH_SKIP_HOOKS" ] && exit 0\nNOCOLOR=1 npx i18nsmith check --fail-on conflicts || exit 1\n`;
58
+ const prePushContent = `#!/bin/sh\n. "$(dirname "$0")/_/husky.sh"\n[ -n "$I18NSMITH_SKIP_HOOKS" ] && exit 0\nNOCOLOR=1 npx i18nsmith sync --dry-run --check || exit 1\n`;
57
59
 
58
- await writeHook(cwd, 'pre-commit', preCommitContent);
59
- await writeHook(cwd, 'pre-push', prePushContent);
60
+ await writeHook(cwd, 'pre-commit', preCommitContent);
61
+ await writeHook(cwd, 'pre-push', prePushContent);
60
62
 
61
- console.log('\nHooks added. Set I18NSMITH_SKIP_HOOKS=1 to bypass.');
62
- console.log('Prototype complete – future versions will offer interactive selection & monorepo scoping.');
63
- });
63
+ console.log('\nHooks added. Set I18NSMITH_SKIP_HOOKS=1 to bypass.');
64
+ console.log('Prototype complete – future versions will offer interactive selection & monorepo scoping.');
65
+ })
66
+ );
64
67
 
65
68
  program.addCommand(cmd);
66
69
  }
@@ -8,8 +8,9 @@ import fs from 'fs/promises';
8
8
  import path from 'path';
9
9
  import chalk from 'chalk';
10
10
  import fg from 'fast-glob';
11
- import { loadConfigWithMeta, diagnoseWorkspace, I18nConfig } from '@i18nsmith/core';
11
+ import { loadConfigWithMeta, I18nConfig } from '@i18nsmith/core';
12
12
  import { hasDependency, readPackageJson } from '../utils/pkg.js';
13
+ import { CliError, withErrorHandling } from '../utils/errors.js';
13
14
 
14
15
  interface PreflightResult {
15
16
  passed: boolean;
@@ -36,18 +37,25 @@ export function registerPreflight(program: Command) {
36
37
  .option('-c, --config <path>', 'Path to i18nsmith config file', 'i18n.config.json')
37
38
  .option('--fix', 'Attempt to fix issues automatically', false)
38
39
  .option('--json', 'Output results as JSON', false)
39
- .action(async (options: PreflightOptions) => {
40
- const result = await runPreflightChecks(options);
41
-
42
- if (options.json) {
43
- console.log(JSON.stringify(result, null, 2));
44
- process.exitCode = result.passed ? 0 : 1;
45
- return;
46
- }
47
-
48
- printPreflightResults(result);
49
- process.exitCode = result.passed ? 0 : 1;
50
- });
40
+ .action(
41
+ withErrorHandling(async (options: PreflightOptions) => {
42
+ try {
43
+ const result = await runPreflightChecks(options);
44
+
45
+ if (options.json) {
46
+ console.log(JSON.stringify(result, null, 2));
47
+ process.exitCode = result.passed ? 0 : 1;
48
+ return;
49
+ }
50
+
51
+ printPreflightResults(result);
52
+ process.exitCode = result.passed ? 0 : 1;
53
+ } catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ throw new CliError(`Preflight failed: ${message}`);
56
+ }
57
+ })
58
+ );
51
59
  }
52
60
 
53
61
  async function runPreflightChecks(options: PreflightOptions): Promise<PreflightResult> {
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { promises as fs } from 'node:fs';
5
5
  import { loadConfig, KeyRenamer, type KeyRenameSummary, type KeyRenameBatchSummary, type KeyRenameMapping } from '@i18nsmith/core';
6
6
  import { applyPreviewFile, writePreviewFile } from '../utils/preview.js';
7
+ import { CliError, withErrorHandling } from '../utils/errors.js';
7
8
 
8
9
  interface ScanOptions {
9
10
  config: string;
@@ -40,58 +41,60 @@ export function registerRename(program: Command): void {
40
41
  .option('--diff', 'Display unified diffs for files that would change', false)
41
42
  .option('--preview-output <path>', 'Write preview summary (JSON) to a file (implies dry-run)')
42
43
  .option('--apply-preview <path>', 'Apply a previously saved rename preview JSON file safely')
43
- .action(async (oldKey: string, newKey: string, options: RenameKeyOptions) => {
44
- if (options.applyPreview) {
45
- await applyPreviewFile('rename-key', options.applyPreview);
46
- return;
47
- }
48
-
49
- const previewMode = Boolean(options.previewOutput);
50
- const writeEnabled = Boolean(options.write) && !previewMode;
51
- if (previewMode && options.write) {
52
- console.log(chalk.yellow('Preview requested; ignoring --write and running in dry-run mode.'));
53
- }
54
- options.write = writeEnabled;
55
-
56
- console.log(chalk.blue(writeEnabled ? 'Renaming translation key...' : 'Planning key rename (dry-run)...'));
57
-
58
- try {
59
- const config = await loadConfig(options.config);
60
- const renamer = new KeyRenamer(config);
61
- const summary = await renamer.rename(oldKey, newKey, {
62
- write: options.write,
63
- diff: options.diff || previewMode,
64
- });
65
-
66
- if (previewMode && options.previewOutput) {
67
- const savedPath = await writePreviewFile('rename-key', summary, options.previewOutput);
68
- console.log(chalk.green(`Preview written to ${path.relative(process.cwd(), savedPath)}`));
44
+ .action(
45
+ withErrorHandling(async (oldKey: string, newKey: string, options: RenameKeyOptions) => {
46
+ if (options.applyPreview) {
47
+ await applyPreviewFile('rename-key', options.applyPreview);
69
48
  return;
70
49
  }
71
50
 
72
- if (options.report) {
73
- const outputPath = path.resolve(process.cwd(), options.report);
74
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
75
- await fs.writeFile(outputPath, JSON.stringify(summary, null, 2));
76
- console.log(chalk.green(`Rename report written to ${outputPath}`));
51
+ const previewMode = Boolean(options.previewOutput);
52
+ const writeEnabled = Boolean(options.write) && !previewMode;
53
+ if (previewMode && options.write) {
54
+ console.log(chalk.yellow('Preview requested; ignoring --write and running in dry-run mode.'));
77
55
  }
56
+ options.write = writeEnabled;
57
+
58
+ console.log(chalk.blue(writeEnabled ? 'Renaming translation key...' : 'Planning key rename (dry-run)...'));
59
+
60
+ try {
61
+ const config = await loadConfig(options.config);
62
+ const renamer = new KeyRenamer(config);
63
+ const summary = await renamer.rename(oldKey, newKey, {
64
+ write: options.write,
65
+ diff: options.diff || previewMode,
66
+ });
67
+
68
+ if (previewMode && options.previewOutput) {
69
+ const savedPath = await writePreviewFile('rename-key', summary, options.previewOutput);
70
+ console.log(chalk.green(`Preview written to ${path.relative(process.cwd(), savedPath)}`));
71
+ return;
72
+ }
78
73
 
79
- if (options.json) {
80
- console.log(JSON.stringify(summary, null, 2));
81
- return;
82
- }
74
+ if (options.report) {
75
+ const outputPath = path.resolve(process.cwd(), options.report);
76
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
77
+ await fs.writeFile(outputPath, JSON.stringify(summary, null, 2));
78
+ console.log(chalk.green(`Rename report written to ${outputPath}`));
79
+ }
80
+
81
+ if (options.json) {
82
+ console.log(JSON.stringify(summary, null, 2));
83
+ return;
84
+ }
83
85
 
84
- printRenameSummary(summary);
86
+ printRenameSummary(summary);
85
87
 
86
- if (!options.write) {
87
- console.log(chalk.cyan('\nšŸ“‹ DRY RUN - No files were modified'));
88
- console.log(chalk.yellow('Run again with --write to apply changes.'));
88
+ if (!options.write) {
89
+ console.log(chalk.cyan('\nšŸ“‹ DRY RUN - No files were modified'));
90
+ console.log(chalk.yellow('Run again with --write to apply changes.'));
91
+ }
92
+ } catch (error) {
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ throw new CliError(`Rename failed: ${message}`);
89
95
  }
90
- } catch (error) {
91
- console.error(chalk.red('Rename failed:'), (error as Error).message);
92
- process.exitCode = 1;
93
- }
94
- });
96
+ })
97
+ );
95
98
 
96
99
  program
97
100
  .command('rename-keys')
@@ -102,49 +105,51 @@ export function registerRename(program: Command): void {
102
105
  .option('--report <path>', 'Write JSON summary to a file (for CI or editors)')
103
106
  .option('--write', 'Write changes to disk (defaults to dry-run)', false)
104
107
  .option('--diff', 'Display unified diffs for files that would change', false)
105
- .action(async (options: RenameMapOptions) => {
106
- console.log(
107
- chalk.blue(options.write ? 'Renaming translation keys from map...' : 'Planning batch rename (dry-run)...')
108
- );
109
-
110
- try {
111
- const config = await loadConfig(options.config);
112
- const mappings = await loadRenameMappings(options.map);
113
- const renamer = new KeyRenamer(config);
114
- const summary = await renamer.renameBatch(mappings, { write: options.write, diff: options.diff });
115
-
116
- if (options.report) {
117
- const outputPath = path.resolve(process.cwd(), options.report);
118
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
119
- await fs.writeFile(outputPath, JSON.stringify(summary, null, 2));
120
- console.log(chalk.green(`Batch rename report written to ${outputPath}`));
121
- }
108
+ .action(
109
+ withErrorHandling(async (options: RenameMapOptions) => {
110
+ console.log(
111
+ chalk.blue(options.write ? 'Renaming translation keys from map...' : 'Planning batch rename (dry-run)...')
112
+ );
113
+
114
+ try {
115
+ const config = await loadConfig(options.config);
116
+ const mappings = await loadRenameMappings(options.map);
117
+ const renamer = new KeyRenamer(config);
118
+ const summary = await renamer.renameBatch(mappings, { write: options.write, diff: options.diff });
119
+
120
+ if (options.report) {
121
+ const outputPath = path.resolve(process.cwd(), options.report);
122
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
123
+ await fs.writeFile(outputPath, JSON.stringify(summary, null, 2));
124
+ console.log(chalk.green(`Batch rename report written to ${outputPath}`));
125
+ }
122
126
 
123
- if (options.json) {
124
- console.log(JSON.stringify(summary, null, 2));
125
- return;
126
- }
127
+ if (options.json) {
128
+ console.log(JSON.stringify(summary, null, 2));
129
+ return;
130
+ }
127
131
 
128
- printRenameBatchSummary(summary);
132
+ printRenameBatchSummary(summary);
129
133
 
130
- // Print source file diffs if requested
131
- if (options.diff && summary.diffs.length > 0) {
132
- console.log(chalk.blue('\nSource file changes:'));
133
- for (const diff of summary.diffs) {
134
- console.log(chalk.cyan(`\n--- ${diff.relativePath} (${diff.changes} change${diff.changes === 1 ? '' : 's'}) ---`));
135
- console.log(diff.diff);
134
+ // Print source file diffs if requested
135
+ if (options.diff && summary.diffs.length > 0) {
136
+ console.log(chalk.blue('\nSource file changes:'));
137
+ for (const diff of summary.diffs) {
138
+ console.log(chalk.cyan(`\n--- ${diff.relativePath} (${diff.changes} change${diff.changes === 1 ? '' : 's'}) ---`));
139
+ console.log(diff.diff);
140
+ }
136
141
  }
137
- }
138
142
 
139
- if (!options.write) {
140
- console.log(chalk.cyan('\nšŸ“‹ DRY RUN - No files were modified'));
141
- console.log(chalk.yellow('Run again with --write to apply changes.'));
143
+ if (!options.write) {
144
+ console.log(chalk.cyan('\nšŸ“‹ DRY RUN - No files were modified'));
145
+ console.log(chalk.yellow('Run again with --write to apply changes.'));
146
+ }
147
+ } catch (error) {
148
+ const message = error instanceof Error ? error.message : String(error);
149
+ throw new CliError(`Batch rename failed: ${message}`);
142
150
  }
143
- } catch (error) {
144
- console.error(chalk.red('Batch rename failed:'), (error as Error).message);
145
- process.exitCode = 1;
146
- }
147
- });
151
+ })
152
+ );
148
153
  }
149
154
 
150
155
  function printRenameSummary(summary: KeyRenameSummary) {