i18nsmith 0.1.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.
- package/dist/commands/audit.d.ts +3 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +180 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/backup.d.ts +6 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +85 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +151 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +235 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/debug-patterns.d.ts +3 -0
- package/dist/commands/debug-patterns.d.ts.map +1 -0
- package/dist/commands/debug-patterns.js +192 -0
- package/dist/commands/debug-patterns.js.map +1 -0
- package/dist/commands/debug-patterns.test.d.ts +2 -0
- package/dist/commands/debug-patterns.test.d.ts.map +1 -0
- package/dist/commands/debug-patterns.test.js +109 -0
- package/dist/commands/debug-patterns.test.js.map +1 -0
- package/dist/commands/diagnose.d.ts +3 -0
- package/dist/commands/diagnose.d.ts.map +1 -0
- package/dist/commands/diagnose.js +117 -0
- package/dist/commands/diagnose.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +450 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +2 -0
- package/dist/commands/init.test.d.ts.map +1 -0
- package/dist/commands/init.test.js +74 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/install-hooks.d.ts +3 -0
- package/dist/commands/install-hooks.d.ts.map +1 -0
- package/dist/commands/install-hooks.js +52 -0
- package/dist/commands/install-hooks.js.map +1 -0
- package/dist/commands/preflight.d.ts +7 -0
- package/dist/commands/preflight.d.ts.map +1 -0
- package/dist/commands/preflight.js +417 -0
- package/dist/commands/preflight.js.map +1 -0
- package/dist/commands/preflight.test.d.ts +5 -0
- package/dist/commands/preflight.test.d.ts.map +1 -0
- package/dist/commands/preflight.test.js +108 -0
- package/dist/commands/preflight.test.js.map +1 -0
- package/dist/commands/rename.d.ts +6 -0
- package/dist/commands/rename.d.ts.map +1 -0
- package/dist/commands/rename.js +204 -0
- package/dist/commands/rename.js.map +1 -0
- package/dist/commands/scaffold-adapter.d.ts +3 -0
- package/dist/commands/scaffold-adapter.d.ts.map +1 -0
- package/dist/commands/scaffold-adapter.js +204 -0
- package/dist/commands/scaffold-adapter.js.map +1 -0
- package/dist/commands/scaffold-adapter.test.d.ts +2 -0
- package/dist/commands/scaffold-adapter.test.d.ts.map +1 -0
- package/dist/commands/scaffold-adapter.test.js +102 -0
- package/dist/commands/scaffold-adapter.test.js.map +1 -0
- package/dist/commands/scan.d.ts +3 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +93 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/sync-seed.test.d.ts +2 -0
- package/dist/commands/sync-seed.test.d.ts.map +1 -0
- package/dist/commands/sync-seed.test.js +86 -0
- package/dist/commands/sync-seed.test.js.map +1 -0
- package/dist/commands/sync.d.ts +3 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +590 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/transform.d.ts +3 -0
- package/dist/commands/transform.d.ts.map +1 -0
- package/dist/commands/transform.js +114 -0
- package/dist/commands/transform.js.map +1 -0
- package/dist/commands/translate/csv-handler.d.ts +21 -0
- package/dist/commands/translate/csv-handler.d.ts.map +1 -0
- package/dist/commands/translate/csv-handler.js +270 -0
- package/dist/commands/translate/csv-handler.js.map +1 -0
- package/dist/commands/translate/executor.d.ts +31 -0
- package/dist/commands/translate/executor.d.ts.map +1 -0
- package/dist/commands/translate/executor.js +117 -0
- package/dist/commands/translate/executor.js.map +1 -0
- package/dist/commands/translate/index.d.ts +10 -0
- package/dist/commands/translate/index.d.ts.map +1 -0
- package/dist/commands/translate/index.js +170 -0
- package/dist/commands/translate/index.js.map +1 -0
- package/dist/commands/translate/reporter.d.ts +29 -0
- package/dist/commands/translate/reporter.d.ts.map +1 -0
- package/dist/commands/translate/reporter.js +103 -0
- package/dist/commands/translate/reporter.js.map +1 -0
- package/dist/commands/translate/types.d.ts +50 -0
- package/dist/commands/translate/types.d.ts.map +1 -0
- package/dist/commands/translate/types.js +5 -0
- package/dist/commands/translate/types.js.map +1 -0
- package/dist/commands/translate.d.ts +7 -0
- package/dist/commands/translate.d.ts.map +1 -0
- package/dist/commands/translate.js +7 -0
- package/dist/commands/translate.js.map +1 -0
- package/dist/commands/translate.test.d.ts +2 -0
- package/dist/commands/translate.test.d.ts.map +1 -0
- package/dist/commands/translate.test.js +118 -0
- package/dist/commands/translate.test.js.map +1 -0
- package/dist/e2e.test.d.ts +6 -0
- package/dist/e2e.test.d.ts.map +1 -0
- package/dist/e2e.test.js +376 -0
- package/dist/e2e.test.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.test.d.ts +6 -0
- package/dist/integration.test.d.ts.map +1 -0
- package/dist/integration.test.js +320 -0
- package/dist/integration.test.js.map +1 -0
- package/dist/utils/diagnostics-exit.d.ts +12 -0
- package/dist/utils/diagnostics-exit.d.ts.map +1 -0
- package/dist/utils/diagnostics-exit.js +49 -0
- package/dist/utils/diagnostics-exit.js.map +1 -0
- package/dist/utils/diagnostics-exit.test.d.ts +2 -0
- package/dist/utils/diagnostics-exit.test.d.ts.map +1 -0
- package/dist/utils/diagnostics-exit.test.js +40 -0
- package/dist/utils/diagnostics-exit.test.js.map +1 -0
- package/dist/utils/diff-utils.d.ts +4 -0
- package/dist/utils/diff-utils.d.ts.map +1 -0
- package/dist/utils/diff-utils.js +30 -0
- package/dist/utils/diff-utils.js.map +1 -0
- package/dist/utils/diff-utils.test.d.ts +2 -0
- package/dist/utils/diff-utils.test.d.ts.map +1 -0
- package/dist/utils/diff-utils.test.js +30 -0
- package/dist/utils/diff-utils.test.js.map +1 -0
- package/dist/utils/exit-codes.d.ts +142 -0
- package/dist/utils/exit-codes.d.ts.map +1 -0
- package/dist/utils/exit-codes.js +168 -0
- package/dist/utils/exit-codes.js.map +1 -0
- package/dist/utils/package-manager.d.ts +4 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +40 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/dist/utils/pkg.d.ts +3 -0
- package/dist/utils/pkg.d.ts.map +1 -0
- package/dist/utils/pkg.js +24 -0
- package/dist/utils/pkg.js.map +1 -0
- package/dist/utils/provider-injector.d.ts +36 -0
- package/dist/utils/provider-injector.d.ts.map +1 -0
- package/dist/utils/provider-injector.js +223 -0
- package/dist/utils/provider-injector.js.map +1 -0
- package/dist/utils/provider-injector.test.d.ts +2 -0
- package/dist/utils/provider-injector.test.d.ts.map +1 -0
- package/dist/utils/provider-injector.test.js +67 -0
- package/dist/utils/provider-injector.test.js.map +1 -0
- package/dist/utils/scaffold.d.ts +20 -0
- package/dist/utils/scaffold.d.ts.map +1 -0
- package/dist/utils/scaffold.js +197 -0
- package/dist/utils/scaffold.js.map +1 -0
- package/package.json +35 -0
- package/src/commands/audit.ts +234 -0
- package/src/commands/backup.ts +96 -0
- package/src/commands/check.ts +191 -0
- package/src/commands/config.ts +263 -0
- package/src/commands/debug-patterns.test.ts +134 -0
- package/src/commands/debug-patterns.ts +257 -0
- package/src/commands/diagnose.ts +136 -0
- package/src/commands/init.test.ts +82 -0
- package/src/commands/init.ts +536 -0
- package/src/commands/install-hooks.ts +66 -0
- package/src/commands/preflight.test.ts +139 -0
- package/src/commands/preflight.ts +488 -0
- package/src/commands/rename.ts +264 -0
- package/src/commands/scaffold-adapter.test.ts +110 -0
- package/src/commands/scaffold-adapter.ts +250 -0
- package/src/commands/scan.ts +125 -0
- package/src/commands/sync-seed.test.ts +116 -0
- package/src/commands/sync.ts +736 -0
- package/src/commands/transform.ts +151 -0
- package/src/commands/translate/README.md +75 -0
- package/src/commands/translate/csv-handler.ts +301 -0
- package/src/commands/translate/executor.ts +188 -0
- package/src/commands/translate/index.ts +220 -0
- package/src/commands/translate/reporter.ts +138 -0
- package/src/commands/translate/types.ts +56 -0
- package/src/commands/translate.test.ts +173 -0
- package/src/commands/translate.ts +6 -0
- package/src/e2e.test.ts +479 -0
- package/src/fixtures/README.md +61 -0
- package/src/fixtures/basic-react/i18n.config.json +15 -0
- package/src/fixtures/basic-react/locales/de.json +8 -0
- package/src/fixtures/basic-react/locales/en.json +8 -0
- package/src/fixtures/basic-react/locales/fr.json +8 -0
- package/src/fixtures/basic-react/src/App.tsx +15 -0
- package/src/fixtures/basic-react/src/Messages.tsx +12 -0
- package/src/fixtures/nested-locales/i18n.config.json +9 -0
- package/src/fixtures/nested-locales/locales/en.json +23 -0
- package/src/fixtures/nested-locales/locales/fr.json +23 -0
- package/src/fixtures/nested-locales/src/HomePage.tsx +13 -0
- package/src/fixtures/suspicious-keys/i18n.config.json +9 -0
- package/src/fixtures/suspicious-keys/locales/en.json +11 -0
- package/src/fixtures/suspicious-keys/locales/fr.json +11 -0
- package/src/fixtures/suspicious-keys/src/BadKeys.tsx +19 -0
- package/src/index.ts +43 -0
- package/src/integration.test.ts +438 -0
- package/src/utils/diagnostics-exit.test.ts +47 -0
- package/src/utils/diagnostics-exit.ts +63 -0
- package/src/utils/diff-utils.test.ts +36 -0
- package/src/utils/diff-utils.ts +42 -0
- package/src/utils/exit-codes.ts +201 -0
- package/src/utils/package-manager.ts +44 -0
- package/src/utils/pkg.ts +23 -0
- package/src/utils/provider-injector.test.ts +79 -0
- package/src/utils/provider-injector.ts +315 -0
- package/src/utils/scaffold.ts +240 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import inquirer, { type CheckboxQuestion } from 'inquirer';
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import {
|
|
7
|
+
Syncer,
|
|
8
|
+
KeyRenamer,
|
|
9
|
+
LocaleStore,
|
|
10
|
+
loadConfig,
|
|
11
|
+
loadConfigWithMeta,
|
|
12
|
+
generateRenameProposals,
|
|
13
|
+
createRenameMappingFile,
|
|
14
|
+
type SyncSummary,
|
|
15
|
+
type KeyRenameBatchSummary,
|
|
16
|
+
} from '@i18nsmith/core';
|
|
17
|
+
import {
|
|
18
|
+
printLocaleDiffs,
|
|
19
|
+
writeLocaleDiffPatches,
|
|
20
|
+
} from '../utils/diff-utils.js';
|
|
21
|
+
import { SYNC_EXIT_CODES } from '../utils/exit-codes.js';
|
|
22
|
+
|
|
23
|
+
interface SyncCommandOptions {
|
|
24
|
+
config?: string;
|
|
25
|
+
json?: boolean;
|
|
26
|
+
target?: string[];
|
|
27
|
+
report?: string;
|
|
28
|
+
listFiles?: boolean;
|
|
29
|
+
include?: string[];
|
|
30
|
+
exclude?: string[];
|
|
31
|
+
write?: boolean;
|
|
32
|
+
prune?: boolean;
|
|
33
|
+
backup?: boolean;
|
|
34
|
+
yes?: boolean;
|
|
35
|
+
check?: boolean;
|
|
36
|
+
strict?: boolean;
|
|
37
|
+
validateInterpolations?: boolean;
|
|
38
|
+
emptyValues?: boolean;
|
|
39
|
+
assume?: string[];
|
|
40
|
+
assumeGlobs?: string[];
|
|
41
|
+
interactive?: boolean;
|
|
42
|
+
diff?: boolean;
|
|
43
|
+
patchDir?: string;
|
|
44
|
+
invalidateCache?: boolean;
|
|
45
|
+
autoRenameSuspicious?: boolean;
|
|
46
|
+
renameMapFile?: string;
|
|
47
|
+
namingConvention?: 'kebab-case' | 'camelCase' | 'snake_case';
|
|
48
|
+
rewriteShape?: 'flat' | 'nested';
|
|
49
|
+
shapeDelimiter?: string;
|
|
50
|
+
seedTargetLocales?: boolean;
|
|
51
|
+
seedValue?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectAssumedKeys(value: string, previous: string[] = []) {
|
|
55
|
+
return previous.concat(value.split(',').map((k) => k.trim()));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function collectTargetPatterns(value: string, previous: string[] = []) {
|
|
59
|
+
return previous.concat(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function registerSync(program: Command) {
|
|
63
|
+
program
|
|
64
|
+
.command('sync')
|
|
65
|
+
.description('Detect missing locale keys and optionally prune unused entries')
|
|
66
|
+
.option('-c, --config <path>', 'Path to i18nsmith config file', 'i18n.config.json')
|
|
67
|
+
.option('--json', 'Print raw JSON results', false)
|
|
68
|
+
.option('--report <path>', 'Write JSON summary to a file (for CI or editors)')
|
|
69
|
+
.option('--write', 'Write changes to disk (defaults to dry-run)', false)
|
|
70
|
+
.option('--prune', 'Remove unused keys from locale files (requires --write)', false)
|
|
71
|
+
.option('--no-backup', 'Disable automatic backup when using --prune (backup is on by default with --prune)')
|
|
72
|
+
.option('-y, --yes', 'Skip confirmation prompts (for CI)', false)
|
|
73
|
+
.option('--check', 'Exit with error code if drift detected', false)
|
|
74
|
+
.option('--strict', 'Exit with error code if any suspicious patterns detected (CI mode)', false)
|
|
75
|
+
.option('--validate-interpolations', 'Validate interpolation placeholders across locales', false)
|
|
76
|
+
.option('--no-empty-values', 'Treat empty or placeholder locale values as failures')
|
|
77
|
+
.option('--assume <keys...>', 'List of runtime keys to assume present (comma-separated)', collectAssumedKeys, [])
|
|
78
|
+
.option('--assume-globs <patterns...>', 'Glob patterns for dynamic key namespaces (e.g., errors.*, navigation.**)', collectTargetPatterns, [])
|
|
79
|
+
.option('--interactive', 'Interactively approve locale mutations before writing', false)
|
|
80
|
+
.option('--diff', 'Display unified diffs for locale files that would change', false)
|
|
81
|
+
.option('--patch-dir <path>', 'Write locale diffs to .patch files in the specified directory')
|
|
82
|
+
.option('--invalidate-cache', 'Ignore cached sync analysis and rescan all source files', false)
|
|
83
|
+
.option('--target <pattern...>', 'Limit translation reference scanning to specific files or glob patterns', collectTargetPatterns, [])
|
|
84
|
+
.option('--include <patterns...>', 'Override include globs from config (comma or space separated)', collectTargetPatterns, [])
|
|
85
|
+
.option('--exclude <patterns...>', 'Override exclude globs from config (comma or space separated)', collectTargetPatterns, [])
|
|
86
|
+
.option('--auto-rename-suspicious', 'Propose normalized names for suspicious keys', false)
|
|
87
|
+
.option('--rename-map-file <path>', 'Write rename proposals to a mapping file (JSON or commented format)')
|
|
88
|
+
.option('--naming-convention <convention>', 'Naming convention for auto-rename (kebab-case, camelCase, snake_case)', 'kebab-case')
|
|
89
|
+
.option('--rewrite-shape <format>', 'Rewrite all locale files to flat or nested format')
|
|
90
|
+
.option('--shape-delimiter <char>', 'Delimiter for key nesting (default: ".")', '.')
|
|
91
|
+
.option('--seed-target-locales', 'Add missing keys to target locale files with empty or placeholder values', false)
|
|
92
|
+
.option('--seed-value <value>', 'Value to use when seeding target locales (default: empty string)', '')
|
|
93
|
+
.action(async (options: SyncCommandOptions) => {
|
|
94
|
+
const interactive = Boolean(options.interactive);
|
|
95
|
+
const diffEnabled = Boolean(options.diff || options.patchDir);
|
|
96
|
+
const invalidateCache = Boolean(options.invalidateCache);
|
|
97
|
+
const diffRequested = diffEnabled || Boolean(options.json);
|
|
98
|
+
if (interactive && options.json) {
|
|
99
|
+
console.error(chalk.red('--interactive cannot be combined with --json output.'));
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(
|
|
105
|
+
chalk.blue(
|
|
106
|
+
interactive
|
|
107
|
+
? 'Interactive sync (dry-run first)...'
|
|
108
|
+
: options.write
|
|
109
|
+
? 'Syncing locale files...'
|
|
110
|
+
: 'Checking locale drift...'
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
|
|
116
|
+
|
|
117
|
+
// Inform user if config was found in a parent directory
|
|
118
|
+
const cwd = process.cwd();
|
|
119
|
+
if (projectRoot !== cwd) {
|
|
120
|
+
console.log(chalk.gray(`Config found at ${path.relative(cwd, configPath)}`));
|
|
121
|
+
console.log(chalk.gray(`Using project root: ${projectRoot}\n`));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (options.include?.length) {
|
|
125
|
+
config.include = options.include;
|
|
126
|
+
}
|
|
127
|
+
if (options.exclude?.length) {
|
|
128
|
+
config.exclude = options.exclude;
|
|
129
|
+
}
|
|
130
|
+
// Merge --assume-globs with config
|
|
131
|
+
if (options.assumeGlobs?.length) {
|
|
132
|
+
config.sync = config.sync ?? {};
|
|
133
|
+
config.sync.dynamicKeyGlobs = [
|
|
134
|
+
...(config.sync.dynamicKeyGlobs ?? []),
|
|
135
|
+
...options.assumeGlobs,
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
// Apply --seed-target-locales and --seed-value flags
|
|
139
|
+
if (options.seedTargetLocales) {
|
|
140
|
+
config.seedTargetLocales = true;
|
|
141
|
+
}
|
|
142
|
+
if (options.seedValue !== undefined && options.seedValue !== '') {
|
|
143
|
+
config.sync = config.sync ?? {};
|
|
144
|
+
config.sync.seedValue = options.seedValue;
|
|
145
|
+
}
|
|
146
|
+
const syncer = new Syncer(config, { workspaceRoot: projectRoot });
|
|
147
|
+
if (interactive) {
|
|
148
|
+
await runInteractiveSync(syncer, { ...options, diff: diffEnabled, invalidateCache });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// If writing with prune, first do a dry-run to check scope
|
|
153
|
+
const PRUNE_CONFIRMATION_THRESHOLD = 10;
|
|
154
|
+
let confirmedPrune = options.prune;
|
|
155
|
+
|
|
156
|
+
if (options.write && options.prune && !options.yes) {
|
|
157
|
+
// Quick dry-run to see how many keys would be pruned
|
|
158
|
+
const dryRunSummary = await syncer.run({
|
|
159
|
+
write: false,
|
|
160
|
+
prune: true,
|
|
161
|
+
validateInterpolations: options.validateInterpolations,
|
|
162
|
+
emptyValuePolicy: options.emptyValues === false ? 'fail' : undefined,
|
|
163
|
+
assumedKeys: options.assume,
|
|
164
|
+
diff: false,
|
|
165
|
+
invalidateCache,
|
|
166
|
+
targets: options.target,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (dryRunSummary.unusedKeys.length >= PRUNE_CONFIRMATION_THRESHOLD) {
|
|
170
|
+
console.log(chalk.yellow(`\n⚠️ About to remove ${dryRunSummary.unusedKeys.length} unused key(s) from locale files.\n`));
|
|
171
|
+
|
|
172
|
+
// Show sample of keys to be removed
|
|
173
|
+
const sampleKeys = dryRunSummary.unusedKeys.slice(0, 10).map(k => k.key);
|
|
174
|
+
for (const key of sampleKeys) {
|
|
175
|
+
console.log(chalk.gray(` - ${key}`));
|
|
176
|
+
}
|
|
177
|
+
if (dryRunSummary.unusedKeys.length > 10) {
|
|
178
|
+
console.log(chalk.gray(` ... and ${dryRunSummary.unusedKeys.length - 10} more`));
|
|
179
|
+
}
|
|
180
|
+
console.log('');
|
|
181
|
+
|
|
182
|
+
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
|
|
183
|
+
{
|
|
184
|
+
type: 'confirm',
|
|
185
|
+
name: 'confirmed',
|
|
186
|
+
message: `Remove these ${dryRunSummary.unusedKeys.length} unused keys?`,
|
|
187
|
+
default: false,
|
|
188
|
+
},
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
if (!confirmed) {
|
|
192
|
+
console.log(chalk.yellow('Prune cancelled. Running with --write only (add missing keys).'));
|
|
193
|
+
confirmedPrune = false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const summary = await syncer.run({
|
|
199
|
+
write: options.write,
|
|
200
|
+
prune: confirmedPrune,
|
|
201
|
+
backup: options.backup,
|
|
202
|
+
validateInterpolations: options.validateInterpolations,
|
|
203
|
+
emptyValuePolicy: options.emptyValues === false ? 'fail' : undefined,
|
|
204
|
+
assumedKeys: options.assume,
|
|
205
|
+
diff: diffRequested,
|
|
206
|
+
invalidateCache,
|
|
207
|
+
targets: options.target,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Show backup info if created
|
|
211
|
+
if (summary.backup) {
|
|
212
|
+
console.log(chalk.blue(`\n📦 ${summary.backup.summary}`));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (options.report) {
|
|
216
|
+
const outputPath = path.resolve(process.cwd(), options.report);
|
|
217
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
218
|
+
await fs.writeFile(outputPath, JSON.stringify(summary, null, 2));
|
|
219
|
+
console.log(chalk.green(`Sync report written to ${outputPath}`));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (options.json) {
|
|
223
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
printSyncSummary(summary);
|
|
228
|
+
if (diffEnabled) {
|
|
229
|
+
printLocaleDiffs(summary.diffs);
|
|
230
|
+
}
|
|
231
|
+
if (options.patchDir) {
|
|
232
|
+
await writeLocaleDiffPatches(summary.diffs, options.patchDir);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Handle --auto-rename-suspicious
|
|
236
|
+
if (options.autoRenameSuspicious && summary.suspiciousKeys.length > 0) {
|
|
237
|
+
await handleAutoRenameSuspicious(summary, options, config, projectRoot);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Handle --rewrite-shape
|
|
241
|
+
if (options.rewriteShape && (options.rewriteShape === 'flat' || options.rewriteShape === 'nested')) {
|
|
242
|
+
await handleRewriteShape(options, config);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const shouldFailPlaceholders = summary.validation.interpolations && summary.placeholderIssues.length > 0;
|
|
246
|
+
const shouldFailEmptyValues =
|
|
247
|
+
summary.validation.emptyValuePolicy === 'fail' && summary.emptyValueViolations.length > 0;
|
|
248
|
+
|
|
249
|
+
// --strict mode: fail on any suspicious patterns
|
|
250
|
+
if (options.strict) {
|
|
251
|
+
const hasSuspiciousKeys = summary.suspiciousKeys.length > 0;
|
|
252
|
+
const hasDrift = summary.missingKeys.length > 0 || summary.unusedKeys.length > 0;
|
|
253
|
+
|
|
254
|
+
if (hasSuspiciousKeys) {
|
|
255
|
+
console.error(chalk.red('\n⚠️ Suspicious patterns detected (--strict mode):'));
|
|
256
|
+
const grouped = new Map<string, string[]>();
|
|
257
|
+
for (const warning of summary.suspiciousKeys.slice(0, 20)) {
|
|
258
|
+
const reason = warning.reason;
|
|
259
|
+
if (!grouped.has(reason)) {
|
|
260
|
+
grouped.set(reason, []);
|
|
261
|
+
}
|
|
262
|
+
grouped.get(reason)!.push(warning.key);
|
|
263
|
+
}
|
|
264
|
+
for (const [reason, keys] of grouped) {
|
|
265
|
+
console.error(chalk.yellow(` ${reason}:`));
|
|
266
|
+
keys.slice(0, 5).forEach((key) => console.error(` • ${key}`));
|
|
267
|
+
if (keys.length > 5) {
|
|
268
|
+
console.error(chalk.gray(` ...and ${keys.length - 5} more.`));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (summary.suspiciousKeys.length > 20) {
|
|
272
|
+
console.error(chalk.gray(` ...and ${summary.suspiciousKeys.length - 20} more warnings.`));
|
|
273
|
+
}
|
|
274
|
+
process.exitCode = SYNC_EXIT_CODES.SUSPICIOUS_KEYS;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (shouldFailPlaceholders) {
|
|
279
|
+
console.error(chalk.red('\nPlaceholder mismatches detected (--strict mode).'));
|
|
280
|
+
process.exitCode = SYNC_EXIT_CODES.PLACEHOLDER_MISMATCH;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (shouldFailEmptyValues) {
|
|
285
|
+
console.error(chalk.red('\nEmpty locale values detected (--strict mode).'));
|
|
286
|
+
process.exitCode = SYNC_EXIT_CODES.EMPTY_VALUES;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (hasDrift) {
|
|
291
|
+
console.error(chalk.red('\nDrift detected (--strict mode). Run with --write to fix.'));
|
|
292
|
+
process.exitCode = SYNC_EXIT_CODES.DRIFT;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log(chalk.green('\n✓ No issues detected (--strict mode passed).'));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (options.check) {
|
|
301
|
+
const hasDrift = summary.missingKeys.length || summary.unusedKeys.length;
|
|
302
|
+
if (shouldFailPlaceholders) {
|
|
303
|
+
console.error(chalk.red('\nPlaceholder mismatches detected. Run with --write to fix.'));
|
|
304
|
+
process.exitCode = SYNC_EXIT_CODES.PLACEHOLDER_MISMATCH;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (shouldFailEmptyValues) {
|
|
308
|
+
console.error(chalk.red('\nEmpty locale values detected. Run with --write to fix.'));
|
|
309
|
+
process.exitCode = SYNC_EXIT_CODES.EMPTY_VALUES;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (hasDrift) {
|
|
313
|
+
console.error(chalk.red('\nDrift detected. Run with --write to fix.'));
|
|
314
|
+
process.exitCode = SYNC_EXIT_CODES.DRIFT;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!options.write) {
|
|
320
|
+
// Show prominent dry-run indicator
|
|
321
|
+
console.log(chalk.cyan('\n📋 DRY RUN - No files were modified'));
|
|
322
|
+
if (summary.missingKeys.length && summary.unusedKeys.length) {
|
|
323
|
+
console.log(chalk.yellow('Run again with --write to add missing keys.'));
|
|
324
|
+
console.log(chalk.yellow('Run with --write --prune to also remove unused keys.'));
|
|
325
|
+
} else if (summary.missingKeys.length) {
|
|
326
|
+
console.log(chalk.yellow('Run again with --write to add missing keys.'));
|
|
327
|
+
} else if (summary.unusedKeys.length) {
|
|
328
|
+
console.log(chalk.yellow('Unused keys found. Run with --write --prune to remove them.'));
|
|
329
|
+
}
|
|
330
|
+
} else if (options.write && !options.prune && summary.unusedKeys.length) {
|
|
331
|
+
console.log(chalk.gray(`\n Note: ${summary.unusedKeys.length} unused key(s) were not removed. Use --prune to remove them.`));
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.error(chalk.red('Sync failed:'), (error as Error).message);
|
|
335
|
+
process.exitCode = 1;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function printSyncSummary(summary: SyncSummary) {
|
|
341
|
+
console.log(
|
|
342
|
+
chalk.green(
|
|
343
|
+
`Scanned ${summary.filesScanned} file${summary.filesScanned === 1 ? '' : 's'}; ` +
|
|
344
|
+
`${summary.references.length} translation reference${summary.references.length === 1 ? '' : 's'} found.`
|
|
345
|
+
)
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (summary.missingKeys.length) {
|
|
349
|
+
console.log(chalk.red('Missing keys:'));
|
|
350
|
+
summary.missingKeys.slice(0, 50).forEach((item) => {
|
|
351
|
+
const sample = item.references[0];
|
|
352
|
+
const location = sample ? `${sample.filePath}:${sample.position.line}` : 'n/a';
|
|
353
|
+
console.log(` • ${item.key} (${item.references.length} reference${item.references.length === 1 ? '' : 's'} — e.g., ${location})`);
|
|
354
|
+
});
|
|
355
|
+
if (summary.missingKeys.length > 50) {
|
|
356
|
+
console.log(chalk.gray(` ...and ${summary.missingKeys.length - 50} more.`));
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
console.log(chalk.green('No missing keys detected.'));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (summary.unusedKeys.length) {
|
|
363
|
+
console.log(chalk.yellow('Unused locale keys:'));
|
|
364
|
+
summary.unusedKeys.slice(0, 50).forEach((item) => {
|
|
365
|
+
console.log(` • ${item.key} (${item.locales.join(', ')})`);
|
|
366
|
+
});
|
|
367
|
+
if (summary.unusedKeys.length > 50) {
|
|
368
|
+
console.log(chalk.gray(` ...and ${summary.unusedKeys.length - 50} more.`));
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
console.log(chalk.green('No unused locale keys detected.'));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (summary.validation.interpolations) {
|
|
375
|
+
if (summary.placeholderIssues.length) {
|
|
376
|
+
console.log(chalk.yellow('Placeholder mismatches:'));
|
|
377
|
+
summary.placeholderIssues.slice(0, 50).forEach((issue) => {
|
|
378
|
+
const missing = issue.missing.length ? `missing [${issue.missing.join(', ')}]` : '';
|
|
379
|
+
const extra = issue.extra.length ? `extra [${issue.extra.join(', ')}]` : '';
|
|
380
|
+
const detail = [missing, extra].filter(Boolean).join('; ');
|
|
381
|
+
console.log(` • ${issue.key} (${issue.locale}) ${detail}`);
|
|
382
|
+
});
|
|
383
|
+
if (summary.placeholderIssues.length > 50) {
|
|
384
|
+
console.log(chalk.gray(` ...and ${summary.placeholderIssues.length - 50} more.`));
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
console.log(chalk.green('No placeholder mismatches detected.'));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (summary.validation.emptyValuePolicy !== 'ignore') {
|
|
392
|
+
if (summary.emptyValueViolations.length) {
|
|
393
|
+
const label =
|
|
394
|
+
summary.validation.emptyValuePolicy === 'fail'
|
|
395
|
+
? chalk.red('Empty locale values:')
|
|
396
|
+
: chalk.yellow('Empty locale values:');
|
|
397
|
+
console.log(label);
|
|
398
|
+
summary.emptyValueViolations.slice(0, 50).forEach((violation) => {
|
|
399
|
+
console.log(` • ${violation.key} (${violation.locale}) — ${violation.reason}`);
|
|
400
|
+
});
|
|
401
|
+
if (summary.emptyValueViolations.length > 50) {
|
|
402
|
+
console.log(chalk.gray(` ...and ${summary.emptyValueViolations.length - 50} more.`));
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
console.log(chalk.green('No empty locale values detected.'));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (summary.dynamicKeyWarnings.length) {
|
|
410
|
+
console.log(chalk.yellow('Dynamic translation keys detected:'));
|
|
411
|
+
summary.dynamicKeyWarnings.slice(0, 50).forEach((warning) => {
|
|
412
|
+
console.log(
|
|
413
|
+
` • ${warning.filePath}:${warning.position.line} (${warning.reason}) ${chalk.gray(warning.expression)}`
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
if (summary.dynamicKeyWarnings.length > 50) {
|
|
417
|
+
console.log(chalk.gray(` ...and ${summary.dynamicKeyWarnings.length - 50} more.`));
|
|
418
|
+
}
|
|
419
|
+
if (summary.assumedKeys.length) {
|
|
420
|
+
console.log(chalk.blue(`Assumed runtime keys: ${summary.assumedKeys.join(', ')}`));
|
|
421
|
+
} else {
|
|
422
|
+
console.log(
|
|
423
|
+
chalk.gray(
|
|
424
|
+
'Use --assume key1,key2 to prevent false positives for known runtime-only translation keys.'
|
|
425
|
+
)
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (summary.localeStats.length) {
|
|
431
|
+
console.log(chalk.blue('Locale file changes:'));
|
|
432
|
+
summary.localeStats.forEach((stat) => {
|
|
433
|
+
console.log(
|
|
434
|
+
` • ${stat.locale}: ${stat.added.length} added, ${stat.updated.length} updated, ${stat.removed.length} removed (total ${stat.totalKeys})`
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!summary.write && summary.localePreview.length) {
|
|
440
|
+
console.log(chalk.blue('Locale diff preview:'));
|
|
441
|
+
summary.localePreview.forEach((stat) => {
|
|
442
|
+
console.log(
|
|
443
|
+
` • ${stat.locale}: ${stat.add.length} to add, ${stat.remove.length} to remove`
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function handleAutoRenameSuspicious(
|
|
450
|
+
summary: SyncSummary,
|
|
451
|
+
options: SyncCommandOptions,
|
|
452
|
+
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
453
|
+
projectRoot: string
|
|
454
|
+
) {
|
|
455
|
+
console.log(chalk.blue('\n📝 Auto-rename suspicious keys analysis:'));
|
|
456
|
+
|
|
457
|
+
// Get existing keys from locale data to check for conflicts
|
|
458
|
+
const localesDir = path.resolve(process.cwd(), config.localesDir ?? 'locales');
|
|
459
|
+
const localeStore = new LocaleStore(localesDir, {
|
|
460
|
+
sortKeys: config.locales?.sortKeys ?? 'alphabetical',
|
|
461
|
+
});
|
|
462
|
+
const sourceLocale = config.sourceLanguage ?? 'en';
|
|
463
|
+
const sourceData = await localeStore.get(sourceLocale);
|
|
464
|
+
const existingKeys = new Set(Object.keys(sourceData));
|
|
465
|
+
|
|
466
|
+
// Generate rename proposals
|
|
467
|
+
const namingConvention = options.namingConvention ?? 'kebab-case';
|
|
468
|
+
const report = generateRenameProposals(summary.suspiciousKeys, {
|
|
469
|
+
existingKeys,
|
|
470
|
+
namingConvention,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Print summary
|
|
474
|
+
console.log(` Found ${report.totalSuspicious} suspicious key(s)`);
|
|
475
|
+
|
|
476
|
+
if (report.safeProposals.length > 0) {
|
|
477
|
+
console.log(chalk.green(`\n ✓ Safe rename proposals (${report.safeProposals.length}):`));
|
|
478
|
+
const toShow = report.safeProposals.slice(0, 10);
|
|
479
|
+
for (const proposal of toShow) {
|
|
480
|
+
console.log(chalk.gray(` "${proposal.originalKey}" → "${proposal.proposedKey}"`));
|
|
481
|
+
console.log(chalk.gray(` (${proposal.reason}) in ${proposal.filePath}:${proposal.position.line}`));
|
|
482
|
+
}
|
|
483
|
+
if (report.safeProposals.length > 10) {
|
|
484
|
+
console.log(chalk.gray(` ...and ${report.safeProposals.length - 10} more`));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (report.conflictProposals.length > 0) {
|
|
489
|
+
console.log(chalk.yellow(`\n ⚠️ Conflicting proposals (${report.conflictProposals.length}):`));
|
|
490
|
+
const toShow = report.conflictProposals.slice(0, 5);
|
|
491
|
+
for (const proposal of toShow) {
|
|
492
|
+
console.log(chalk.yellow(` "${proposal.originalKey}" → "${proposal.proposedKey}"`));
|
|
493
|
+
console.log(chalk.gray(` Conflicts with: ${proposal.conflictsWith}`));
|
|
494
|
+
}
|
|
495
|
+
if (report.conflictProposals.length > 5) {
|
|
496
|
+
console.log(chalk.gray(` ...and ${report.conflictProposals.length - 5} more`));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (report.skippedKeys.length > 0) {
|
|
501
|
+
console.log(chalk.gray(`\n Skipped ${report.skippedKeys.length} key(s) (already normalized or no change needed)`));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Write mapping file if requested
|
|
505
|
+
const hasMappings = Object.keys(report.renameMapping).length > 0;
|
|
506
|
+
|
|
507
|
+
if (options.renameMapFile && hasMappings) {
|
|
508
|
+
const outputPath = path.resolve(process.cwd(), options.renameMapFile);
|
|
509
|
+
const isJsonFormat = outputPath.endsWith('.json');
|
|
510
|
+
const content = createRenameMappingFile(report.renameMapping, {
|
|
511
|
+
includeComments: !isJsonFormat,
|
|
512
|
+
});
|
|
513
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
514
|
+
await fs.writeFile(outputPath, content, 'utf8');
|
|
515
|
+
console.log(chalk.green(`\n ✓ Rename mapping written to ${outputPath}`));
|
|
516
|
+
console.log(chalk.gray(' Apply with: npx i18nsmith rename-keys --map ' + options.renameMapFile + ' --write'));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (options.write) {
|
|
520
|
+
if (report.safeProposals.length === 0) {
|
|
521
|
+
console.log(chalk.yellow('\n No safe rename proposals to apply.'));
|
|
522
|
+
} else {
|
|
523
|
+
console.log(chalk.blue('\n✍️ Applying safe rename proposals (source + locales)...'));
|
|
524
|
+
const mappings = report.safeProposals.map((proposal) => ({
|
|
525
|
+
from: proposal.originalKey,
|
|
526
|
+
to: proposal.proposedKey,
|
|
527
|
+
}));
|
|
528
|
+
|
|
529
|
+
const renamer = new KeyRenamer(config, { workspaceRoot: projectRoot });
|
|
530
|
+
const applySummary = await renamer.renameBatch(mappings, { write: true, diff: Boolean(options.diff) });
|
|
531
|
+
printRenameBatchSummary(applySummary);
|
|
532
|
+
|
|
533
|
+
if (!options.renameMapFile && hasMappings) {
|
|
534
|
+
const defaultMapPath = path.resolve(projectRoot, '.i18nsmith', 'auto-rename-map.json');
|
|
535
|
+
await fs.mkdir(path.dirname(defaultMapPath), { recursive: true });
|
|
536
|
+
await fs.writeFile(defaultMapPath, JSON.stringify(report.renameMapping, null, 2));
|
|
537
|
+
console.log(
|
|
538
|
+
chalk.gray(
|
|
539
|
+
`\n Saved rename mapping to ${path.relative(process.cwd(), defaultMapPath)} (set --rename-map-file to customize)`
|
|
540
|
+
)
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} else if (hasMappings && !options.renameMapFile) {
|
|
545
|
+
console.log(chalk.gray('\n Use --rename-map-file <path> to export mappings for later application.'));
|
|
546
|
+
console.log(chalk.gray(' Run with --write to apply safe proposals automatically.'));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function handleRewriteShape(
|
|
551
|
+
options: SyncCommandOptions,
|
|
552
|
+
config: Awaited<ReturnType<typeof loadConfig>>
|
|
553
|
+
) {
|
|
554
|
+
const targetFormat = options.rewriteShape as 'flat' | 'nested';
|
|
555
|
+
const delimiter = options.shapeDelimiter ?? '.';
|
|
556
|
+
|
|
557
|
+
console.log(chalk.blue(`\n🔄 Rewriting locale files to ${targetFormat} format...`));
|
|
558
|
+
|
|
559
|
+
const localesDir = path.resolve(process.cwd(), config.localesDir ?? 'locales');
|
|
560
|
+
const localeStore = new LocaleStore(localesDir, {
|
|
561
|
+
delimiter,
|
|
562
|
+
sortKeys: config.locales?.sortKeys ?? 'alphabetical',
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Load all configured locales
|
|
566
|
+
const sourceLocale = config.sourceLanguage ?? 'en';
|
|
567
|
+
const targetLocales = config.targetLanguages ?? [];
|
|
568
|
+
const allLocales = [sourceLocale, ...targetLocales];
|
|
569
|
+
|
|
570
|
+
for (const locale of allLocales) {
|
|
571
|
+
await localeStore.get(locale); // Load into cache
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Rewrite all locales to the target format
|
|
575
|
+
const stats = await localeStore.rewriteShape(targetFormat, { delimiter });
|
|
576
|
+
|
|
577
|
+
if (stats.length === 0) {
|
|
578
|
+
console.log(chalk.yellow(' No locale files found to rewrite.'));
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
console.log(chalk.green(` ✓ Rewrote ${stats.length} locale file(s) to ${targetFormat} format:`));
|
|
583
|
+
for (const stat of stats) {
|
|
584
|
+
console.log(chalk.gray(` • ${stat.locale}: ${stat.totalKeys} keys`));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function runInteractiveSync(syncer: Syncer, options: SyncCommandOptions) {
|
|
589
|
+
const diffEnabled = Boolean(options.diff || options.patchDir);
|
|
590
|
+
const invalidateCache = Boolean(options.invalidateCache);
|
|
591
|
+
const baseline = await syncer.run({
|
|
592
|
+
write: false,
|
|
593
|
+
validateInterpolations: options.validateInterpolations,
|
|
594
|
+
emptyValuePolicy: options.emptyValues === false ? 'fail' : undefined,
|
|
595
|
+
assumedKeys: options.assume,
|
|
596
|
+
diff: diffEnabled,
|
|
597
|
+
invalidateCache,
|
|
598
|
+
targets: options.target,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
printSyncSummary(baseline);
|
|
602
|
+
if (diffEnabled) {
|
|
603
|
+
printLocaleDiffs(baseline.diffs);
|
|
604
|
+
}
|
|
605
|
+
if (options.patchDir) {
|
|
606
|
+
await writeLocaleDiffPatches(baseline.diffs, options.patchDir);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (!baseline.missingKeys.length && !baseline.unusedKeys.length) {
|
|
610
|
+
console.log(chalk.green('No drift detected. Nothing to apply.'));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const prompts: CheckboxQuestion[] = [];
|
|
615
|
+
if (baseline.missingKeys.length) {
|
|
616
|
+
prompts.push({
|
|
617
|
+
type: 'checkbox',
|
|
618
|
+
name: 'missing',
|
|
619
|
+
message: 'Select missing keys to add',
|
|
620
|
+
pageSize: 15,
|
|
621
|
+
choices: baseline.missingKeys.map((item) => ({
|
|
622
|
+
name: `${item.key} (${item.references.length} reference${item.references.length === 1 ? '' : 's'})`,
|
|
623
|
+
value: item.key,
|
|
624
|
+
checked: true,
|
|
625
|
+
})),
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (baseline.unusedKeys.length) {
|
|
630
|
+
prompts.push({
|
|
631
|
+
type: 'checkbox',
|
|
632
|
+
name: 'unused',
|
|
633
|
+
message: 'Select unused keys to prune',
|
|
634
|
+
pageSize: 15,
|
|
635
|
+
choices: baseline.unusedKeys.map((item) => ({
|
|
636
|
+
name: `${item.key} (${item.locales.join(', ')})`,
|
|
637
|
+
value: item.key,
|
|
638
|
+
checked: true,
|
|
639
|
+
})),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const answers = prompts.length ? await inquirer.prompt(prompts) : {};
|
|
644
|
+
const selectedMissing: string[] = (answers as { missing?: string[] }).missing ?? [];
|
|
645
|
+
const selectedUnused: string[] = (answers as { unused?: string[] }).unused ?? [];
|
|
646
|
+
|
|
647
|
+
if (!selectedMissing.length && !selectedUnused.length) {
|
|
648
|
+
console.log(chalk.yellow('No changes selected. Run again later if needed.'));
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const confirmation = await inquirer.prompt<{ proceed: boolean }>([
|
|
653
|
+
{
|
|
654
|
+
type: 'confirm',
|
|
655
|
+
name: 'proceed',
|
|
656
|
+
default: true,
|
|
657
|
+
message: `Apply ${selectedMissing.length} addition${selectedMissing.length === 1 ? '' : 's'} and ${selectedUnused.length} removal${selectedUnused.length === 1 ? '' : 's'}?`,
|
|
658
|
+
},
|
|
659
|
+
]);
|
|
660
|
+
|
|
661
|
+
if (!confirmation.proceed) {
|
|
662
|
+
console.log(chalk.yellow('Aborted. No changes written.'));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const writeSummary = await syncer.run({
|
|
667
|
+
write: true,
|
|
668
|
+
validateInterpolations: options.validateInterpolations,
|
|
669
|
+
emptyValuePolicy: options.emptyValues === false ? 'fail' : undefined,
|
|
670
|
+
assumedKeys: options.assume,
|
|
671
|
+
selection: {
|
|
672
|
+
missing: selectedMissing,
|
|
673
|
+
unused: selectedUnused,
|
|
674
|
+
},
|
|
675
|
+
diff: diffEnabled,
|
|
676
|
+
targets: options.target,
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
printSyncSummary(writeSummary);
|
|
680
|
+
if (diffEnabled) {
|
|
681
|
+
printLocaleDiffs(writeSummary.diffs);
|
|
682
|
+
}
|
|
683
|
+
if (options.patchDir) {
|
|
684
|
+
await writeLocaleDiffPatches(writeSummary.diffs, options.patchDir);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function printRenameBatchSummary(summary: KeyRenameBatchSummary) {
|
|
689
|
+
console.log(
|
|
690
|
+
chalk.green(
|
|
691
|
+
`Updated ${summary.occurrences} occurrence${summary.occurrences === 1 ? '' : 's'} across ${summary.filesUpdated.length} file${summary.filesUpdated.length === 1 ? '' : 's'}.`
|
|
692
|
+
)
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
if (summary.mappingSummaries.length === 0) {
|
|
696
|
+
console.log(chalk.yellow('No mappings were applied.'));
|
|
697
|
+
} else {
|
|
698
|
+
console.log(chalk.blue('Mappings:'));
|
|
699
|
+
summary.mappingSummaries.slice(0, 50).forEach((mapping) => {
|
|
700
|
+
const refLabel = `${mapping.occurrences} reference${mapping.occurrences === 1 ? '' : 's'}`;
|
|
701
|
+
console.log(` • ${mapping.from} → ${mapping.to} (${refLabel})`);
|
|
702
|
+
|
|
703
|
+
const duplicates = mapping.localePreview
|
|
704
|
+
.filter((preview) => preview.duplicate)
|
|
705
|
+
.map((preview) => preview.locale);
|
|
706
|
+
const missing = mapping.missingLocales;
|
|
707
|
+
|
|
708
|
+
const annotations = [
|
|
709
|
+
missing.length ? `missing locales: ${missing.join(', ')}` : null,
|
|
710
|
+
duplicates.length ? `target already exists in: ${duplicates.join(', ')}` : null,
|
|
711
|
+
].filter(Boolean);
|
|
712
|
+
|
|
713
|
+
if (annotations.length) {
|
|
714
|
+
console.log(chalk.gray(` ${annotations.join(' · ')}`));
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
if (summary.mappingSummaries.length > 50) {
|
|
719
|
+
console.log(chalk.gray(` ...and ${summary.mappingSummaries.length - 50} more.`));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (summary.filesUpdated.length) {
|
|
724
|
+
console.log(chalk.blue('Files updated:'));
|
|
725
|
+
summary.filesUpdated.forEach((file) => console.log(` • ${file}`));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (summary.localeStats.length) {
|
|
729
|
+
console.log(chalk.blue('Locale updates:'));
|
|
730
|
+
summary.localeStats.forEach((stat) => {
|
|
731
|
+
console.log(
|
|
732
|
+
` • ${stat.locale}: ${stat.added.length} added, ${stat.updated.length} updated, ${stat.removed.length} removed (total ${stat.totalKeys})`
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|