i18nsmith 0.2.1 → 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.
Files changed (53) hide show
  1. package/dist/commands/audit.d.ts.map +1 -1
  2. package/dist/commands/backup.d.ts.map +1 -1
  3. package/dist/commands/check.d.ts +25 -0
  4. package/dist/commands/check.d.ts.map +1 -1
  5. package/dist/commands/config.d.ts.map +1 -1
  6. package/dist/commands/debug-patterns.d.ts.map +1 -1
  7. package/dist/commands/diagnose.d.ts.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/install-hooks.d.ts.map +1 -1
  10. package/dist/commands/preflight.d.ts.map +1 -1
  11. package/dist/commands/rename.d.ts.map +1 -1
  12. package/dist/commands/review.d.ts.map +1 -1
  13. package/dist/commands/scaffold-adapter.d.ts.map +1 -1
  14. package/dist/commands/scan.d.ts.map +1 -1
  15. package/dist/commands/sync.d.ts +1 -1
  16. package/dist/commands/sync.d.ts.map +1 -1
  17. package/dist/commands/transform.d.ts.map +1 -1
  18. package/dist/commands/translate/index.d.ts.map +1 -1
  19. package/dist/index.js +2536 -107783
  20. package/dist/rename-suspicious.test.d.ts +2 -0
  21. package/dist/rename-suspicious.test.d.ts.map +1 -0
  22. package/dist/utils/diff-utils.d.ts +5 -0
  23. package/dist/utils/diff-utils.d.ts.map +1 -1
  24. package/dist/utils/errors.d.ts +8 -0
  25. package/dist/utils/errors.d.ts.map +1 -0
  26. package/dist/utils/locale-audit.d.ts +39 -0
  27. package/dist/utils/locale-audit.d.ts.map +1 -0
  28. package/dist/utils/preview.d.ts.map +1 -1
  29. package/package.json +5 -5
  30. package/src/commands/audit.ts +18 -209
  31. package/src/commands/backup.ts +67 -63
  32. package/src/commands/check.ts +119 -68
  33. package/src/commands/config.ts +117 -95
  34. package/src/commands/debug-patterns.ts +25 -22
  35. package/src/commands/diagnose.ts +29 -26
  36. package/src/commands/init.ts +84 -79
  37. package/src/commands/install-hooks.ts +18 -15
  38. package/src/commands/preflight.ts +21 -13
  39. package/src/commands/rename.ts +86 -81
  40. package/src/commands/review.ts +81 -78
  41. package/src/commands/scaffold-adapter.ts +8 -4
  42. package/src/commands/scan.ts +61 -58
  43. package/src/commands/sync.ts +640 -203
  44. package/src/commands/transform.ts +46 -18
  45. package/src/commands/translate/index.ts +7 -4
  46. package/src/e2e.test.ts +34 -14
  47. package/src/integration.test.ts +86 -0
  48. package/src/rename-suspicious.test.ts +124 -0
  49. package/src/utils/diff-utils.ts +6 -0
  50. package/src/utils/errors.ts +34 -0
  51. package/src/utils/locale-audit.ts +219 -0
  52. package/src/utils/preview.test.ts +43 -0
  53. package/src/utils/preview.ts +2 -8
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import inquirer from 'inquirer';
5
5
  import type { Command } from 'commander';
6
6
  import { loadConfigWithMeta, Scanner, type ScanCandidate, type ScanSummary } from '@i18nsmith/core';
7
+ import { CliError, withErrorHandling } from '../utils/errors.js';
7
8
 
8
9
  interface ReviewCommandOptions {
9
10
  config?: string;
@@ -133,94 +134,96 @@ export function registerReview(program: Command) {
133
134
  .option('--json', 'Print raw bucket data as JSON', false)
134
135
  .option('--limit <n>', 'Limit the number of items per session (default: 20)', (value) => parseInt(value, 10))
135
136
  .option('--scan-calls', 'Include translation call arguments', false)
136
- .action(async (options: ReviewCommandOptions) => {
137
- try {
138
- const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
139
- const scanner = new Scanner(config, { workspaceRoot: projectRoot });
140
- const summary = scanner.scan({ scanCalls: options.scanCalls }) as BucketedScanSummary;
141
- const buckets = summary.buckets ?? {};
142
- const needsReview = buckets.needsReview ?? [];
143
- const skipped = buckets.skipped ?? [];
144
-
145
- if (options.json) {
146
- console.log(JSON.stringify({ needsReview, skipped }, null, 2));
147
- return;
148
- }
149
-
150
- if (!needsReview.length) {
151
- console.log(chalk.green('No borderline candidates detected.'));
152
- const reasons = summarizeSkipReasons(skipped);
153
- if (reasons.length) {
154
- console.log(chalk.gray('Most common skip reasons:'));
155
- reasons.forEach((line) => console.log(chalk.gray(` • ${line}`)));
137
+ .action(
138
+ withErrorHandling(async (options: ReviewCommandOptions) => {
139
+ try {
140
+ const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
141
+ const scanner = new Scanner(config, { workspaceRoot: projectRoot });
142
+ const summary = scanner.scan({ scanCalls: options.scanCalls }) as BucketedScanSummary;
143
+ const buckets = summary.buckets ?? {};
144
+ const needsReview = buckets.needsReview ?? [];
145
+ const skipped = buckets.skipped ?? [];
146
+
147
+ if (options.json) {
148
+ console.log(JSON.stringify({ needsReview, skipped }, null, 2));
149
+ return;
156
150
  }
157
- return;
158
- }
159
151
 
160
- if (!process.stdout.isTTY || process.env.CI === 'true') {
161
- console.log(chalk.red('Interactive review requires a TTY. Use --json for non-interactive output.'));
162
- process.exitCode = 1;
163
- return;
164
- }
152
+ if (!needsReview.length) {
153
+ console.log(chalk.green('No borderline candidates detected.'));
154
+ const reasons = summarizeSkipReasons(skipped);
155
+ if (reasons.length) {
156
+ console.log(chalk.gray('Most common skip reasons:'));
157
+ reasons.forEach((line) => console.log(chalk.gray(` • ${line}`)));
158
+ }
159
+ return;
160
+ }
165
161
 
166
- const limit = normalizeLimit(options.limit);
167
- const queue = needsReview.slice(0, limit);
168
- console.log(
169
- chalk.blue(
170
- `Reviewing ${queue.length} of ${needsReview.length} candidate${needsReview.length === 1 ? '' : 's'} (limit=${limit}).`
171
- )
172
- );
173
-
174
- const allowPatterns: string[] = [];
175
- const denyPatterns: string[] = [];
176
-
177
- for (const candidate of queue) {
178
- printCandidate(candidate);
179
- const action = await promptAction();
180
- if (action === 'stop') {
181
- break;
162
+ if (!process.stdout.isTTY || process.env.CI === 'true') {
163
+ console.log(chalk.red('Interactive review requires a TTY. Use --json for non-interactive output.'));
164
+ process.exitCode = 1;
165
+ return;
182
166
  }
183
- if (action === 'skip') {
184
- continue;
167
+
168
+ const limit = normalizeLimit(options.limit);
169
+ const queue = needsReview.slice(0, limit);
170
+ console.log(
171
+ chalk.blue(
172
+ `Reviewing ${queue.length} of ${needsReview.length} candidate${needsReview.length === 1 ? '' : 's'} (limit=${limit}).`
173
+ )
174
+ );
175
+
176
+ const allowPatterns: string[] = [];
177
+ const denyPatterns: string[] = [];
178
+
179
+ for (const candidate of queue) {
180
+ printCandidate(candidate);
181
+ const action = await promptAction();
182
+ if (action === 'stop') {
183
+ break;
184
+ }
185
+ if (action === 'skip') {
186
+ continue;
187
+ }
188
+ const pattern = literalToRegexPattern(candidate.text);
189
+ if (action === 'allow') {
190
+ allowPatterns.push(pattern);
191
+ console.log(chalk.green(` → Queued ${pattern} for allowPatterns`));
192
+ } else if (action === 'deny') {
193
+ denyPatterns.push(pattern);
194
+ console.log(chalk.yellow(` → Queued ${pattern} for denyPatterns`));
195
+ }
185
196
  }
186
- const pattern = literalToRegexPattern(candidate.text);
187
- if (action === 'allow') {
188
- allowPatterns.push(pattern);
189
- console.log(chalk.green(` → Queued ${pattern} for allowPatterns`));
190
- } else if (action === 'deny') {
191
- denyPatterns.push(pattern);
192
- console.log(chalk.yellow(` → Queued ${pattern} for denyPatterns`));
197
+
198
+ if (!allowPatterns.length && !denyPatterns.length) {
199
+ console.log(chalk.gray('No config changes requested.'));
200
+ return;
193
201
  }
194
- }
195
202
 
196
- if (!allowPatterns.length && !denyPatterns.length) {
197
- console.log(chalk.gray('No config changes requested.'));
198
- return;
199
- }
203
+ const { allowAdded, denyAdded, wrote } = await writeExtractionOverrides(
204
+ configPath,
205
+ allowPatterns,
206
+ denyPatterns
207
+ );
200
208
 
201
- const { allowAdded, denyAdded, wrote } = await writeExtractionOverrides(
202
- configPath,
203
- allowPatterns,
204
- denyPatterns
205
- );
209
+ if (!wrote) {
210
+ console.log(chalk.gray('Patterns already existed; config unchanged.'));
211
+ return;
212
+ }
206
213
 
207
- if (!wrote) {
208
- console.log(chalk.gray('Patterns already existed; config unchanged.'));
209
- return;
214
+ console.log(
215
+ chalk.green(
216
+ `Updated ${relativize(configPath)} (${allowAdded} allow, ${denyAdded} deny pattern${
217
+ allowAdded + denyAdded === 1 ? '' : 's'
218
+ } added).`
219
+ )
220
+ );
221
+ } catch (error) {
222
+ const message = error instanceof Error ? error.message : String(error);
223
+ throw new CliError(`Review failed: ${message}`);
210
224
  }
211
-
212
- console.log(
213
- chalk.green(
214
- `Updated ${relativize(configPath)} (${allowAdded} allow, ${denyAdded} deny pattern${
215
- allowAdded + denyAdded === 1 ? '' : 's'
216
- } added).`
217
- )
218
- );
219
- } catch (error) {
220
- console.error(chalk.red('Review failed:'), (error as Error).message);
221
- process.exitCode = 1;
222
- }
223
- });
225
+ })
226
+ );
224
227
  }
225
228
 
226
229
  function relativize(filePath: string): string {
@@ -2,10 +2,11 @@ import { Command } from 'commander';
2
2
  import inquirer from 'inquirer';
3
3
  import chalk from 'chalk';
4
4
  import { diagnoseWorkspace, loadConfig } from '@i18nsmith/core';
5
- import { scaffoldTranslationContext, scaffoldI18next, ScaffoldResult } from '../utils/scaffold.js';
5
+ import { scaffoldTranslationContext, scaffoldI18next } from '../utils/scaffold.js';
6
6
  import { readPackageJson, hasDependency } from '../utils/pkg.js';
7
7
  import { detectPackageManager, installDependencies } from '../utils/package-manager.js';
8
8
  import { maybeInjectProvider } from '../utils/provider-injector.js';
9
+ import { CliError, withErrorHandling } from '../utils/errors.js';
9
10
 
10
11
  interface ScaffoldCommandOptions {
11
12
  type?: 'custom' | 'react-i18next';
@@ -44,7 +45,8 @@ export function registerScaffoldAdapter(program: Command) {
44
45
  .option('--install-deps', 'Automatically install adapter dependencies when missing', false)
45
46
  .option('--dry-run', 'Preview provider injection changes without modifying files', false)
46
47
  .option('--no-skip-if-detected', 'Force scaffolding even if existing adapters/providers are detected')
47
- .action(async (options: ScaffoldCommandOptions) => {
48
+ .action(
49
+ withErrorHandling(async (options: ScaffoldCommandOptions) => {
48
50
  console.log(chalk.blue('Scaffolding translation resources...'));
49
51
 
50
52
  const answers = await inquirer.prompt<ScaffoldAnswers>([
@@ -221,9 +223,11 @@ export function registerScaffoldAdapter(program: Command) {
221
223
  }
222
224
  }
223
225
  } catch (error) {
224
- console.error(chalk.red('Failed to scaffold adapter:'), (error as Error).message);
226
+ const message = error instanceof Error ? error.message : String(error);
227
+ throw new CliError(`Failed to scaffold adapter: ${message}`);
225
228
  }
226
- });
229
+ })
230
+ );
227
231
  }
228
232
 
229
233
  async function detectExistingRuntime(): Promise<string | null> {
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import type { Command } from 'commander';
5
5
  import { loadConfigWithMeta, Scanner } from '@i18nsmith/core';
6
6
  import type { ScanCandidate } from '@i18nsmith/core';
7
+ import { CliError, withErrorHandling } from '../utils/errors.js';
7
8
 
8
9
  interface ScanOptions {
9
10
  config?: string;
@@ -54,72 +55,74 @@ export function registerScan(program: Command) {
54
55
  .option('--list-files', 'List the files that were scanned', false)
55
56
  .option('--include <patterns...>', 'Override include globs from config (comma or space separated)', collectTargetPatterns, [])
56
57
  .option('--exclude <patterns...>', 'Override exclude globs from config (comma or space separated)', collectTargetPatterns, [])
57
- .action(async (options: ScanOptions) => {
58
- console.log(chalk.blue('Starting scan...'));
58
+ .action(
59
+ withErrorHandling(async (options: ScanOptions) => {
60
+ console.log(chalk.blue('Starting scan...'));
59
61
 
60
- try {
61
- const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
62
-
63
- // Inform user if config was found in a parent directory
64
- const cwd = process.cwd();
65
- if (projectRoot !== cwd) {
66
- console.log(chalk.gray(`Config found at ${path.relative(cwd, configPath)}`));
67
- console.log(chalk.gray(`Using project root: ${projectRoot}\n`));
68
- }
69
-
70
- if (options.include?.length) {
71
- config.include = options.include;
72
- }
73
- if (options.exclude?.length) {
74
- config.exclude = options.exclude;
75
- }
76
- const scanner = new Scanner(config, { workspaceRoot: projectRoot });
77
- const summary = scanner.scan();
62
+ try {
63
+ const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
78
64
 
79
- if (options.report) {
80
- const outputPath = path.resolve(process.cwd(), options.report);
81
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
82
- await fs.writeFile(outputPath, JSON.stringify(summary, null, 2));
83
- console.log(chalk.green(`Scan report written to ${outputPath}`));
84
- }
65
+ // Inform user if config was found in a parent directory
66
+ const cwd = process.cwd();
67
+ if (projectRoot !== cwd) {
68
+ console.log(chalk.gray(`Config found at ${path.relative(cwd, configPath)}`));
69
+ console.log(chalk.gray(`Using project root: ${projectRoot}\n`));
70
+ }
85
71
 
86
- if (options.json) {
87
- console.log(JSON.stringify(summary, null, 2));
88
- return;
89
- }
72
+ if (options.include?.length) {
73
+ config.include = options.include;
74
+ }
75
+ if (options.exclude?.length) {
76
+ config.exclude = options.exclude;
77
+ }
78
+ const scanner = new Scanner(config, { workspaceRoot: projectRoot });
79
+ const summary = scanner.scan();
90
80
 
91
- console.log(
92
- chalk.green(
93
- `Scanned ${summary.filesScanned} file${summary.filesScanned === 1 ? '' : 's'} and found ${summary.candidates.length} candidate${summary.candidates.length === 1 ? '' : 's'}.`
94
- )
95
- );
81
+ if (options.report) {
82
+ const outputPath = path.resolve(process.cwd(), options.report);
83
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
84
+ await fs.writeFile(outputPath, JSON.stringify(summary, null, 2));
85
+ console.log(chalk.green(`Scan report written to ${outputPath}`));
86
+ }
96
87
 
97
- if (summary.candidates.length === 0) {
98
- console.log(chalk.yellow('No translatable strings found.'));
99
- return;
100
- }
88
+ if (options.json) {
89
+ console.log(JSON.stringify(summary, null, 2));
90
+ return;
91
+ }
92
+
93
+ console.log(
94
+ chalk.green(
95
+ `Scanned ${summary.filesScanned} file${summary.filesScanned === 1 ? '' : 's'} and found ${summary.candidates.length} candidate${summary.candidates.length === 1 ? '' : 's'}.`
96
+ )
97
+ );
98
+
99
+ if (summary.candidates.length === 0) {
100
+ console.log(chalk.yellow('No translatable strings found.'));
101
+ return;
102
+ }
101
103
 
102
- printCandidateTable(summary.candidates);
104
+ printCandidateTable(summary.candidates);
103
105
 
104
- if (options.listFiles) {
105
- if (summary.filesExamined.length === 0) {
106
- console.log(chalk.yellow('No files matched the configured include/exclude patterns.'));
107
- } else {
108
- console.log(chalk.blue(`Files scanned (${summary.filesExamined.length}):`));
109
- const preview = summary.filesExamined.slice(0, 200);
110
- preview.forEach((file) => console.log(` • ${file}`));
111
- if (summary.filesExamined.length > preview.length) {
112
- console.log(
113
- chalk.gray(
114
- ` ...and ${summary.filesExamined.length - preview.length} more. Use --target to narrow the list.`
115
- )
116
- );
106
+ if (options.listFiles) {
107
+ if (summary.filesExamined.length === 0) {
108
+ console.log(chalk.yellow('No files matched the configured include/exclude patterns.'));
109
+ } else {
110
+ console.log(chalk.blue(`Files scanned (${summary.filesExamined.length}):`));
111
+ const preview = summary.filesExamined.slice(0, 200);
112
+ preview.forEach((file) => console.log(` • ${file}`));
113
+ if (summary.filesExamined.length > preview.length) {
114
+ console.log(
115
+ chalk.gray(
116
+ ` ...and ${summary.filesExamined.length - preview.length} more. Use --target to narrow the list.`
117
+ )
118
+ );
119
+ }
117
120
  }
118
121
  }
122
+ } catch (error) {
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ throw new CliError(`Scan failed: ${message}`);
119
125
  }
120
- } catch (error) {
121
- console.error(chalk.red('Scan failed:'), (error as Error).message);
122
- process.exitCode = 1;
123
- }
124
- });
126
+ })
127
+ );
125
128
  }