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.
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/check.d.ts +25 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/debug-patterns.d.ts.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/install-hooks.d.ts.map +1 -1
- package/dist/commands/preflight.d.ts.map +1 -1
- package/dist/commands/rename.d.ts.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/scaffold-adapter.d.ts.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/sync.d.ts +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/transform.d.ts.map +1 -1
- package/dist/commands/translate/index.d.ts.map +1 -1
- package/dist/index.js +2536 -107783
- package/dist/rename-suspicious.test.d.ts +2 -0
- package/dist/rename-suspicious.test.d.ts.map +1 -0
- package/dist/utils/diff-utils.d.ts +5 -0
- package/dist/utils/diff-utils.d.ts.map +1 -1
- package/dist/utils/errors.d.ts +8 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/locale-audit.d.ts +39 -0
- package/dist/utils/locale-audit.d.ts.map +1 -0
- package/dist/utils/preview.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/commands/audit.ts +18 -209
- package/src/commands/backup.ts +67 -63
- package/src/commands/check.ts +119 -68
- package/src/commands/config.ts +117 -95
- package/src/commands/debug-patterns.ts +25 -22
- package/src/commands/diagnose.ts +29 -26
- package/src/commands/init.ts +84 -79
- package/src/commands/install-hooks.ts +18 -15
- package/src/commands/preflight.ts +21 -13
- package/src/commands/rename.ts +86 -81
- package/src/commands/review.ts +81 -78
- package/src/commands/scaffold-adapter.ts +8 -4
- package/src/commands/scan.ts +61 -58
- package/src/commands/sync.ts +640 -203
- package/src/commands/transform.ts +46 -18
- package/src/commands/translate/index.ts +7 -4
- package/src/e2e.test.ts +34 -14
- package/src/integration.test.ts +86 -0
- package/src/rename-suspicious.test.ts +124 -0
- package/src/utils/diff-utils.ts +6 -0
- package/src/utils/errors.ts +34 -0
- package/src/utils/locale-audit.ts +219 -0
- package/src/utils/preview.test.ts +43 -0
- package/src/utils/preview.ts +2 -8
package/src/commands/init.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
}
|
|
459
|
-
|
|
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(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
+
await writeHook(cwd, 'pre-commit', preCommitContent);
|
|
61
|
+
await writeHook(cwd, 'pre-push', prePushContent);
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
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,
|
|
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(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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> {
|
package/src/commands/rename.ts
CHANGED
|
@@ -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(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
+
printRenameSummary(summary);
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
}
|
|
91
|
-
|
|
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(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
if (options.json) {
|
|
128
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
127
131
|
|
|
128
|
-
|
|
132
|
+
printRenameBatchSummary(summary);
|
|
129
133
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
}
|
|
144
|
-
|
|
145
|
-
process.exitCode = 1;
|
|
146
|
-
}
|
|
147
|
-
});
|
|
151
|
+
})
|
|
152
|
+
);
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
function printRenameSummary(summary: KeyRenameSummary) {
|