i18nsmith 0.3.3 → 0.4.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.
Files changed (36) hide show
  1. package/build.mjs +1 -1
  2. package/dist/commands/detect.d.ts +3 -0
  3. package/dist/commands/detect.d.ts.map +1 -0
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/rename.d.ts.map +1 -1
  6. package/dist/commands/scan.d.ts.map +1 -1
  7. package/dist/commands/sync.d.ts.map +1 -1
  8. package/dist/commands/transform.d.ts.map +1 -1
  9. package/dist/commands/translate/csv-handler.d.ts.map +1 -1
  10. package/dist/index.cjs +47711 -42734
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/test-helpers/ensure-cli-built.d.ts.map +1 -1
  13. package/dist/utils/adapter-preflight.d.ts +10 -0
  14. package/dist/utils/adapter-preflight.d.ts.map +1 -0
  15. package/i18n.config.json +14 -0
  16. package/package.json +4 -2
  17. package/src/commands/detect.ts +342 -0
  18. package/src/commands/init.test.ts +208 -1
  19. package/src/commands/init.ts +472 -195
  20. package/src/commands/rename.ts +13 -0
  21. package/src/commands/review.ts +1 -1
  22. package/src/commands/scan.ts +4 -1
  23. package/src/commands/sync.ts +23 -3
  24. package/src/commands/transform.ts +54 -2
  25. package/src/commands/translate/csv-handler.ts +2 -1
  26. package/src/e2e.test.ts +4 -4
  27. package/src/fixtures/suspicious-keys/locales/en.json +8 -8
  28. package/src/fixtures/suspicious-keys/locales/fr.json +8 -8
  29. package/src/fixtures/suspicious-keys/preview.json +419 -0
  30. package/src/fixtures/suspicious-keys/src/BadKeys.tsx.backup +19 -0
  31. package/src/index.ts +3 -1
  32. package/src/integration.test.ts +2 -6
  33. package/src/rename-suspicious.test.ts +3 -3
  34. package/src/test-helpers/ensure-cli-built.ts +18 -0
  35. package/src/utils/adapter-preflight.ts +53 -0
  36. package/test.vue +33 -0
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { promises as fs } from 'node:fs';
5
5
  import { loadConfig, KeyRenamer, type KeyRenameSummary, type KeyRenameBatchSummary, type KeyRenameMapping } from '@i18nsmith/core';
6
6
  import { applyPreviewFile, writePreviewFile } from '../utils/preview.js';
7
+ import { runAdapterPreflight } from '../utils/adapter-preflight.js';
7
8
  import { CliError, withErrorHandling } from '../utils/errors.js';
8
9
 
9
10
  interface ScanOptions {
@@ -59,6 +60,12 @@ export function registerRename(program: Command): void {
59
60
 
60
61
  try {
61
62
  const config = await loadConfig(options.config);
63
+
64
+ // Run preflight checks for write operations
65
+ if (writeEnabled) {
66
+ await runAdapterPreflight();
67
+ }
68
+
62
69
  const renamer = new KeyRenamer(config);
63
70
  const summary = await renamer.rename(oldKey, newKey, {
64
71
  write: options.write,
@@ -113,6 +120,12 @@ export function registerRename(program: Command): void {
113
120
 
114
121
  try {
115
122
  const config = await loadConfig(options.config);
123
+
124
+ // Run preflight checks for write operations
125
+ if (options.write) {
126
+ await runAdapterPreflight();
127
+ }
128
+
116
129
  const mappings = await loadRenameMappings(options.map);
117
130
  const renamer = new KeyRenamer(config);
118
131
  const summary = await renamer.renameBatch(mappings, { write: options.write, diff: options.diff });
@@ -138,7 +138,7 @@ export function registerReview(program: Command) {
138
138
  withErrorHandling(async (options: ReviewCommandOptions) => {
139
139
  try {
140
140
  const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
141
- const scanner = new Scanner(config, { workspaceRoot: projectRoot });
141
+ const scanner = await Scanner.create(config, { workspaceRoot: projectRoot });
142
142
  const summary = scanner.scan({ scanCalls: options.scanCalls }) as BucketedScanSummary;
143
143
  const buckets = summary.buckets ?? {};
144
144
  const needsReview = buckets.needsReview ?? [];
@@ -75,7 +75,10 @@ export function registerScan(program: Command) {
75
75
  if (options.exclude?.length) {
76
76
  config.exclude = options.exclude;
77
77
  }
78
- const scanner = new Scanner(config, { workspaceRoot: projectRoot });
78
+ // Use factory that registers framework adapters (React/Vue) so
79
+ // scans include files handled by adapters. Backwards compatible
80
+ // API: Scanner.create will return a scanner with adapters wired.
81
+ const scanner = await Scanner.create(config, { workspaceRoot: projectRoot });
79
82
  const summary = scanner.scan();
80
83
 
81
84
  if (options.report) {
@@ -554,12 +554,15 @@ export function registerSync(program: Command) {
554
554
 
555
555
  // Handle --auto-rename-suspicious
556
556
  if (options.autoRenameSuspicious && summary.suspiciousKeys.length > 0) {
557
- await handleAutoRenameSuspicious(
557
+ const renameDiffs = await handleAutoRenameSuspicious(
558
558
  summary,
559
559
  options,
560
560
  config,
561
561
  projectRoot
562
562
  );
563
+ if (renameDiffs && renameDiffs.length > 0) {
564
+ summary.renameDiffs = renameDiffs;
565
+ }
563
566
  }
564
567
 
565
568
  // Handle --rewrite-shape
@@ -827,7 +830,7 @@ function printSyncSummary(summary: SyncSummary) {
827
830
  } else {
828
831
  console.log(
829
832
  chalk.gray(
830
- "Use --assume key1,key2 to prevent false positives for known runtime-only translation keys."
833
+ "Use --assume to prevent false positives for known runtime-only translation keys."
831
834
  )
832
835
  );
833
836
  }
@@ -857,7 +860,7 @@ async function handleAutoRenameSuspicious(
857
860
  options: SyncCommandOptions,
858
861
  config: Awaited<ReturnType<typeof loadConfig>>,
859
862
  projectRoot: string
860
- ) {
863
+ ): Promise<any[] | undefined> {
861
864
  console.log(chalk.blue("\n📝 Auto-rename suspicious keys analysis:"));
862
865
 
863
866
  // Get existing keys from locale data to check for conflicts
@@ -1008,6 +1011,23 @@ async function handleAutoRenameSuspicious(
1008
1011
  chalk.gray(" Run with --write to apply safe proposals automatically.")
1009
1012
  );
1010
1013
  }
1014
+
1015
+ // Generate diffs for preview mode
1016
+ if (options.diff && report.safeProposals.length > 0) {
1017
+ const mappings = report.safeProposals.map((proposal) => ({
1018
+ from: proposal.originalKey,
1019
+ to: proposal.proposedKey,
1020
+ }));
1021
+
1022
+ const renamer = new KeyRenamer(config, { workspaceRoot: projectRoot });
1023
+ const diffSummary = await renamer.renameBatch(mappings, {
1024
+ write: false,
1025
+ diff: true,
1026
+ allowConflicts: true,
1027
+ });
1028
+
1029
+ return diffSummary.diffs;
1030
+ }
1011
1031
  }
1012
1032
 
1013
1033
  async function handleRewriteShape(
@@ -1,5 +1,7 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
+ import { createRequire } from 'module';
4
+ import { fileURLToPath } from 'url';
3
5
  import chalk from 'chalk';
4
6
  import type { Command } from 'commander';
5
7
  import { loadConfigWithMeta } from '@i18nsmith/core';
@@ -8,6 +10,8 @@ import type { TransformProgress, TransformSummary } from '@i18nsmith/transformer
8
10
  import { printLocaleDiffs, writeLocaleDiffPatches } from '../utils/diff-utils.js';
9
11
  import { applyPreviewFile, writePreviewFile } from '../utils/preview.js';
10
12
  import { CliError, withErrorHandling } from '../utils/errors.js';
13
+ import inquirer from 'inquirer';
14
+ import { detectPackageManager, installDependencies } from '../utils/package-manager.js';
11
15
 
12
16
  interface TransformOptions {
13
17
  config?: string;
@@ -32,6 +36,7 @@ const collectTargetPatterns = (value: string | string[], previous: string[]) =>
32
36
  return [...previous, ...tokens];
33
37
  };
34
38
 
39
+ /* Re-enabled after framework migration stabilization */
35
40
  function printTransformSummary(summary: TransformSummary) {
36
41
  const counts = summary.candidates.reduce(
37
42
  (acc, c) => {
@@ -68,7 +73,8 @@ function printTransformSummary(summary: TransformSummary) {
68
73
 
69
74
  console.table(preview);
70
75
 
71
- const pending = summary.candidates.filter((candidate) => candidate.status === 'pending').length;
76
+ const pending = summary.candidateStats?.pending
77
+ ?? summary.candidates.filter((candidate) => candidate.status === 'pending').length;
72
78
  if (pending > 0) {
73
79
  console.log(
74
80
  chalk.yellow(
@@ -97,6 +103,13 @@ function printTransformSummary(summary: TransformSummary) {
97
103
  console.log(chalk.yellow('Skipped items:'));
98
104
  summary.skippedFiles.forEach((item) => console.log(` • ${item.filePath}: ${item.reason}`));
99
105
  }
106
+
107
+ if (summary.skippedReasons && Object.keys(summary.skippedReasons).length) {
108
+ console.log(chalk.yellow('Skipped reasons:'));
109
+ (Object.entries(summary.skippedReasons) as [string, number][])
110
+ .sort((a, b) => b[1] - a[1])
111
+ .forEach(([reason, count]) => console.log(` • ${reason}: ${count}`));
112
+ }
100
113
  }
101
114
 
102
115
  function createProgressLogger() {
@@ -194,7 +207,7 @@ export function registerTransform(program: Command) {
194
207
  .option('--apply-preview <path>', 'Apply a previously saved transform preview JSON file safely')
195
208
  .action(
196
209
  withErrorHandling(async (options: TransformOptions) => {
197
- if (options.applyPreview) {
210
+ if (options.applyPreview) {
198
211
  await applyPreviewFile('transform', options.applyPreview);
199
212
  return;
200
213
  }
@@ -225,6 +238,45 @@ export function registerTransform(program: Command) {
225
238
  console.log(chalk.gray(`Config found at ${path.relative(cwd, configPath)}`));
226
239
  console.log(chalk.gray(`Using project root: ${projectRoot}\n`));
227
240
  }
241
+
242
+ // Proactively check if Vue files are targeted but the parser is missing
243
+ const includesVue = config.include?.some(pattern => pattern.includes('.vue')) ?? false;
244
+ let isVueParserAvailable = false;
245
+ try {
246
+ const require = createRequire(import.meta.url);
247
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
248
+ require.resolve('vue-eslint-parser', { paths: [projectRoot, moduleDir] });
249
+ isVueParserAvailable = true;
250
+ } catch {
251
+ isVueParserAvailable = false;
252
+ }
253
+
254
+ if (includesVue && !isVueParserAvailable) {
255
+ console.log(chalk.yellow('⚠️ Vue files detected but "vue-eslint-parser" is not installed.'));
256
+ console.log(chalk.yellow(' Verification of Vue templates might be incomplete or fail.'));
257
+
258
+ if (process.stdout.isTTY) {
259
+ const { install } = await inquirer.prompt<{ install: boolean }>([
260
+ {
261
+ type: 'confirm',
262
+ name: 'install',
263
+ message: 'Do you want to install "vue-eslint-parser" (dev dependency) now?',
264
+ default: true,
265
+ },
266
+ ]);
267
+
268
+ if (install) {
269
+ const pm = await detectPackageManager(projectRoot);
270
+ const cmd = pm === 'npm' ? 'npm install --save-dev vue-eslint-parser' : `${pm} add -D vue-eslint-parser`;
271
+ console.log(chalk.gray(`> ${cmd}`));
272
+ // We use installDependencies helper but need to pass dev flag args if not flexible
273
+ // The helper seems simple: const args = manager === 'npm' ? ['install', ...deps] : ['add', ...deps];
274
+ // So for dev we need to include -D in deps
275
+ await installDependencies(pm, ['-D', 'vue-eslint-parser'], projectRoot);
276
+ console.log(chalk.green('✔ Installed. Continuing...'));
277
+ }
278
+ }
279
+ }
228
280
 
229
281
  const transformer = new Transformer(config, { workspaceRoot: projectRoot });
230
282
  const progressLogger = createProgressLogger();
@@ -78,6 +78,8 @@ export async function handleCsvExport(options: TranslateCommandOptions): Promise
78
78
  const plan = await translationService.buildPlan({
79
79
  locales: options.locales,
80
80
  force: options.force,
81
+ // Export should represent what the UI calls "missing" (usually includes empty strings)
82
+ treatEmptyAsMissing: true,
81
83
  });
82
84
 
83
85
  if (!plan.totalTasks) {
@@ -165,7 +167,6 @@ export async function handleCsvImport(options: TranslateCommandOptions): Promise
165
167
  // Parse header
166
168
  const headerFields = parseCsvLine(lines[0]);
167
169
  const keyIdx = headerFields.indexOf('key');
168
- const sourceLocaleIdx = headerFields.indexOf('sourceLocale');
169
170
  const sourceValueIdx = headerFields.indexOf('sourceValue');
170
171
  const targetLocaleIdx = headerFields.indexOf('targetLocale');
171
172
  const translatedValueIdx = headerFields.indexOf('translatedValue');
package/src/e2e.test.ts CHANGED
@@ -281,13 +281,13 @@ describe('E2E Fixture Tests', () => {
281
281
  const renameMap = parseRenameMap(mapContents);
282
282
  const helloKey = renameMap['Hello World'];
283
283
  expect(helloKey).toBeDefined();
284
- const submitKey = renameMap['buttons.submit'];
285
- expect(submitKey).toBeDefined();
284
+ const saveKey = renameMap['Save'];
285
+ expect(saveKey).toBeDefined();
286
286
 
287
287
  expect(enLocale).toHaveProperty(helloKey!);
288
288
  expect(frLocale).toHaveProperty(helloKey!);
289
- expect(enLocale).toHaveProperty(submitKey!);
290
- expect(frLocale).toHaveProperty(submitKey!);
289
+ expect(enLocale).toHaveProperty(saveKey!);
290
+ expect(frLocale).toHaveProperty(saveKey!);
291
291
 
292
292
  const sourceFile = await fs.readFile(path.join(fixtureDir, 'src', 'BadKeys.tsx'), 'utf8');
293
293
  expect(sourceFile).toContain(`t('${helloKey!}')`);
@@ -1,11 +1,11 @@
1
1
  {
2
- "Hello World": "Hello World",
3
- "Welcome to our app!": "Welcome to our app!",
4
- "Click Here": "Click Here",
5
- "Save": "Save",
6
- "The Quick Brown Fox": "The Quick Brown Fox",
7
- "When to use this feature:": "When to use this feature:",
2
+ "common.badkeys.buttons-submit.3b94a6": "Submit",
3
+ "common.badkeys.click-here.788c1e": "Click Here",
4
+ "common.badkeys.hello-world.66bf53": "Hello World",
5
+ "common.badkeys.save.a495bf": "Save",
6
+ "common.badkeys.welcome-to-our-app.a53bf0": "Welcome to our app!",
7
+ "common.title": "Application Title",
8
8
  "proper.namespaced.key": "This is a properly namespaced key",
9
- "buttons.submit": "Submit",
10
- "common.title": "Application Title"
9
+ "The Quick Brown Fox": "The Quick Brown Fox",
10
+ "When to use this feature:": "When to use this feature:"
11
11
  }
@@ -1,11 +1,11 @@
1
1
  {
2
- "Hello World": "Bonjour le monde",
3
- "Welcome to our app!": "Bienvenue dans notre application!",
4
- "Click Here": "Cliquez ici",
5
- "Save": "Sauvegarder",
6
- "The Quick Brown Fox": "Le Renard Brun Rapide",
7
- "When to use this feature:": "Quand utiliser cette fonctionnalité:",
2
+ "common.badkeys.buttons-submit.3b94a6": "Soumettre",
3
+ "common.badkeys.click-here.788c1e": "Cliquez ici",
4
+ "common.badkeys.hello-world.66bf53": "Bonjour le monde",
5
+ "common.badkeys.save.a495bf": "Sauvegarder",
6
+ "common.badkeys.welcome-to-our-app.a53bf0": "Bienvenue dans notre application!",
7
+ "common.title": "Titre de l'application",
8
8
  "proper.namespaced.key": "Ceci est une clé correctement nommée",
9
- "buttons.submit": "Soumettre",
10
- "common.title": "Titre de l'application"
9
+ "The Quick Brown Fox": "Le Renard Brun Rapide",
10
+ "When to use this feature:": "Quand utiliser cette fonctionnalité:"
11
11
  }