i18nsmith 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.mjs +5 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/transform.d.ts.map +1 -1
- package/dist/index.cjs +6354 -2910
- package/package.json +1 -1
- package/src/commands/check.ts +5 -1
- package/src/commands/config.ts +126 -0
- package/src/commands/init.test.ts +80 -0
- package/src/commands/init.ts +33 -22
- package/src/commands/scan.ts +8 -1
- package/src/commands/sync.ts +43 -4
- package/src/commands/transform.ts +8 -1
- package/src/index.ts +1 -1
- package/src/integration.test.ts +145 -12
package/package.json
CHANGED
package/src/commands/check.ts
CHANGED
|
@@ -186,7 +186,11 @@ export function registerCheck(program: Command) {
|
|
|
186
186
|
|
|
187
187
|
export async function runCheck(options: CheckCommandOptions): Promise<void> {
|
|
188
188
|
const auditEnabled = Boolean(options.audit || options.auditStrict);
|
|
189
|
-
|
|
189
|
+
if (options.json) {
|
|
190
|
+
console.error(chalk.blue('Running guided repository health check...'));
|
|
191
|
+
} else {
|
|
192
|
+
console.log(chalk.blue('Running guided repository health check...'));
|
|
193
|
+
}
|
|
190
194
|
try {
|
|
191
195
|
const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
|
|
192
196
|
|
package/src/commands/config.ts
CHANGED
|
@@ -282,4 +282,130 @@ export function registerConfig(program: Command) {
|
|
|
282
282
|
}
|
|
283
283
|
})
|
|
284
284
|
);
|
|
285
|
+
|
|
286
|
+
// Subcommand: config migrate
|
|
287
|
+
configCmd
|
|
288
|
+
.command('migrate')
|
|
289
|
+
.description('Migrate configuration from v1 to v2 format and apply modern defaults')
|
|
290
|
+
.option('-c, --config <path>', 'Path to i18nsmith config file', DEFAULT_CONFIG_FILENAME)
|
|
291
|
+
.option('--dry-run', 'Show what would be changed without modifying files', false)
|
|
292
|
+
.option('--json', 'Output result as JSON', false)
|
|
293
|
+
.action(
|
|
294
|
+
withErrorHandling(async (options: { config?: string; dryRun?: boolean; json?: boolean }) => {
|
|
295
|
+
try {
|
|
296
|
+
const { configPath } = await loadConfigWithMeta(options.config);
|
|
297
|
+
const { parsed: rawConfig } = await readRawConfig(configPath);
|
|
298
|
+
|
|
299
|
+
// Check if migration is needed
|
|
300
|
+
const currentVersion = (rawConfig.configVersion ?? rawConfig.version ?? 1) as number;
|
|
301
|
+
if (currentVersion >= 2) {
|
|
302
|
+
if (options.json) {
|
|
303
|
+
console.log(JSON.stringify({ migrated: false, reason: 'Already at latest version' }, null, 2));
|
|
304
|
+
} else {
|
|
305
|
+
console.log(chalk.blue('Configuration is already up to date (v2 or later)'));
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Apply migration transformations
|
|
311
|
+
const migratedConfig = { ...rawConfig };
|
|
312
|
+
|
|
313
|
+
// Set new version
|
|
314
|
+
migratedConfig.configVersion = 2;
|
|
315
|
+
delete migratedConfig.version;
|
|
316
|
+
|
|
317
|
+
// Migrate field names
|
|
318
|
+
if (rawConfig.sourceLocale) {
|
|
319
|
+
migratedConfig.sourceLanguage = rawConfig.sourceLocale;
|
|
320
|
+
delete migratedConfig.sourceLocale;
|
|
321
|
+
}
|
|
322
|
+
if (rawConfig.targetLocales) {
|
|
323
|
+
migratedConfig.targetLanguages = rawConfig.targetLocales;
|
|
324
|
+
delete migratedConfig.targetLocales;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Add extraction preset with strict defaults for new configs
|
|
328
|
+
if (!migratedConfig.extraction) {
|
|
329
|
+
migratedConfig.extraction = {};
|
|
330
|
+
}
|
|
331
|
+
const extraction = migratedConfig.extraction as Record<string, unknown>;
|
|
332
|
+
if (!extraction.preset) {
|
|
333
|
+
extraction.preset = 'strict';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Clean up deprecated fields
|
|
337
|
+
const deprecatedFields = ['projectName'];
|
|
338
|
+
for (const field of deprecatedFields) {
|
|
339
|
+
if (field in migratedConfig) {
|
|
340
|
+
delete migratedConfig[field];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (options.dryRun) {
|
|
345
|
+
if (options.json) {
|
|
346
|
+
console.log(JSON.stringify({
|
|
347
|
+
migrated: true,
|
|
348
|
+
dryRun: true,
|
|
349
|
+
changes: {
|
|
350
|
+
added: { configVersion: 2, 'extraction.preset': 'strict' },
|
|
351
|
+
removed: deprecatedFields.filter(f => f in rawConfig),
|
|
352
|
+
renamed: Object.assign({},
|
|
353
|
+
rawConfig.sourceLocale ? { sourceLocale: 'sourceLanguage' } : {},
|
|
354
|
+
rawConfig.targetLocales ? { targetLocales: 'targetLanguages' } : {},
|
|
355
|
+
),
|
|
356
|
+
},
|
|
357
|
+
result: migratedConfig,
|
|
358
|
+
}, null, 2));
|
|
359
|
+
} else {
|
|
360
|
+
console.log(chalk.blue('Migration preview (dry run):'));
|
|
361
|
+
console.log();
|
|
362
|
+
console.log(chalk.green('✓ Would set configVersion: 2'));
|
|
363
|
+
console.log(chalk.green('✓ Would add extraction.preset: strict'));
|
|
364
|
+
if (rawConfig.sourceLocale) {
|
|
365
|
+
console.log(chalk.green(`✓ Would rename sourceLocale → sourceLanguage`));
|
|
366
|
+
}
|
|
367
|
+
if (rawConfig.targetLocales) {
|
|
368
|
+
console.log(chalk.green(`✓ Would rename targetLocales → targetLanguages`));
|
|
369
|
+
}
|
|
370
|
+
const removed = deprecatedFields.filter(f => f in rawConfig);
|
|
371
|
+
if (removed.length > 0) {
|
|
372
|
+
console.log(chalk.green(`✓ Would remove deprecated fields: ${removed.join(', ')}`));
|
|
373
|
+
}
|
|
374
|
+
console.log();
|
|
375
|
+
console.log(chalk.dim('Run without --dry-run to apply changes'));
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
await writeConfig(configPath, migratedConfig);
|
|
379
|
+
|
|
380
|
+
if (options.json) {
|
|
381
|
+
console.log(JSON.stringify({
|
|
382
|
+
migrated: true,
|
|
383
|
+
configPath,
|
|
384
|
+
changes: {
|
|
385
|
+
added: { configVersion: 2, 'extraction.preset': 'strict' },
|
|
386
|
+
removed: deprecatedFields.filter(f => f in rawConfig),
|
|
387
|
+
renamed: Object.assign({},
|
|
388
|
+
rawConfig.sourceLocale ? { sourceLocale: 'sourceLanguage' } : {},
|
|
389
|
+
rawConfig.targetLocales ? { targetLocales: 'targetLanguages' } : {},
|
|
390
|
+
),
|
|
391
|
+
},
|
|
392
|
+
}, null, 2));
|
|
393
|
+
} else {
|
|
394
|
+
console.log(chalk.green('✓ Configuration migrated to v2'));
|
|
395
|
+
console.log(chalk.dim(` Updated: ${configPath}`));
|
|
396
|
+
console.log();
|
|
397
|
+
console.log(chalk.blue('New features available:'));
|
|
398
|
+
console.log(chalk.dim(' • extraction.preset: strict (reduces false positives)'));
|
|
399
|
+
console.log(chalk.dim(' • Improved field names for clarity'));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error instanceof CliError) {
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
407
|
+
throw new CliError(`Failed to migrate config: ${message}`);
|
|
408
|
+
}
|
|
409
|
+
})
|
|
410
|
+
);
|
|
285
411
|
}
|
|
@@ -160,6 +160,7 @@ vi.mock('inquirer', () => ({
|
|
|
160
160
|
sourceLanguage: 'en',
|
|
161
161
|
adapter: 'custom',
|
|
162
162
|
localesDir: 'locales',
|
|
163
|
+
seedTargetLocales: false,
|
|
163
164
|
}),
|
|
164
165
|
},
|
|
165
166
|
}));
|
|
@@ -256,6 +257,85 @@ describe('init command', () => {
|
|
|
256
257
|
);
|
|
257
258
|
});
|
|
258
259
|
});
|
|
260
|
+
|
|
261
|
+
describe('interactive mode', () => {
|
|
262
|
+
it('writes seedTargetLocales into config when user enables it', async () => {
|
|
263
|
+
const program = new Command();
|
|
264
|
+
registerInit(program);
|
|
265
|
+
const command = program.commands.find((cmd) => cmd.name() === 'init')!;
|
|
266
|
+
|
|
267
|
+
// Mock process.cwd to return a test directory
|
|
268
|
+
const originalCwd = process.cwd;
|
|
269
|
+
process.cwd = vi.fn().mockReturnValue('/test/project');
|
|
270
|
+
|
|
271
|
+
// Make fs.access throw so init thinks config doesn't exist
|
|
272
|
+
vi.mocked(fs.access).mockRejectedValueOnce(new Error('File not found'));
|
|
273
|
+
|
|
274
|
+
// Override inquirer for this run to enable seedTargetLocales
|
|
275
|
+
const inquirer = await import('inquirer');
|
|
276
|
+
vi.mocked(inquirer.default.prompt).mockResolvedValueOnce({
|
|
277
|
+
setupMode: 'auto',
|
|
278
|
+
sourceLanguage: 'en',
|
|
279
|
+
localesDir: 'locales',
|
|
280
|
+
seedTargetLocales: true,
|
|
281
|
+
} as any);
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
await (command as any).parseAsync([], { from: 'user' });
|
|
285
|
+
} finally {
|
|
286
|
+
process.cwd = originalCwd;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Find the writeFile call that wrote i18n.config.json
|
|
290
|
+
const wroteConfig = vi.mocked(fs.writeFile).mock.calls.find((c) => String(c[0]).endsWith('i18n.config.json'));
|
|
291
|
+
expect(wroteConfig).toBeDefined();
|
|
292
|
+
expect(String(wroteConfig![1])).toContain('"seedTargetLocales": true');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('continues when user chooses Overwrite for existing assets and records mergeStrategy', async () => {
|
|
296
|
+
const program = new Command();
|
|
297
|
+
registerInit(program);
|
|
298
|
+
const command = program.commands.find((cmd) => cmd.name() === 'init')!;
|
|
299
|
+
|
|
300
|
+
const originalCwd = process.cwd;
|
|
301
|
+
process.cwd = vi.fn().mockReturnValue('/test/project');
|
|
302
|
+
|
|
303
|
+
// Make fs.access throw so init thinks config doesn't exist
|
|
304
|
+
vi.mocked(fs.access).mockRejectedValueOnce(new Error('File not found'));
|
|
305
|
+
|
|
306
|
+
// Make diagnoseWorkspace report existing locale files so merge prompt appears
|
|
307
|
+
const core = await import('@i18nsmith/core');
|
|
308
|
+
vi.mocked(core.diagnoseWorkspace).mockResolvedValueOnce({
|
|
309
|
+
localesDir: 'locales',
|
|
310
|
+
localeFiles: [{ locale: 'en', missing: false, parseError: false }],
|
|
311
|
+
detectedLocales: [],
|
|
312
|
+
runtimePackages: [],
|
|
313
|
+
providerFiles: [],
|
|
314
|
+
adapterFiles: [],
|
|
315
|
+
translationUsage: { hookName: 'useTranslation', translationIdentifier: 't', filesExamined: 0, hookOccurrences: 0, identifierOccurrences: 0, hookExampleFiles: [], identifierExampleFiles: [] },
|
|
316
|
+
actionableItems: [],
|
|
317
|
+
conflicts: [],
|
|
318
|
+
recommendations: [],
|
|
319
|
+
} as any);
|
|
320
|
+
|
|
321
|
+
// Sequence of prompts: first the main answers, then the merge strategy choice
|
|
322
|
+
const inquirer = await import('inquirer');
|
|
323
|
+
vi.mocked(inquirer.default.prompt)
|
|
324
|
+
.mockResolvedValueOnce({ setupMode: 'auto', sourceLanguage: 'en', localesDir: 'locales', seedTargetLocales: false } as any)
|
|
325
|
+
.mockResolvedValueOnce({ strategy: 'overwrite' } as any);
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
await (command as any).parseAsync([], { from: 'user' });
|
|
329
|
+
} finally {
|
|
330
|
+
process.cwd = originalCwd;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Ensure config was written with mergeStrategy set to overwrite
|
|
334
|
+
const wroteConfig = vi.mocked(fs.writeFile).mock.calls.find((c) => String(c[0]).endsWith('i18n.config.json'));
|
|
335
|
+
expect(wroteConfig).toBeDefined();
|
|
336
|
+
expect(String(wroteConfig![1])).toContain('"mergeStrategy": "overwrite"');
|
|
337
|
+
});
|
|
338
|
+
});
|
|
259
339
|
});
|
|
260
340
|
|
|
261
341
|
describe('parseGlobList', () => {
|
package/src/commands/init.ts
CHANGED
|
@@ -112,7 +112,7 @@ async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promis
|
|
|
112
112
|
let suggestedConfig: SuggestedConfig;
|
|
113
113
|
|
|
114
114
|
if (intelligence) {
|
|
115
|
-
const { framework, locales, filePatterns
|
|
115
|
+
const { framework, locales, filePatterns } = intelligence;
|
|
116
116
|
|
|
117
117
|
// Report detection results
|
|
118
118
|
if (framework.type !== 'unknown') {
|
|
@@ -510,6 +510,13 @@ export function registerInit(program: Command) {
|
|
|
510
510
|
return !isNaN(num) && num > 0 ? true : 'Please enter a positive number';
|
|
511
511
|
},
|
|
512
512
|
},
|
|
513
|
+
{
|
|
514
|
+
type: 'confirm',
|
|
515
|
+
name: 'seedTargetLocales',
|
|
516
|
+
message: 'Seed target locale files with placeholders for missing keys?',
|
|
517
|
+
when: () => true,
|
|
518
|
+
default: false,
|
|
519
|
+
},
|
|
513
520
|
]);
|
|
514
521
|
|
|
515
522
|
|
|
@@ -645,6 +652,12 @@ export function registerInit(program: Command) {
|
|
|
645
652
|
};
|
|
646
653
|
}
|
|
647
654
|
|
|
655
|
+
// Honor explicit interactive answer for seeding target locales regardless
|
|
656
|
+
// of which setup mode (auto/template/manual) produced the initial config.
|
|
657
|
+
if (typeof (answers as InitAnswers).seedTargetLocales === 'boolean' && config) {
|
|
658
|
+
config.seedTargetLocales = (answers as InitAnswers).seedTargetLocales;
|
|
659
|
+
}
|
|
660
|
+
|
|
648
661
|
const mergeDecision = await maybePromptMergeStrategy(config, workspaceRoot, Boolean(commandOptions.merge));
|
|
649
662
|
if (mergeDecision?.aborted) {
|
|
650
663
|
console.log(chalk.yellow('Aborting init to avoid overwriting existing i18n assets. Re-run with --merge to bypass.'));
|
|
@@ -787,35 +800,33 @@ async function maybePromptMergeStrategy(
|
|
|
787
800
|
}
|
|
788
801
|
}
|
|
789
802
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
return { strategy: null, aborted: true };
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
+
// Single consolidated prompt for both --merge and interactive flows. When
|
|
804
|
+
// `--merge` is provided we omit the 'Abort' choice and default to
|
|
805
|
+
// 'keep-source'; otherwise include an explicit 'Abort' option (last).
|
|
806
|
+
const baseChoices = [
|
|
807
|
+
{ name: 'Keep source values (append new keys only)', value: 'keep-source' },
|
|
808
|
+
{ name: 'Overwrite with placeholders (backup first)', value: 'overwrite' },
|
|
809
|
+
{ name: 'Prompt during sync to review changes interactively', value: 'interactive' },
|
|
810
|
+
];
|
|
811
|
+
|
|
812
|
+
const choices = mergeRequested ? baseChoices : [...baseChoices, { name: 'Abort (do not touch existing files)', value: 'abort' }];
|
|
803
813
|
|
|
804
|
-
const { strategy } = await inquirer.prompt<{
|
|
814
|
+
const { strategy } = await inquirer.prompt<{
|
|
815
|
+
strategy: 'abort' | MergeStrategy;
|
|
816
|
+
}>([
|
|
805
817
|
{
|
|
806
818
|
type: 'list',
|
|
807
819
|
name: 'strategy',
|
|
808
|
-
message: '
|
|
809
|
-
choices
|
|
810
|
-
{ name: 'Keep source values (append new keys only)', value: 'keep-source' },
|
|
811
|
-
{ name: 'Overwrite with placeholders (backup first)', value: 'overwrite' },
|
|
812
|
-
{ name: 'Interactive review during sync', value: 'interactive' },
|
|
813
|
-
],
|
|
820
|
+
message: 'Existing i18n assets detected — choose how to proceed',
|
|
821
|
+
choices,
|
|
814
822
|
default: 'keep-source',
|
|
815
823
|
},
|
|
816
824
|
]);
|
|
817
825
|
|
|
818
|
-
|
|
826
|
+
if (strategy === 'abort') {
|
|
827
|
+
return { strategy: null, aborted: true };
|
|
828
|
+
}
|
|
829
|
+
return { strategy: strategy as MergeStrategy, aborted: false };
|
|
819
830
|
} catch (error) {
|
|
820
831
|
console.warn(chalk.gray(`Skipping merge diagnostics: ${(error as Error).message}`));
|
|
821
832
|
return { strategy: null, aborted: false };
|
package/src/commands/scan.ts
CHANGED
|
@@ -57,7 +57,14 @@ export function registerScan(program: Command) {
|
|
|
57
57
|
.option('--exclude <patterns...>', 'Override exclude globs from config (comma or space separated)', collectTargetPatterns, [])
|
|
58
58
|
.action(
|
|
59
59
|
withErrorHandling(async (options: ScanOptions) => {
|
|
60
|
-
|
|
60
|
+
// When JSON output is requested, avoid human-readable preamble on stdout
|
|
61
|
+
// so callers can reliably parse the JSON summary. Send banner to stderr
|
|
62
|
+
// when --json is used.
|
|
63
|
+
if (options.json) {
|
|
64
|
+
console.error(chalk.blue('Starting scan...'));
|
|
65
|
+
} else {
|
|
66
|
+
console.log(chalk.blue('Starting scan...'));
|
|
67
|
+
}
|
|
61
68
|
|
|
62
69
|
try {
|
|
63
70
|
const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
|
package/src/commands/sync.ts
CHANGED
|
@@ -48,7 +48,7 @@ interface SyncCommandOptions {
|
|
|
48
48
|
invalidateCache?: boolean;
|
|
49
49
|
autoRenameSuspicious?: boolean;
|
|
50
50
|
renameMapFile?: string;
|
|
51
|
-
namingConvention?: "kebab-case" | "camelCase" | "snake_case";
|
|
51
|
+
namingConvention?: "kebab-case" | "camelCase" | "snake_case" | "auto";
|
|
52
52
|
rewriteShape?: "flat" | "nested";
|
|
53
53
|
shapeDelimiter?: string;
|
|
54
54
|
seedTargetLocales?: boolean;
|
|
@@ -193,7 +193,7 @@ export function registerSync(program: Command) {
|
|
|
193
193
|
)
|
|
194
194
|
.option(
|
|
195
195
|
"--naming-convention <convention>",
|
|
196
|
-
"Naming convention for auto-rename (kebab-case, camelCase, snake_case)",
|
|
196
|
+
"Naming convention for auto-rename (kebab-case, camelCase, snake_case, auto)",
|
|
197
197
|
"kebab-case"
|
|
198
198
|
)
|
|
199
199
|
.option(
|
|
@@ -328,7 +328,14 @@ export function registerSync(program: Command) {
|
|
|
328
328
|
: writeEnabled
|
|
329
329
|
? "Syncing locale files..."
|
|
330
330
|
: "Checking locale drift...";
|
|
331
|
-
|
|
331
|
+
// When JSON output is requested, avoid human-readable preamble on stdout
|
|
332
|
+
// so callers can reliably parse the JSON summary. Send banner to stderr
|
|
333
|
+
// when --json is used.
|
|
334
|
+
if (options.json) {
|
|
335
|
+
console.error(chalk.blue(banner));
|
|
336
|
+
} else {
|
|
337
|
+
console.log(chalk.blue(banner));
|
|
338
|
+
}
|
|
332
339
|
|
|
333
340
|
try {
|
|
334
341
|
const { config, projectRoot, configPath } = await loadConfigWithMeta(
|
|
@@ -480,6 +487,8 @@ export function registerSync(program: Command) {
|
|
|
480
487
|
namingConvention,
|
|
481
488
|
workspaceRoot: projectRoot,
|
|
482
489
|
allowExistingConflicts: true,
|
|
490
|
+
allExistingKeys: Object.keys(sourceData), // Pass all existing keys for convention detection
|
|
491
|
+
preserveExistingConvention: true, // Respect existing project conventions
|
|
483
492
|
});
|
|
484
493
|
|
|
485
494
|
if (report.safeProposals.length > 0) {
|
|
@@ -769,6 +778,18 @@ function printSyncSummary(summary: SyncSummary) {
|
|
|
769
778
|
console.log(chalk.green("No unused locale keys detected."));
|
|
770
779
|
}
|
|
771
780
|
|
|
781
|
+
if (summary.untranslatedKeys.length) {
|
|
782
|
+
console.log(chalk.blue("Untranslated keys (protected from pruning):"));
|
|
783
|
+
summary.untranslatedKeys.slice(0, 50).forEach((item) => {
|
|
784
|
+
console.log(` • ${item.key} (${item.locales.join(", ")})`);
|
|
785
|
+
});
|
|
786
|
+
if (summary.untranslatedKeys.length > 50) {
|
|
787
|
+
console.log(
|
|
788
|
+
chalk.gray(` ...and ${summary.untranslatedKeys.length - 50} more.`)
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
772
793
|
if (summary.validation.interpolations) {
|
|
773
794
|
if (summary.placeholderIssues.length) {
|
|
774
795
|
console.log(chalk.yellow("Placeholder mismatches:"));
|
|
@@ -880,11 +901,29 @@ async function handleAutoRenameSuspicious(
|
|
|
880
901
|
const sourceData = await localeStore.get(sourceLocale);
|
|
881
902
|
const existingKeys = new Set(Object.keys(sourceData));
|
|
882
903
|
|
|
883
|
-
//
|
|
904
|
+
// For auto-detection, collect all existing keys from all locales
|
|
905
|
+
let allExistingKeys: string[] | undefined;
|
|
884
906
|
const namingConvention = options.namingConvention ?? "kebab-case";
|
|
907
|
+
if (namingConvention === "auto") {
|
|
908
|
+
try {
|
|
909
|
+
const allLocales = await localeStore.getStoredLocales();
|
|
910
|
+
const allKeys = new Set<string>();
|
|
911
|
+
for (const locale of allLocales) {
|
|
912
|
+
const localeData = await localeStore.get(locale);
|
|
913
|
+
Object.keys(localeData).forEach(key => allKeys.add(key));
|
|
914
|
+
}
|
|
915
|
+
allExistingKeys = Array.from(allKeys);
|
|
916
|
+
console.log(` Auto-detected naming convention from ${allKeys.size} existing keys`);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
console.warn(chalk.yellow(" Warning: Could not load all locales for convention detection, falling back to kebab-case"));
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Generate rename proposals
|
|
885
923
|
const report = generateRenameProposals(summary.suspiciousKeys, {
|
|
886
924
|
existingKeys,
|
|
887
925
|
namingConvention,
|
|
926
|
+
allExistingKeys,
|
|
888
927
|
allowExistingConflicts: true,
|
|
889
928
|
});
|
|
890
929
|
|
|
@@ -227,7 +227,14 @@ export function registerTransform(program: Command) {
|
|
|
227
227
|
: writeEnabled
|
|
228
228
|
? 'Running transform (write mode)...'
|
|
229
229
|
: 'Planning transform (dry-run)...';
|
|
230
|
-
|
|
230
|
+
// When JSON output is requested, avoid human-readable preamble on stdout
|
|
231
|
+
// so callers can reliably parse the JSON summary. Send banner to stderr
|
|
232
|
+
// when --json is used.
|
|
233
|
+
if (options.json) {
|
|
234
|
+
console.error(chalk.blue(banner));
|
|
235
|
+
} else {
|
|
236
|
+
console.log(chalk.blue(banner));
|
|
237
|
+
}
|
|
231
238
|
|
|
232
239
|
try {
|
|
233
240
|
const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
|
package/src/index.ts
CHANGED