i18nsmith 0.2.0 → 0.3.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.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 +2574 -107704
- 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/dist/utils/preview.test.d.ts +2 -0
- package/dist/utils/preview.test.d.ts.map +1 -0
- 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 +117 -8
- package/src/commands/translate/index.ts +7 -4
- package/src/e2e.test.ts +78 -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 +137 -0
- package/src/utils/preview.ts +2 -8
package/src/commands/sync.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import chalk from
|
|
3
|
-
import inquirer, { type CheckboxQuestion } from
|
|
4
|
-
import { promises as fs } from
|
|
5
|
-
import path from
|
|
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
6
|
import {
|
|
7
7
|
Syncer,
|
|
8
8
|
KeyRenamer,
|
|
@@ -14,13 +14,15 @@ import {
|
|
|
14
14
|
type SyncSummary,
|
|
15
15
|
type KeyRenameBatchSummary,
|
|
16
16
|
type SyncSelection,
|
|
17
|
-
} from
|
|
17
|
+
} from "@i18nsmith/core";
|
|
18
18
|
import {
|
|
19
19
|
printLocaleDiffs,
|
|
20
20
|
writeLocaleDiffPatches,
|
|
21
|
-
} from
|
|
22
|
-
import { applyPreviewFile, writePreviewFile } from
|
|
23
|
-
import { SYNC_EXIT_CODES } from
|
|
21
|
+
} from "../utils/diff-utils.js";
|
|
22
|
+
import { applyPreviewFile, writePreviewFile } from "../utils/preview.js";
|
|
23
|
+
import { SYNC_EXIT_CODES } from "../utils/exit-codes.js";
|
|
24
|
+
import { runCheck } from "./check.js";
|
|
25
|
+
import { withErrorHandling } from "../utils/errors.js";
|
|
24
26
|
|
|
25
27
|
interface SyncCommandOptions {
|
|
26
28
|
config?: string;
|
|
@@ -46,8 +48,8 @@ interface SyncCommandOptions {
|
|
|
46
48
|
invalidateCache?: boolean;
|
|
47
49
|
autoRenameSuspicious?: boolean;
|
|
48
50
|
renameMapFile?: string;
|
|
49
|
-
namingConvention?:
|
|
50
|
-
rewriteShape?:
|
|
51
|
+
namingConvention?: "kebab-case" | "camelCase" | "snake_case";
|
|
52
|
+
rewriteShape?: "flat" | "nested";
|
|
51
53
|
shapeDelimiter?: string;
|
|
52
54
|
seedTargetLocales?: boolean;
|
|
53
55
|
seedValue?: string;
|
|
@@ -57,57 +59,219 @@ interface SyncCommandOptions {
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
function collectAssumedKeys(value: string, previous: string[] = []) {
|
|
60
|
-
return previous.concat(value.split(
|
|
62
|
+
return previous.concat(value.split(",").map((k) => k.trim()));
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
function collectTargetPatterns(value: string, previous: string[] = []) {
|
|
64
66
|
return previous.concat(value);
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
const SYNC_HELP_SECTIONS = `
|
|
70
|
+
Examples:
|
|
71
|
+
$ i18nsmith sync # analyze without writing changes
|
|
72
|
+
$ i18nsmith sync --write --prune --yes # prune unused keys non-interactively
|
|
73
|
+
$ i18nsmith sync --preview-output sync-preview.json
|
|
74
|
+
|
|
75
|
+
Option groups:
|
|
76
|
+
General workflow:
|
|
77
|
+
--write, --prune, --no-backup, --yes, --selection-file
|
|
78
|
+
Safety & CI:
|
|
79
|
+
--strict, --validate-interpolations, --no-empty-values, --check, --interactive
|
|
80
|
+
Targeting & detection scope:
|
|
81
|
+
--target, --include, --exclude, --assume, --assume-globs, --invalidate-cache
|
|
82
|
+
Output, previews & diffs:
|
|
83
|
+
--json, --report, --diff, --patch-dir, --preview-output, --apply-preview
|
|
84
|
+
Automation & renaming:
|
|
85
|
+
--auto-rename-suspicious, --rename-map-file, --naming-convention
|
|
86
|
+
Locale shaping & seeding:
|
|
87
|
+
--rewrite-shape, --shape-delimiter, --seed-target-locales, --seed-value
|
|
88
|
+
`;
|
|
89
|
+
|
|
67
90
|
export function registerSync(program: Command) {
|
|
68
91
|
program
|
|
69
|
-
.command(
|
|
70
|
-
.description(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.option(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.option(
|
|
79
|
-
.option(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.option(
|
|
84
|
-
.option(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.option(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
.option(
|
|
94
|
-
.option(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.option(
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
.command("sync")
|
|
93
|
+
.description(
|
|
94
|
+
"Detect missing locale keys and optionally prune unused entries"
|
|
95
|
+
)
|
|
96
|
+
.option(
|
|
97
|
+
"-c, --config <path>",
|
|
98
|
+
"Path to i18nsmith config file",
|
|
99
|
+
"i18n.config.json"
|
|
100
|
+
)
|
|
101
|
+
.option("--json", "Print raw JSON results", false)
|
|
102
|
+
.option(
|
|
103
|
+
"--report <path>",
|
|
104
|
+
"Write JSON summary to a file (for CI or editors)"
|
|
105
|
+
)
|
|
106
|
+
.option("--write", "Write changes to disk (defaults to dry-run)", false)
|
|
107
|
+
.option(
|
|
108
|
+
"--prune",
|
|
109
|
+
"Remove unused keys from locale files (requires --write)",
|
|
110
|
+
false
|
|
111
|
+
)
|
|
112
|
+
.option(
|
|
113
|
+
"--no-backup",
|
|
114
|
+
"Disable automatic backup when using --prune (backup is on by default with --prune)"
|
|
115
|
+
)
|
|
116
|
+
.option("-y, --yes", "Skip confirmation prompts (for CI)", false)
|
|
117
|
+
.option(
|
|
118
|
+
"--check",
|
|
119
|
+
'Alias for "i18nsmith check" (runs the health report instead of sync)',
|
|
120
|
+
false
|
|
121
|
+
)
|
|
122
|
+
.option(
|
|
123
|
+
"--strict",
|
|
124
|
+
"Exit with error code if any suspicious patterns detected (CI mode)",
|
|
125
|
+
false
|
|
126
|
+
)
|
|
127
|
+
.option(
|
|
128
|
+
"--validate-interpolations",
|
|
129
|
+
"Validate interpolation placeholders across locales",
|
|
130
|
+
false
|
|
131
|
+
)
|
|
132
|
+
.option(
|
|
133
|
+
"--no-empty-values",
|
|
134
|
+
"Treat empty or placeholder locale values as failures"
|
|
135
|
+
)
|
|
136
|
+
.option(
|
|
137
|
+
"--assume <keys...>",
|
|
138
|
+
"List of runtime keys to assume present (comma-separated)",
|
|
139
|
+
collectAssumedKeys,
|
|
140
|
+
[]
|
|
141
|
+
)
|
|
142
|
+
.option(
|
|
143
|
+
"--assume-globs <patterns...>",
|
|
144
|
+
"Glob patterns for dynamic key namespaces (e.g., errors.*, navigation.**)",
|
|
145
|
+
collectTargetPatterns,
|
|
146
|
+
[]
|
|
147
|
+
)
|
|
148
|
+
.option(
|
|
149
|
+
"--interactive",
|
|
150
|
+
"Interactively approve locale mutations before writing",
|
|
151
|
+
false
|
|
152
|
+
)
|
|
153
|
+
.option(
|
|
154
|
+
"--diff",
|
|
155
|
+
"Display unified diffs for locale files that would change",
|
|
156
|
+
false
|
|
157
|
+
)
|
|
158
|
+
.option(
|
|
159
|
+
"--patch-dir <path>",
|
|
160
|
+
"Write locale diffs to .patch files in the specified directory"
|
|
161
|
+
)
|
|
162
|
+
.option(
|
|
163
|
+
"--invalidate-cache",
|
|
164
|
+
"Ignore cached sync analysis and rescan all source files",
|
|
165
|
+
false
|
|
166
|
+
)
|
|
167
|
+
.option(
|
|
168
|
+
"--target <pattern...>",
|
|
169
|
+
"Limit translation reference scanning to specific files or glob patterns",
|
|
170
|
+
collectTargetPatterns,
|
|
171
|
+
[]
|
|
172
|
+
)
|
|
173
|
+
.option(
|
|
174
|
+
"--include <patterns...>",
|
|
175
|
+
"Override include globs from config (comma or space separated)",
|
|
176
|
+
collectTargetPatterns,
|
|
177
|
+
[]
|
|
178
|
+
)
|
|
179
|
+
.option(
|
|
180
|
+
"--exclude <patterns...>",
|
|
181
|
+
"Override exclude globs from config (comma or space separated)",
|
|
182
|
+
collectTargetPatterns,
|
|
183
|
+
[]
|
|
184
|
+
)
|
|
185
|
+
.option(
|
|
186
|
+
"--auto-rename-suspicious",
|
|
187
|
+
"Propose normalized names for suspicious keys",
|
|
188
|
+
false
|
|
189
|
+
)
|
|
190
|
+
.option(
|
|
191
|
+
"--rename-map-file <path>",
|
|
192
|
+
"Write rename proposals to a mapping file (JSON or commented format)"
|
|
193
|
+
)
|
|
194
|
+
.option(
|
|
195
|
+
"--naming-convention <convention>",
|
|
196
|
+
"Naming convention for auto-rename (kebab-case, camelCase, snake_case)",
|
|
197
|
+
"kebab-case"
|
|
198
|
+
)
|
|
199
|
+
.option(
|
|
200
|
+
"--rewrite-shape <format>",
|
|
201
|
+
"Rewrite all locale files to flat or nested format"
|
|
202
|
+
)
|
|
203
|
+
.option(
|
|
204
|
+
"--shape-delimiter <char>",
|
|
205
|
+
'Delimiter for key nesting (default: ".")',
|
|
206
|
+
"."
|
|
207
|
+
)
|
|
208
|
+
.option(
|
|
209
|
+
"--seed-target-locales",
|
|
210
|
+
"Add missing keys to target locale files with empty or placeholder values",
|
|
211
|
+
false
|
|
212
|
+
)
|
|
213
|
+
.option(
|
|
214
|
+
"--seed-value <value>",
|
|
215
|
+
"Value to use when seeding target locales (default: empty string)",
|
|
216
|
+
""
|
|
217
|
+
)
|
|
218
|
+
.option(
|
|
219
|
+
"--preview-output <path>",
|
|
220
|
+
"Write preview summary (JSON) to a file (implies dry-run)"
|
|
221
|
+
)
|
|
222
|
+
.option(
|
|
223
|
+
"--selection-file <path>",
|
|
224
|
+
"Path to JSON file with selected missing/unused keys to write (used with --write)"
|
|
225
|
+
)
|
|
226
|
+
.option(
|
|
227
|
+
"--apply-preview <path>",
|
|
228
|
+
"Apply a previously saved sync preview JSON file safely"
|
|
229
|
+
)
|
|
230
|
+
.addHelpText("after", SYNC_HELP_SECTIONS)
|
|
231
|
+
.action(
|
|
232
|
+
withErrorHandling(async (options: SyncCommandOptions) => {
|
|
233
|
+
if (options.check) {
|
|
234
|
+
console.log(
|
|
235
|
+
chalk.yellow(
|
|
236
|
+
"`sync --check` now runs the guided health check. Redirecting to `i18nsmith check`..."
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
await runCheck({
|
|
240
|
+
config: options.config,
|
|
241
|
+
json: options.json,
|
|
242
|
+
report: options.report,
|
|
243
|
+
target: options.target,
|
|
244
|
+
assume: options.assume,
|
|
245
|
+
assumeGlobs: options.assumeGlobs,
|
|
246
|
+
validateInterpolations: options.validateInterpolations,
|
|
247
|
+
emptyValues: options.emptyValues,
|
|
248
|
+
diff: options.diff,
|
|
249
|
+
invalidateCache: options.invalidateCache,
|
|
250
|
+
failOn: options.strict ? "warnings" : "conflicts",
|
|
251
|
+
audit: options.strict,
|
|
252
|
+
auditStrict: options.strict,
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
102
257
|
if (options.applyPreview) {
|
|
103
258
|
const extraArgs: string[] = [];
|
|
104
259
|
if (options.selectionFile) {
|
|
105
|
-
extraArgs.push(
|
|
260
|
+
extraArgs.push("--selection-file", options.selectionFile);
|
|
106
261
|
}
|
|
107
262
|
if (options.prune) {
|
|
108
|
-
extraArgs.push(
|
|
263
|
+
extraArgs.push("--prune");
|
|
109
264
|
}
|
|
110
|
-
|
|
265
|
+
if (options.yes) {
|
|
266
|
+
extraArgs.push("--yes");
|
|
267
|
+
}
|
|
268
|
+
if (options.seedTargetLocales) {
|
|
269
|
+
extraArgs.push("--seed-target-locales");
|
|
270
|
+
}
|
|
271
|
+
if (options.seedValue) {
|
|
272
|
+
extraArgs.push("--seed-value", options.seedValue);
|
|
273
|
+
}
|
|
274
|
+
await applyPreviewFile("sync", options.applyPreview, extraArgs);
|
|
111
275
|
return;
|
|
112
276
|
}
|
|
113
277
|
|
|
@@ -118,13 +282,17 @@ export function registerSync(program: Command) {
|
|
|
118
282
|
const diffRequested = diffEnabled || Boolean(options.json) || previewMode;
|
|
119
283
|
|
|
120
284
|
if (interactive && options.json) {
|
|
121
|
-
console.error(
|
|
285
|
+
console.error(
|
|
286
|
+
chalk.red("--interactive cannot be combined with --json output.")
|
|
287
|
+
);
|
|
122
288
|
process.exitCode = 1;
|
|
123
289
|
return;
|
|
124
290
|
}
|
|
125
291
|
|
|
126
292
|
if (previewMode && interactive) {
|
|
127
|
-
console.error(
|
|
293
|
+
console.error(
|
|
294
|
+
chalk.red("--preview-output cannot be combined with --interactive.")
|
|
295
|
+
);
|
|
128
296
|
process.exitCode = 1;
|
|
129
297
|
return;
|
|
130
298
|
}
|
|
@@ -132,13 +300,19 @@ export function registerSync(program: Command) {
|
|
|
132
300
|
const writeEnabled = Boolean(options.write) && !previewMode;
|
|
133
301
|
if (previewMode && options.write) {
|
|
134
302
|
console.log(
|
|
135
|
-
chalk.yellow(
|
|
303
|
+
chalk.yellow(
|
|
304
|
+
"Preview requested; ignoring --write and running in dry-run mode."
|
|
305
|
+
)
|
|
136
306
|
);
|
|
137
307
|
}
|
|
138
308
|
options.write = writeEnabled;
|
|
139
309
|
|
|
140
310
|
if (options.selectionFile && !options.write) {
|
|
141
|
-
console.error(
|
|
311
|
+
console.error(
|
|
312
|
+
chalk.red(
|
|
313
|
+
"--selection-file requires --write (or --apply-preview) to take effect."
|
|
314
|
+
)
|
|
315
|
+
);
|
|
142
316
|
process.exitCode = 1;
|
|
143
317
|
return;
|
|
144
318
|
}
|
|
@@ -148,24 +322,28 @@ export function registerSync(program: Command) {
|
|
|
148
322
|
: undefined;
|
|
149
323
|
|
|
150
324
|
const banner = previewMode
|
|
151
|
-
?
|
|
325
|
+
? "Generating sync preview..."
|
|
152
326
|
: interactive
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
327
|
+
? "Interactive sync (dry-run first)..."
|
|
328
|
+
: writeEnabled
|
|
329
|
+
? "Syncing locale files..."
|
|
330
|
+
: "Checking locale drift...";
|
|
157
331
|
console.log(chalk.blue(banner));
|
|
158
332
|
|
|
159
333
|
try {
|
|
160
|
-
const { config, projectRoot, configPath } = await loadConfigWithMeta(
|
|
161
|
-
|
|
334
|
+
const { config, projectRoot, configPath } = await loadConfigWithMeta(
|
|
335
|
+
options.config
|
|
336
|
+
);
|
|
337
|
+
|
|
162
338
|
// Inform user if config was found in a parent directory
|
|
163
339
|
const cwd = process.cwd();
|
|
164
340
|
if (projectRoot !== cwd) {
|
|
165
|
-
console.log(
|
|
341
|
+
console.log(
|
|
342
|
+
chalk.gray(`Config found at ${path.relative(cwd, configPath)}`)
|
|
343
|
+
);
|
|
166
344
|
console.log(chalk.gray(`Using project root: ${projectRoot}\n`));
|
|
167
345
|
}
|
|
168
|
-
|
|
346
|
+
|
|
169
347
|
if (options.include?.length) {
|
|
170
348
|
config.include = options.include;
|
|
171
349
|
}
|
|
@@ -184,27 +362,32 @@ export function registerSync(program: Command) {
|
|
|
184
362
|
if (options.seedTargetLocales) {
|
|
185
363
|
config.seedTargetLocales = true;
|
|
186
364
|
}
|
|
187
|
-
if (options.seedValue !== undefined && options.seedValue !==
|
|
365
|
+
if (options.seedValue !== undefined && options.seedValue !== "") {
|
|
188
366
|
config.sync = config.sync ?? {};
|
|
189
367
|
config.sync.seedValue = options.seedValue;
|
|
190
368
|
}
|
|
191
369
|
const syncer = new Syncer(config, { workspaceRoot: projectRoot });
|
|
192
370
|
if (interactive) {
|
|
193
|
-
await runInteractiveSync(syncer, {
|
|
371
|
+
await runInteractiveSync(syncer, {
|
|
372
|
+
...options,
|
|
373
|
+
diff: diffEnabled,
|
|
374
|
+
invalidateCache,
|
|
375
|
+
});
|
|
194
376
|
return;
|
|
195
377
|
}
|
|
196
378
|
|
|
197
379
|
// If writing with prune, first do a dry-run to check scope
|
|
198
380
|
const PRUNE_CONFIRMATION_THRESHOLD = 10;
|
|
199
381
|
let confirmedPrune = options.prune;
|
|
200
|
-
|
|
382
|
+
|
|
201
383
|
if (options.write && options.prune && !options.yes) {
|
|
202
384
|
// Quick dry-run to see how many keys would be pruned
|
|
203
385
|
const dryRunSummary = await syncer.run({
|
|
204
386
|
write: false,
|
|
205
387
|
prune: true,
|
|
206
388
|
validateInterpolations: options.validateInterpolations,
|
|
207
|
-
emptyValuePolicy:
|
|
389
|
+
emptyValuePolicy:
|
|
390
|
+
options.emptyValues === false ? "fail" : undefined,
|
|
208
391
|
assumedKeys: options.assume,
|
|
209
392
|
diff: false,
|
|
210
393
|
invalidateCache,
|
|
@@ -212,29 +395,45 @@ export function registerSync(program: Command) {
|
|
|
212
395
|
});
|
|
213
396
|
|
|
214
397
|
if (dryRunSummary.unusedKeys.length >= PRUNE_CONFIRMATION_THRESHOLD) {
|
|
215
|
-
console.log(
|
|
216
|
-
|
|
398
|
+
console.log(
|
|
399
|
+
chalk.yellow(
|
|
400
|
+
`\n⚠️ About to remove ${dryRunSummary.unusedKeys.length} unused key(s) from locale files.\n`
|
|
401
|
+
)
|
|
402
|
+
);
|
|
403
|
+
|
|
217
404
|
// Show sample of keys to be removed
|
|
218
|
-
const sampleKeys = dryRunSummary.unusedKeys
|
|
405
|
+
const sampleKeys = dryRunSummary.unusedKeys
|
|
406
|
+
.slice(0, 10)
|
|
407
|
+
.map((k) => k.key);
|
|
219
408
|
for (const key of sampleKeys) {
|
|
220
409
|
console.log(chalk.gray(` - ${key}`));
|
|
221
410
|
}
|
|
222
411
|
if (dryRunSummary.unusedKeys.length > 10) {
|
|
223
|
-
console.log(
|
|
412
|
+
console.log(
|
|
413
|
+
chalk.gray(
|
|
414
|
+
` ... and ${dryRunSummary.unusedKeys.length - 10} more`
|
|
415
|
+
)
|
|
416
|
+
);
|
|
224
417
|
}
|
|
225
|
-
console.log(
|
|
226
|
-
|
|
227
|
-
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
418
|
+
console.log("");
|
|
419
|
+
|
|
420
|
+
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>(
|
|
421
|
+
[
|
|
422
|
+
{
|
|
423
|
+
type: "confirm",
|
|
424
|
+
name: "confirmed",
|
|
425
|
+
message: `Remove these ${dryRunSummary.unusedKeys.length} unused keys?`,
|
|
426
|
+
default: false,
|
|
427
|
+
},
|
|
428
|
+
]
|
|
429
|
+
);
|
|
235
430
|
|
|
236
431
|
if (!confirmed) {
|
|
237
|
-
console.log(
|
|
432
|
+
console.log(
|
|
433
|
+
chalk.yellow(
|
|
434
|
+
"Prune cancelled. Running with --write only (add missing keys)."
|
|
435
|
+
)
|
|
436
|
+
);
|
|
238
437
|
confirmedPrune = false;
|
|
239
438
|
}
|
|
240
439
|
}
|
|
@@ -245,7 +444,7 @@ export function registerSync(program: Command) {
|
|
|
245
444
|
prune: confirmedPrune,
|
|
246
445
|
backup: options.backup,
|
|
247
446
|
validateInterpolations: options.validateInterpolations,
|
|
248
|
-
emptyValuePolicy: options.emptyValues === false ?
|
|
447
|
+
emptyValuePolicy: options.emptyValues === false ? "fail" : undefined,
|
|
249
448
|
assumedKeys: options.assume,
|
|
250
449
|
selection: selectionFromFile,
|
|
251
450
|
diff: diffRequested,
|
|
@@ -253,9 +452,79 @@ export function registerSync(program: Command) {
|
|
|
253
452
|
targets: options.target,
|
|
254
453
|
});
|
|
255
454
|
|
|
455
|
+
// If previewing with auto-rename, calculate the rename diffs and include them
|
|
456
|
+
if (
|
|
457
|
+
previewMode &&
|
|
458
|
+
options.autoRenameSuspicious &&
|
|
459
|
+
summary.suspiciousKeys.length > 0
|
|
460
|
+
) {
|
|
461
|
+
const localesDir = path.resolve(
|
|
462
|
+
process.cwd(),
|
|
463
|
+
config.localesDir ?? "locales"
|
|
464
|
+
);
|
|
465
|
+
const localeStore = new LocaleStore(localesDir, {
|
|
466
|
+
sortKeys: config.locales?.sortKeys ?? "alphabetical",
|
|
467
|
+
});
|
|
468
|
+
const sourceLocale = config.sourceLanguage ?? "en";
|
|
469
|
+
const sourceData = await localeStore.get(sourceLocale);
|
|
470
|
+
const existingKeys = new Set(Object.keys(sourceData));
|
|
471
|
+
|
|
472
|
+
const namingConvention = options.namingConvention ?? "kebab-case";
|
|
473
|
+
const report = generateRenameProposals(summary.suspiciousKeys, {
|
|
474
|
+
existingKeys,
|
|
475
|
+
namingConvention,
|
|
476
|
+
workspaceRoot: projectRoot,
|
|
477
|
+
allowExistingConflicts: true,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
if (report.safeProposals.length > 0) {
|
|
481
|
+
const mappings = report.safeProposals.map((proposal) => ({
|
|
482
|
+
from: proposal.originalKey,
|
|
483
|
+
to: proposal.proposedKey,
|
|
484
|
+
}));
|
|
485
|
+
|
|
486
|
+
const renamer = new KeyRenamer(config, {
|
|
487
|
+
workspaceRoot: projectRoot,
|
|
488
|
+
});
|
|
489
|
+
// Run rename batch in dry-run mode with diffs
|
|
490
|
+
const batchSummary = await renamer.renameBatch(mappings, {
|
|
491
|
+
write: false,
|
|
492
|
+
diff: true,
|
|
493
|
+
allowConflicts: true,
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Merge rename diffs into summary
|
|
497
|
+
summary.renameDiffs = batchSummary.diffs;
|
|
498
|
+
|
|
499
|
+
// Merge locale diffs if any (renamer returns localeDiffs)
|
|
500
|
+
if (
|
|
501
|
+
batchSummary.localeDiffs &&
|
|
502
|
+
batchSummary.localeDiffs.length > 0
|
|
503
|
+
) {
|
|
504
|
+
summary.localeDiffs = [
|
|
505
|
+
...(summary.localeDiffs || []),
|
|
506
|
+
...batchSummary.localeDiffs,
|
|
507
|
+
];
|
|
508
|
+
// Also update main diffs array if it's used for preview
|
|
509
|
+
summary.diffs = [
|
|
510
|
+
...(summary.diffs || []),
|
|
511
|
+
...batchSummary.localeDiffs,
|
|
512
|
+
];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
256
517
|
if (previewMode && options.previewOutput) {
|
|
257
|
-
const savedPath = await writePreviewFile(
|
|
258
|
-
|
|
518
|
+
const savedPath = await writePreviewFile(
|
|
519
|
+
"sync",
|
|
520
|
+
summary,
|
|
521
|
+
options.previewOutput
|
|
522
|
+
);
|
|
523
|
+
console.log(
|
|
524
|
+
chalk.green(
|
|
525
|
+
`Preview written to ${path.relative(process.cwd(), savedPath)}`
|
|
526
|
+
)
|
|
527
|
+
);
|
|
259
528
|
}
|
|
260
529
|
|
|
261
530
|
// Show backup info if created
|
|
@@ -285,25 +554,39 @@ export function registerSync(program: Command) {
|
|
|
285
554
|
|
|
286
555
|
// Handle --auto-rename-suspicious
|
|
287
556
|
if (options.autoRenameSuspicious && summary.suspiciousKeys.length > 0) {
|
|
288
|
-
await handleAutoRenameSuspicious(
|
|
557
|
+
await handleAutoRenameSuspicious(
|
|
558
|
+
summary,
|
|
559
|
+
options,
|
|
560
|
+
config,
|
|
561
|
+
projectRoot
|
|
562
|
+
);
|
|
289
563
|
}
|
|
290
564
|
|
|
291
565
|
// Handle --rewrite-shape
|
|
292
|
-
if (
|
|
566
|
+
if (
|
|
567
|
+
options.rewriteShape &&
|
|
568
|
+
(options.rewriteShape === "flat" || options.rewriteShape === "nested")
|
|
569
|
+
) {
|
|
293
570
|
await handleRewriteShape(options, config);
|
|
294
571
|
}
|
|
295
572
|
|
|
296
|
-
const shouldFailPlaceholders =
|
|
573
|
+
const shouldFailPlaceholders =
|
|
574
|
+
summary.validation.interpolations &&
|
|
575
|
+
summary.placeholderIssues.length > 0;
|
|
297
576
|
const shouldFailEmptyValues =
|
|
298
|
-
summary.validation.emptyValuePolicy ===
|
|
577
|
+
summary.validation.emptyValuePolicy === "fail" &&
|
|
578
|
+
summary.emptyValueViolations.length > 0;
|
|
299
579
|
|
|
300
580
|
// --strict mode: fail on any suspicious patterns
|
|
301
581
|
if (options.strict) {
|
|
302
582
|
const hasSuspiciousKeys = summary.suspiciousKeys.length > 0;
|
|
303
|
-
const hasDrift =
|
|
583
|
+
const hasDrift =
|
|
584
|
+
summary.missingKeys.length > 0 || summary.unusedKeys.length > 0;
|
|
304
585
|
|
|
305
586
|
if (hasSuspiciousKeys) {
|
|
306
|
-
console.error(
|
|
587
|
+
console.error(
|
|
588
|
+
chalk.red("\n⚠️ Suspicious patterns detected (--strict mode):")
|
|
589
|
+
);
|
|
307
590
|
const grouped = new Map<string, string[]>();
|
|
308
591
|
for (const warning of summary.suspiciousKeys.slice(0, 20)) {
|
|
309
592
|
const reason = warning.reason;
|
|
@@ -316,52 +599,79 @@ export function registerSync(program: Command) {
|
|
|
316
599
|
console.error(chalk.yellow(` ${reason}:`));
|
|
317
600
|
keys.slice(0, 5).forEach((key) => console.error(` • ${key}`));
|
|
318
601
|
if (keys.length > 5) {
|
|
319
|
-
console.error(
|
|
602
|
+
console.error(
|
|
603
|
+
chalk.gray(` ...and ${keys.length - 5} more.`)
|
|
604
|
+
);
|
|
320
605
|
}
|
|
321
606
|
}
|
|
322
607
|
if (summary.suspiciousKeys.length > 20) {
|
|
323
|
-
console.error(
|
|
608
|
+
console.error(
|
|
609
|
+
chalk.gray(
|
|
610
|
+
` ...and ${summary.suspiciousKeys.length - 20} more warnings.`
|
|
611
|
+
)
|
|
612
|
+
);
|
|
324
613
|
}
|
|
325
614
|
process.exitCode = SYNC_EXIT_CODES.SUSPICIOUS_KEYS;
|
|
326
615
|
return;
|
|
327
616
|
}
|
|
328
617
|
|
|
329
618
|
if (shouldFailPlaceholders) {
|
|
330
|
-
console.error(
|
|
619
|
+
console.error(
|
|
620
|
+
chalk.red("\nPlaceholder mismatches detected (--strict mode).")
|
|
621
|
+
);
|
|
331
622
|
process.exitCode = SYNC_EXIT_CODES.PLACEHOLDER_MISMATCH;
|
|
332
623
|
return;
|
|
333
624
|
}
|
|
334
625
|
|
|
335
626
|
if (shouldFailEmptyValues) {
|
|
336
|
-
console.error(
|
|
627
|
+
console.error(
|
|
628
|
+
chalk.red("\nEmpty locale values detected (--strict mode).")
|
|
629
|
+
);
|
|
337
630
|
process.exitCode = SYNC_EXIT_CODES.EMPTY_VALUES;
|
|
338
631
|
return;
|
|
339
632
|
}
|
|
340
633
|
|
|
341
634
|
if (hasDrift) {
|
|
342
|
-
console.error(
|
|
635
|
+
console.error(
|
|
636
|
+
chalk.red(
|
|
637
|
+
"\nDrift detected (--strict mode). Run with --write to fix."
|
|
638
|
+
)
|
|
639
|
+
);
|
|
343
640
|
process.exitCode = SYNC_EXIT_CODES.DRIFT;
|
|
344
641
|
return;
|
|
345
642
|
}
|
|
346
643
|
|
|
347
|
-
console.log(
|
|
644
|
+
console.log(
|
|
645
|
+
chalk.green("\n✓ No issues detected (--strict mode passed).")
|
|
646
|
+
);
|
|
348
647
|
return;
|
|
349
648
|
}
|
|
350
649
|
|
|
351
650
|
if (options.check) {
|
|
352
|
-
const hasDrift =
|
|
651
|
+
const hasDrift =
|
|
652
|
+
summary.missingKeys.length || summary.unusedKeys.length;
|
|
353
653
|
if (shouldFailPlaceholders) {
|
|
354
|
-
console.error(
|
|
654
|
+
console.error(
|
|
655
|
+
chalk.red(
|
|
656
|
+
"\nPlaceholder mismatches detected. Run with --write to fix."
|
|
657
|
+
)
|
|
658
|
+
);
|
|
355
659
|
process.exitCode = SYNC_EXIT_CODES.PLACEHOLDER_MISMATCH;
|
|
356
660
|
return;
|
|
357
661
|
}
|
|
358
662
|
if (shouldFailEmptyValues) {
|
|
359
|
-
console.error(
|
|
663
|
+
console.error(
|
|
664
|
+
chalk.red(
|
|
665
|
+
"\nEmpty locale values detected. Run with --write to fix."
|
|
666
|
+
)
|
|
667
|
+
);
|
|
360
668
|
process.exitCode = SYNC_EXIT_CODES.EMPTY_VALUES;
|
|
361
669
|
return;
|
|
362
670
|
}
|
|
363
671
|
if (hasDrift) {
|
|
364
|
-
console.error(
|
|
672
|
+
console.error(
|
|
673
|
+
chalk.red("\nDrift detected. Run with --write to fix.")
|
|
674
|
+
);
|
|
365
675
|
process.exitCode = SYNC_EXIT_CODES.DRIFT;
|
|
366
676
|
return;
|
|
367
677
|
}
|
|
@@ -369,117 +679,162 @@ export function registerSync(program: Command) {
|
|
|
369
679
|
|
|
370
680
|
if (!options.write) {
|
|
371
681
|
// Show prominent dry-run indicator
|
|
372
|
-
console.log(chalk.cyan(
|
|
682
|
+
console.log(chalk.cyan("\n📋 DRY RUN - No files were modified"));
|
|
373
683
|
if (summary.missingKeys.length && summary.unusedKeys.length) {
|
|
374
|
-
console.log(
|
|
375
|
-
|
|
684
|
+
console.log(
|
|
685
|
+
chalk.yellow("Run again with --write to add missing keys.")
|
|
686
|
+
);
|
|
687
|
+
console.log(
|
|
688
|
+
chalk.yellow(
|
|
689
|
+
"Run with --write --prune to also remove unused keys."
|
|
690
|
+
)
|
|
691
|
+
);
|
|
376
692
|
} else if (summary.missingKeys.length) {
|
|
377
|
-
console.log(
|
|
693
|
+
console.log(
|
|
694
|
+
chalk.yellow("Run again with --write to add missing keys.")
|
|
695
|
+
);
|
|
378
696
|
} else if (summary.unusedKeys.length) {
|
|
379
|
-
console.log(
|
|
697
|
+
console.log(
|
|
698
|
+
chalk.yellow(
|
|
699
|
+
"Unused keys found. Run with --write --prune to remove them."
|
|
700
|
+
)
|
|
701
|
+
);
|
|
380
702
|
}
|
|
381
|
-
} else if (
|
|
382
|
-
|
|
703
|
+
} else if (
|
|
704
|
+
options.write &&
|
|
705
|
+
!options.prune &&
|
|
706
|
+
summary.unusedKeys.length
|
|
707
|
+
) {
|
|
708
|
+
console.log(
|
|
709
|
+
chalk.gray(
|
|
710
|
+
`\n Note: ${summary.unusedKeys.length} unused key(s) were not removed. Use --prune to remove them.`
|
|
711
|
+
)
|
|
712
|
+
);
|
|
383
713
|
}
|
|
384
714
|
} catch (error) {
|
|
385
|
-
console.error(chalk.red(
|
|
715
|
+
console.error(chalk.red("Sync failed:"), (error as Error).message);
|
|
386
716
|
process.exitCode = 1;
|
|
387
717
|
}
|
|
388
|
-
|
|
718
|
+
})
|
|
719
|
+
);
|
|
389
720
|
}
|
|
390
721
|
|
|
391
722
|
function printSyncSummary(summary: SyncSummary) {
|
|
392
723
|
console.log(
|
|
393
724
|
chalk.green(
|
|
394
|
-
`Scanned ${summary.filesScanned} file${summary.filesScanned === 1 ?
|
|
395
|
-
`${summary.references.length} translation reference${summary.references.length === 1 ?
|
|
725
|
+
`Scanned ${summary.filesScanned} file${summary.filesScanned === 1 ? "" : "s"}; ` +
|
|
726
|
+
`${summary.references.length} translation reference${summary.references.length === 1 ? "" : "s"} found.`
|
|
396
727
|
)
|
|
397
728
|
);
|
|
398
729
|
|
|
399
730
|
if (summary.missingKeys.length) {
|
|
400
|
-
console.log(chalk.red(
|
|
731
|
+
console.log(chalk.red("Missing keys:"));
|
|
401
732
|
summary.missingKeys.slice(0, 50).forEach((item) => {
|
|
402
733
|
const sample = item.references[0];
|
|
403
|
-
const location = sample
|
|
404
|
-
|
|
734
|
+
const location = sample
|
|
735
|
+
? `${sample.filePath}:${sample.position.line}`
|
|
736
|
+
: "n/a";
|
|
737
|
+
console.log(
|
|
738
|
+
` • ${item.key} (${item.references.length} reference${item.references.length === 1 ? "" : "s"} — e.g., ${location})`
|
|
739
|
+
);
|
|
405
740
|
});
|
|
406
741
|
if (summary.missingKeys.length > 50) {
|
|
407
|
-
console.log(
|
|
742
|
+
console.log(
|
|
743
|
+
chalk.gray(` ...and ${summary.missingKeys.length - 50} more.`)
|
|
744
|
+
);
|
|
408
745
|
}
|
|
409
746
|
} else {
|
|
410
|
-
console.log(chalk.green(
|
|
747
|
+
console.log(chalk.green("No missing keys detected."));
|
|
411
748
|
}
|
|
412
749
|
|
|
413
750
|
if (summary.unusedKeys.length) {
|
|
414
|
-
console.log(chalk.yellow(
|
|
751
|
+
console.log(chalk.yellow("Unused locale keys:"));
|
|
415
752
|
summary.unusedKeys.slice(0, 50).forEach((item) => {
|
|
416
|
-
console.log(` • ${item.key} (${item.locales.join(
|
|
753
|
+
console.log(` • ${item.key} (${item.locales.join(", ")})`);
|
|
417
754
|
});
|
|
418
755
|
if (summary.unusedKeys.length > 50) {
|
|
419
|
-
console.log(
|
|
756
|
+
console.log(
|
|
757
|
+
chalk.gray(` ...and ${summary.unusedKeys.length - 50} more.`)
|
|
758
|
+
);
|
|
420
759
|
}
|
|
421
760
|
} else {
|
|
422
|
-
console.log(chalk.green(
|
|
761
|
+
console.log(chalk.green("No unused locale keys detected."));
|
|
423
762
|
}
|
|
424
763
|
|
|
425
764
|
if (summary.validation.interpolations) {
|
|
426
765
|
if (summary.placeholderIssues.length) {
|
|
427
|
-
console.log(chalk.yellow(
|
|
766
|
+
console.log(chalk.yellow("Placeholder mismatches:"));
|
|
428
767
|
summary.placeholderIssues.slice(0, 50).forEach((issue) => {
|
|
429
|
-
const missing = issue.missing.length
|
|
430
|
-
|
|
431
|
-
|
|
768
|
+
const missing = issue.missing.length
|
|
769
|
+
? `missing [${issue.missing.join(", ")}]`
|
|
770
|
+
: "";
|
|
771
|
+
const extra = issue.extra.length
|
|
772
|
+
? `extra [${issue.extra.join(", ")}]`
|
|
773
|
+
: "";
|
|
774
|
+
const detail = [missing, extra].filter(Boolean).join("; ");
|
|
432
775
|
console.log(` • ${issue.key} (${issue.locale}) ${detail}`);
|
|
433
776
|
});
|
|
434
777
|
if (summary.placeholderIssues.length > 50) {
|
|
435
|
-
console.log(
|
|
778
|
+
console.log(
|
|
779
|
+
chalk.gray(` ...and ${summary.placeholderIssues.length - 50} more.`)
|
|
780
|
+
);
|
|
436
781
|
}
|
|
437
782
|
} else {
|
|
438
|
-
console.log(chalk.green(
|
|
783
|
+
console.log(chalk.green("No placeholder mismatches detected."));
|
|
439
784
|
}
|
|
440
785
|
}
|
|
441
786
|
|
|
442
|
-
if (summary.validation.emptyValuePolicy !==
|
|
787
|
+
if (summary.validation.emptyValuePolicy !== "ignore") {
|
|
443
788
|
if (summary.emptyValueViolations.length) {
|
|
444
789
|
const label =
|
|
445
|
-
summary.validation.emptyValuePolicy ===
|
|
446
|
-
? chalk.red(
|
|
447
|
-
: chalk.yellow(
|
|
790
|
+
summary.validation.emptyValuePolicy === "fail"
|
|
791
|
+
? chalk.red("Empty locale values:")
|
|
792
|
+
: chalk.yellow("Empty locale values:");
|
|
448
793
|
console.log(label);
|
|
449
794
|
summary.emptyValueViolations.slice(0, 50).forEach((violation) => {
|
|
450
|
-
console.log(
|
|
795
|
+
console.log(
|
|
796
|
+
` • ${violation.key} (${violation.locale}) — ${violation.reason}`
|
|
797
|
+
);
|
|
451
798
|
});
|
|
452
799
|
if (summary.emptyValueViolations.length > 50) {
|
|
453
|
-
console.log(
|
|
800
|
+
console.log(
|
|
801
|
+
chalk.gray(
|
|
802
|
+
` ...and ${summary.emptyValueViolations.length - 50} more.`
|
|
803
|
+
)
|
|
804
|
+
);
|
|
454
805
|
}
|
|
455
806
|
} else {
|
|
456
|
-
console.log(chalk.green(
|
|
807
|
+
console.log(chalk.green("No empty locale values detected."));
|
|
457
808
|
}
|
|
458
809
|
}
|
|
459
810
|
|
|
460
811
|
if (summary.dynamicKeyWarnings.length) {
|
|
461
|
-
console.log(chalk.yellow(
|
|
812
|
+
console.log(chalk.yellow("Dynamic translation keys detected:"));
|
|
462
813
|
summary.dynamicKeyWarnings.slice(0, 50).forEach((warning) => {
|
|
463
814
|
console.log(
|
|
464
815
|
` • ${warning.filePath}:${warning.position.line} (${warning.reason}) ${chalk.gray(warning.expression)}`
|
|
465
816
|
);
|
|
466
817
|
});
|
|
467
818
|
if (summary.dynamicKeyWarnings.length > 50) {
|
|
468
|
-
console.log(
|
|
819
|
+
console.log(
|
|
820
|
+
chalk.gray(` ...and ${summary.dynamicKeyWarnings.length - 50} more.`)
|
|
821
|
+
);
|
|
469
822
|
}
|
|
470
823
|
if (summary.assumedKeys.length) {
|
|
471
|
-
console.log(
|
|
824
|
+
console.log(
|
|
825
|
+
chalk.blue(`Assumed runtime keys: ${summary.assumedKeys.join(", ")}`)
|
|
826
|
+
);
|
|
472
827
|
} else {
|
|
473
828
|
console.log(
|
|
474
829
|
chalk.gray(
|
|
475
|
-
|
|
830
|
+
"Use --assume key1,key2 to prevent false positives for known runtime-only translation keys."
|
|
476
831
|
)
|
|
477
832
|
);
|
|
478
833
|
}
|
|
479
834
|
}
|
|
480
835
|
|
|
481
836
|
if (summary.localeStats.length) {
|
|
482
|
-
console.log(chalk.blue(
|
|
837
|
+
console.log(chalk.blue("Locale file changes:"));
|
|
483
838
|
summary.localeStats.forEach((stat) => {
|
|
484
839
|
console.log(
|
|
485
840
|
` • ${stat.locale}: ${stat.added.length} added, ${stat.updated.length} updated, ${stat.removed.length} removed (total ${stat.totalKeys})`
|
|
@@ -488,7 +843,7 @@ function printSyncSummary(summary: SyncSummary) {
|
|
|
488
843
|
}
|
|
489
844
|
|
|
490
845
|
if (!summary.write && summary.localePreview.length) {
|
|
491
|
-
console.log(chalk.blue(
|
|
846
|
+
console.log(chalk.blue("Locale diff preview:"));
|
|
492
847
|
summary.localePreview.forEach((stat) => {
|
|
493
848
|
console.log(
|
|
494
849
|
` • ${stat.locale}: ${stat.add.length} to add, ${stat.remove.length} to remove`
|
|
@@ -503,53 +858,85 @@ async function handleAutoRenameSuspicious(
|
|
|
503
858
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
504
859
|
projectRoot: string
|
|
505
860
|
) {
|
|
506
|
-
console.log(chalk.blue(
|
|
861
|
+
console.log(chalk.blue("\n📝 Auto-rename suspicious keys analysis:"));
|
|
507
862
|
|
|
508
863
|
// Get existing keys from locale data to check for conflicts
|
|
509
|
-
const localesDir = path.resolve(
|
|
864
|
+
const localesDir = path.resolve(
|
|
865
|
+
process.cwd(),
|
|
866
|
+
config.localesDir ?? "locales"
|
|
867
|
+
);
|
|
510
868
|
const localeStore = new LocaleStore(localesDir, {
|
|
511
|
-
sortKeys: config.locales?.sortKeys ??
|
|
869
|
+
sortKeys: config.locales?.sortKeys ?? "alphabetical",
|
|
512
870
|
});
|
|
513
|
-
const sourceLocale = config.sourceLanguage ??
|
|
871
|
+
const sourceLocale = config.sourceLanguage ?? "en";
|
|
514
872
|
const sourceData = await localeStore.get(sourceLocale);
|
|
515
873
|
const existingKeys = new Set(Object.keys(sourceData));
|
|
516
874
|
|
|
517
875
|
// Generate rename proposals
|
|
518
|
-
const namingConvention = options.namingConvention ??
|
|
876
|
+
const namingConvention = options.namingConvention ?? "kebab-case";
|
|
519
877
|
const report = generateRenameProposals(summary.suspiciousKeys, {
|
|
520
878
|
existingKeys,
|
|
521
879
|
namingConvention,
|
|
880
|
+
allowExistingConflicts: true,
|
|
522
881
|
});
|
|
523
882
|
|
|
524
883
|
// Print summary
|
|
525
884
|
console.log(` Found ${report.totalSuspicious} suspicious key(s)`);
|
|
526
885
|
|
|
527
886
|
if (report.safeProposals.length > 0) {
|
|
528
|
-
console.log(
|
|
887
|
+
console.log(
|
|
888
|
+
chalk.green(
|
|
889
|
+
`\n ✓ Safe rename proposals (${report.safeProposals.length}):`
|
|
890
|
+
)
|
|
891
|
+
);
|
|
529
892
|
const toShow = report.safeProposals.slice(0, 10);
|
|
530
893
|
for (const proposal of toShow) {
|
|
531
|
-
console.log(
|
|
532
|
-
|
|
894
|
+
console.log(
|
|
895
|
+
chalk.gray(` "${proposal.originalKey}" → "${proposal.proposedKey}"`)
|
|
896
|
+
);
|
|
897
|
+
console.log(
|
|
898
|
+
chalk.gray(
|
|
899
|
+
` (${proposal.reason}) in ${proposal.filePath}:${proposal.position.line}`
|
|
900
|
+
)
|
|
901
|
+
);
|
|
533
902
|
}
|
|
534
903
|
if (report.safeProposals.length > 10) {
|
|
535
|
-
console.log(
|
|
904
|
+
console.log(
|
|
905
|
+
chalk.gray(` ...and ${report.safeProposals.length - 10} more`)
|
|
906
|
+
);
|
|
536
907
|
}
|
|
537
908
|
}
|
|
538
909
|
|
|
539
910
|
if (report.conflictProposals.length > 0) {
|
|
540
|
-
console.log(
|
|
911
|
+
console.log(
|
|
912
|
+
chalk.yellow(
|
|
913
|
+
`\n ⚠️ Conflicting proposals (${report.conflictProposals.length}):`
|
|
914
|
+
)
|
|
915
|
+
);
|
|
541
916
|
const toShow = report.conflictProposals.slice(0, 5);
|
|
542
917
|
for (const proposal of toShow) {
|
|
543
|
-
console.log(
|
|
544
|
-
|
|
918
|
+
console.log(
|
|
919
|
+
chalk.yellow(
|
|
920
|
+
` "${proposal.originalKey}" → "${proposal.proposedKey}"`
|
|
921
|
+
)
|
|
922
|
+
);
|
|
923
|
+
console.log(
|
|
924
|
+
chalk.gray(` Conflicts with: ${proposal.conflictsWith}`)
|
|
925
|
+
);
|
|
545
926
|
}
|
|
546
927
|
if (report.conflictProposals.length > 5) {
|
|
547
|
-
console.log(
|
|
928
|
+
console.log(
|
|
929
|
+
chalk.gray(` ...and ${report.conflictProposals.length - 5} more`)
|
|
930
|
+
);
|
|
548
931
|
}
|
|
549
932
|
}
|
|
550
933
|
|
|
551
934
|
if (report.skippedKeys.length > 0) {
|
|
552
|
-
console.log(
|
|
935
|
+
console.log(
|
|
936
|
+
chalk.gray(
|
|
937
|
+
`\n Skipped ${report.skippedKeys.length} key(s) (already normalized or no change needed)`
|
|
938
|
+
)
|
|
939
|
+
);
|
|
553
940
|
}
|
|
554
941
|
|
|
555
942
|
// Write mapping file if requested
|
|
@@ -557,34 +944,53 @@ async function handleAutoRenameSuspicious(
|
|
|
557
944
|
|
|
558
945
|
if (options.renameMapFile && hasMappings) {
|
|
559
946
|
const outputPath = path.resolve(process.cwd(), options.renameMapFile);
|
|
560
|
-
const isJsonFormat = outputPath.endsWith(
|
|
947
|
+
const isJsonFormat = outputPath.endsWith(".json");
|
|
561
948
|
const content = createRenameMappingFile(report.renameMapping, {
|
|
562
949
|
includeComments: !isJsonFormat,
|
|
563
950
|
});
|
|
564
951
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
565
|
-
await fs.writeFile(outputPath, content,
|
|
952
|
+
await fs.writeFile(outputPath, content, "utf8");
|
|
566
953
|
console.log(chalk.green(`\n ✓ Rename mapping written to ${outputPath}`));
|
|
567
|
-
console.log(
|
|
954
|
+
console.log(
|
|
955
|
+
chalk.gray(
|
|
956
|
+
" Apply with: npx i18nsmith rename-keys --map " +
|
|
957
|
+
options.renameMapFile +
|
|
958
|
+
" --write"
|
|
959
|
+
)
|
|
960
|
+
);
|
|
568
961
|
}
|
|
569
962
|
|
|
570
963
|
if (options.write) {
|
|
571
964
|
if (report.safeProposals.length === 0) {
|
|
572
|
-
console.log(chalk.yellow(
|
|
965
|
+
console.log(chalk.yellow("\n No safe rename proposals to apply."));
|
|
573
966
|
} else {
|
|
574
|
-
console.log(
|
|
967
|
+
console.log(
|
|
968
|
+
chalk.blue("\n✍️ Applying safe rename proposals (source + locales)...")
|
|
969
|
+
);
|
|
575
970
|
const mappings = report.safeProposals.map((proposal) => ({
|
|
576
971
|
from: proposal.originalKey,
|
|
577
972
|
to: proposal.proposedKey,
|
|
578
973
|
}));
|
|
579
974
|
|
|
580
975
|
const renamer = new KeyRenamer(config, { workspaceRoot: projectRoot });
|
|
581
|
-
const applySummary = await renamer.renameBatch(mappings, {
|
|
976
|
+
const applySummary = await renamer.renameBatch(mappings, {
|
|
977
|
+
write: true,
|
|
978
|
+
diff: Boolean(options.diff),
|
|
979
|
+
allowConflicts: true,
|
|
980
|
+
});
|
|
582
981
|
printRenameBatchSummary(applySummary);
|
|
583
982
|
|
|
584
983
|
if (!options.renameMapFile && hasMappings) {
|
|
585
|
-
const defaultMapPath = path.resolve(
|
|
984
|
+
const defaultMapPath = path.resolve(
|
|
985
|
+
projectRoot,
|
|
986
|
+
".i18nsmith",
|
|
987
|
+
"auto-rename-map.json"
|
|
988
|
+
);
|
|
586
989
|
await fs.mkdir(path.dirname(defaultMapPath), { recursive: true });
|
|
587
|
-
await fs.writeFile(
|
|
990
|
+
await fs.writeFile(
|
|
991
|
+
defaultMapPath,
|
|
992
|
+
JSON.stringify(report.renameMapping, null, 2)
|
|
993
|
+
);
|
|
588
994
|
console.log(
|
|
589
995
|
chalk.gray(
|
|
590
996
|
`\n Saved rename mapping to ${path.relative(process.cwd(), defaultMapPath)} (set --rename-map-file to customize)`
|
|
@@ -593,8 +999,14 @@ async function handleAutoRenameSuspicious(
|
|
|
593
999
|
}
|
|
594
1000
|
}
|
|
595
1001
|
} else if (hasMappings && !options.renameMapFile) {
|
|
596
|
-
console.log(
|
|
597
|
-
|
|
1002
|
+
console.log(
|
|
1003
|
+
chalk.gray(
|
|
1004
|
+
"\n Use --rename-map-file <path> to export mappings for later application."
|
|
1005
|
+
)
|
|
1006
|
+
);
|
|
1007
|
+
console.log(
|
|
1008
|
+
chalk.gray(" Run with --write to apply safe proposals automatically.")
|
|
1009
|
+
);
|
|
598
1010
|
}
|
|
599
1011
|
}
|
|
600
1012
|
|
|
@@ -602,19 +1014,24 @@ async function handleRewriteShape(
|
|
|
602
1014
|
options: SyncCommandOptions,
|
|
603
1015
|
config: Awaited<ReturnType<typeof loadConfig>>
|
|
604
1016
|
) {
|
|
605
|
-
const targetFormat = options.rewriteShape as
|
|
606
|
-
const delimiter = options.shapeDelimiter ??
|
|
1017
|
+
const targetFormat = options.rewriteShape as "flat" | "nested";
|
|
1018
|
+
const delimiter = options.shapeDelimiter ?? ".";
|
|
607
1019
|
|
|
608
|
-
console.log(
|
|
1020
|
+
console.log(
|
|
1021
|
+
chalk.blue(`\n🔄 Rewriting locale files to ${targetFormat} format...`)
|
|
1022
|
+
);
|
|
609
1023
|
|
|
610
|
-
const localesDir = path.resolve(
|
|
1024
|
+
const localesDir = path.resolve(
|
|
1025
|
+
process.cwd(),
|
|
1026
|
+
config.localesDir ?? "locales"
|
|
1027
|
+
);
|
|
611
1028
|
const localeStore = new LocaleStore(localesDir, {
|
|
612
1029
|
delimiter,
|
|
613
|
-
sortKeys: config.locales?.sortKeys ??
|
|
1030
|
+
sortKeys: config.locales?.sortKeys ?? "alphabetical",
|
|
614
1031
|
});
|
|
615
1032
|
|
|
616
1033
|
// Load all configured locales
|
|
617
|
-
const sourceLocale = config.sourceLanguage ??
|
|
1034
|
+
const sourceLocale = config.sourceLanguage ?? "en";
|
|
618
1035
|
const targetLocales = config.targetLanguages ?? [];
|
|
619
1036
|
const allLocales = [sourceLocale, ...targetLocales];
|
|
620
1037
|
|
|
@@ -626,11 +1043,15 @@ async function handleRewriteShape(
|
|
|
626
1043
|
const stats = await localeStore.rewriteShape(targetFormat, { delimiter });
|
|
627
1044
|
|
|
628
1045
|
if (stats.length === 0) {
|
|
629
|
-
console.log(chalk.yellow(
|
|
1046
|
+
console.log(chalk.yellow(" No locale files found to rewrite."));
|
|
630
1047
|
return;
|
|
631
1048
|
}
|
|
632
1049
|
|
|
633
|
-
console.log(
|
|
1050
|
+
console.log(
|
|
1051
|
+
chalk.green(
|
|
1052
|
+
` ✓ Rewrote ${stats.length} locale file(s) to ${targetFormat} format:`
|
|
1053
|
+
)
|
|
1054
|
+
);
|
|
634
1055
|
for (const stat of stats) {
|
|
635
1056
|
console.log(chalk.gray(` • ${stat.locale}: ${stat.totalKeys} keys`));
|
|
636
1057
|
}
|
|
@@ -642,7 +1063,7 @@ async function runInteractiveSync(syncer: Syncer, options: SyncCommandOptions) {
|
|
|
642
1063
|
const baseline = await syncer.run({
|
|
643
1064
|
write: false,
|
|
644
1065
|
validateInterpolations: options.validateInterpolations,
|
|
645
|
-
emptyValuePolicy: options.emptyValues === false ?
|
|
1066
|
+
emptyValuePolicy: options.emptyValues === false ? "fail" : undefined,
|
|
646
1067
|
assumedKeys: options.assume,
|
|
647
1068
|
diff: diffEnabled,
|
|
648
1069
|
invalidateCache,
|
|
@@ -658,19 +1079,19 @@ async function runInteractiveSync(syncer: Syncer, options: SyncCommandOptions) {
|
|
|
658
1079
|
}
|
|
659
1080
|
|
|
660
1081
|
if (!baseline.missingKeys.length && !baseline.unusedKeys.length) {
|
|
661
|
-
console.log(chalk.green(
|
|
1082
|
+
console.log(chalk.green("No drift detected. Nothing to apply."));
|
|
662
1083
|
return;
|
|
663
1084
|
}
|
|
664
1085
|
|
|
665
1086
|
const prompts: CheckboxQuestion[] = [];
|
|
666
1087
|
if (baseline.missingKeys.length) {
|
|
667
1088
|
prompts.push({
|
|
668
|
-
type:
|
|
669
|
-
name:
|
|
670
|
-
message:
|
|
1089
|
+
type: "checkbox",
|
|
1090
|
+
name: "missing",
|
|
1091
|
+
message: "Select missing keys to add",
|
|
671
1092
|
pageSize: 15,
|
|
672
1093
|
choices: baseline.missingKeys.map((item) => ({
|
|
673
|
-
name: `${item.key} (${item.references.length} reference${item.references.length === 1 ?
|
|
1094
|
+
name: `${item.key} (${item.references.length} reference${item.references.length === 1 ? "" : "s"})`,
|
|
674
1095
|
value: item.key,
|
|
675
1096
|
checked: true,
|
|
676
1097
|
})),
|
|
@@ -679,12 +1100,12 @@ async function runInteractiveSync(syncer: Syncer, options: SyncCommandOptions) {
|
|
|
679
1100
|
|
|
680
1101
|
if (baseline.unusedKeys.length) {
|
|
681
1102
|
prompts.push({
|
|
682
|
-
type:
|
|
683
|
-
name:
|
|
684
|
-
message:
|
|
1103
|
+
type: "checkbox",
|
|
1104
|
+
name: "unused",
|
|
1105
|
+
message: "Select unused keys to prune",
|
|
685
1106
|
pageSize: 15,
|
|
686
1107
|
choices: baseline.unusedKeys.map((item) => ({
|
|
687
|
-
name: `${item.key} (${item.locales.join(
|
|
1108
|
+
name: `${item.key} (${item.locales.join(", ")})`,
|
|
688
1109
|
value: item.key,
|
|
689
1110
|
checked: true,
|
|
690
1111
|
})),
|
|
@@ -692,32 +1113,36 @@ async function runInteractiveSync(syncer: Syncer, options: SyncCommandOptions) {
|
|
|
692
1113
|
}
|
|
693
1114
|
|
|
694
1115
|
const answers = prompts.length ? await inquirer.prompt(prompts) : {};
|
|
695
|
-
const selectedMissing: string[] =
|
|
696
|
-
|
|
1116
|
+
const selectedMissing: string[] =
|
|
1117
|
+
(answers as { missing?: string[] }).missing ?? [];
|
|
1118
|
+
const selectedUnused: string[] =
|
|
1119
|
+
(answers as { unused?: string[] }).unused ?? [];
|
|
697
1120
|
|
|
698
1121
|
if (!selectedMissing.length && !selectedUnused.length) {
|
|
699
|
-
console.log(
|
|
1122
|
+
console.log(
|
|
1123
|
+
chalk.yellow("No changes selected. Run again later if needed.")
|
|
1124
|
+
);
|
|
700
1125
|
return;
|
|
701
1126
|
}
|
|
702
1127
|
|
|
703
1128
|
const confirmation = await inquirer.prompt<{ proceed: boolean }>([
|
|
704
1129
|
{
|
|
705
|
-
type:
|
|
706
|
-
name:
|
|
1130
|
+
type: "confirm",
|
|
1131
|
+
name: "proceed",
|
|
707
1132
|
default: true,
|
|
708
|
-
message: `Apply ${selectedMissing.length} addition${selectedMissing.length === 1 ?
|
|
1133
|
+
message: `Apply ${selectedMissing.length} addition${selectedMissing.length === 1 ? "" : "s"} and ${selectedUnused.length} removal${selectedUnused.length === 1 ? "" : "s"}?`,
|
|
709
1134
|
},
|
|
710
1135
|
]);
|
|
711
1136
|
|
|
712
1137
|
if (!confirmation.proceed) {
|
|
713
|
-
console.log(chalk.yellow(
|
|
1138
|
+
console.log(chalk.yellow("Aborted. No changes written."));
|
|
714
1139
|
return;
|
|
715
1140
|
}
|
|
716
1141
|
|
|
717
1142
|
const writeSummary = await syncer.run({
|
|
718
1143
|
write: true,
|
|
719
1144
|
validateInterpolations: options.validateInterpolations,
|
|
720
|
-
emptyValuePolicy: options.emptyValues === false ?
|
|
1145
|
+
emptyValuePolicy: options.emptyValues === false ? "fail" : undefined,
|
|
721
1146
|
assumedKeys: options.assume,
|
|
722
1147
|
selection: {
|
|
723
1148
|
missing: selectedMissing,
|
|
@@ -737,13 +1162,17 @@ async function runInteractiveSync(syncer: Syncer, options: SyncCommandOptions) {
|
|
|
737
1162
|
}
|
|
738
1163
|
|
|
739
1164
|
async function loadSelectionFile(filePath: string): Promise<SyncSelection> {
|
|
740
|
-
const resolvedPath = path.isAbsolute(filePath)
|
|
1165
|
+
const resolvedPath = path.isAbsolute(filePath)
|
|
1166
|
+
? filePath
|
|
1167
|
+
: path.resolve(process.cwd(), filePath);
|
|
741
1168
|
let raw: string;
|
|
742
1169
|
try {
|
|
743
|
-
raw = await fs.readFile(resolvedPath,
|
|
1170
|
+
raw = await fs.readFile(resolvedPath, "utf8");
|
|
744
1171
|
} catch (error) {
|
|
745
1172
|
const message = error instanceof Error ? error.message : String(error);
|
|
746
|
-
throw new Error(
|
|
1173
|
+
throw new Error(
|
|
1174
|
+
`Unable to read selection file at ${resolvedPath}: ${message}`
|
|
1175
|
+
);
|
|
747
1176
|
}
|
|
748
1177
|
|
|
749
1178
|
let parsed: unknown;
|
|
@@ -751,7 +1180,9 @@ async function loadSelectionFile(filePath: string): Promise<SyncSelection> {
|
|
|
751
1180
|
parsed = JSON.parse(raw);
|
|
752
1181
|
} catch (error) {
|
|
753
1182
|
const message = error instanceof Error ? error.message : String(error);
|
|
754
|
-
throw new Error(
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
`Selection file ${resolvedPath} contains invalid JSON: ${message}`
|
|
1185
|
+
);
|
|
755
1186
|
}
|
|
756
1187
|
|
|
757
1188
|
const selection: SyncSelection = {};
|
|
@@ -773,7 +1204,9 @@ async function loadSelectionFile(filePath: string): Promise<SyncSelection> {
|
|
|
773
1204
|
}
|
|
774
1205
|
|
|
775
1206
|
if (!selection.missing?.length && !selection.unused?.length) {
|
|
776
|
-
throw new Error(
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
`Selection file ${resolvedPath} must include at least one "missing" or "unused" entry.`
|
|
1209
|
+
);
|
|
777
1210
|
}
|
|
778
1211
|
|
|
779
1212
|
return selection;
|
|
@@ -782,16 +1215,16 @@ async function loadSelectionFile(filePath: string): Promise<SyncSelection> {
|
|
|
782
1215
|
function printRenameBatchSummary(summary: KeyRenameBatchSummary) {
|
|
783
1216
|
console.log(
|
|
784
1217
|
chalk.green(
|
|
785
|
-
`Updated ${summary.occurrences} occurrence${summary.occurrences === 1 ?
|
|
1218
|
+
`Updated ${summary.occurrences} occurrence${summary.occurrences === 1 ? "" : "s"} across ${summary.filesUpdated.length} file${summary.filesUpdated.length === 1 ? "" : "s"}.`
|
|
786
1219
|
)
|
|
787
1220
|
);
|
|
788
1221
|
|
|
789
1222
|
if (summary.mappingSummaries.length === 0) {
|
|
790
|
-
console.log(chalk.yellow(
|
|
1223
|
+
console.log(chalk.yellow("No mappings were applied."));
|
|
791
1224
|
} else {
|
|
792
|
-
console.log(chalk.blue(
|
|
1225
|
+
console.log(chalk.blue("Mappings:"));
|
|
793
1226
|
summary.mappingSummaries.slice(0, 50).forEach((mapping) => {
|
|
794
|
-
const refLabel = `${mapping.occurrences} reference${mapping.occurrences === 1 ?
|
|
1227
|
+
const refLabel = `${mapping.occurrences} reference${mapping.occurrences === 1 ? "" : "s"}`;
|
|
795
1228
|
console.log(` • ${mapping.from} → ${mapping.to} (${refLabel})`);
|
|
796
1229
|
|
|
797
1230
|
const duplicates = mapping.localePreview
|
|
@@ -800,27 +1233,31 @@ function printRenameBatchSummary(summary: KeyRenameBatchSummary) {
|
|
|
800
1233
|
const missing = mapping.missingLocales;
|
|
801
1234
|
|
|
802
1235
|
const annotations = [
|
|
803
|
-
missing.length ? `missing locales: ${missing.join(
|
|
804
|
-
duplicates.length
|
|
1236
|
+
missing.length ? `missing locales: ${missing.join(", ")}` : null,
|
|
1237
|
+
duplicates.length
|
|
1238
|
+
? `target already exists in: ${duplicates.join(", ")}`
|
|
1239
|
+
: null,
|
|
805
1240
|
].filter(Boolean);
|
|
806
1241
|
|
|
807
1242
|
if (annotations.length) {
|
|
808
|
-
console.log(chalk.gray(` ${annotations.join(
|
|
1243
|
+
console.log(chalk.gray(` ${annotations.join(" · ")}`));
|
|
809
1244
|
}
|
|
810
1245
|
});
|
|
811
1246
|
|
|
812
1247
|
if (summary.mappingSummaries.length > 50) {
|
|
813
|
-
console.log(
|
|
1248
|
+
console.log(
|
|
1249
|
+
chalk.gray(` ...and ${summary.mappingSummaries.length - 50} more.`)
|
|
1250
|
+
);
|
|
814
1251
|
}
|
|
815
1252
|
}
|
|
816
1253
|
|
|
817
1254
|
if (summary.filesUpdated.length) {
|
|
818
|
-
console.log(chalk.blue(
|
|
1255
|
+
console.log(chalk.blue("Files updated:"));
|
|
819
1256
|
summary.filesUpdated.forEach((file) => console.log(` • ${file}`));
|
|
820
1257
|
}
|
|
821
1258
|
|
|
822
1259
|
if (summary.localeStats.length) {
|
|
823
|
-
console.log(chalk.blue(
|
|
1260
|
+
console.log(chalk.blue("Locale updates:"));
|
|
824
1261
|
summary.localeStats.forEach((stat) => {
|
|
825
1262
|
console.log(
|
|
826
1263
|
` • ${stat.locale}: ${stat.added.length} added, ${stat.updated.length} updated, ${stat.removed.length} removed (total ${stat.totalKeys})`
|