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.
- 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 +2536 -107783
- 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/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 +46 -18
- package/src/commands/translate/index.ts +7 -4
- package/src/e2e.test.ts +34 -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 +43 -0
- package/src/utils/preview.ts +2 -8
package/src/commands/review.ts
CHANGED
|
@@ -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(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
203
|
+
const { allowAdded, denyAdded, wrote } = await writeExtractionOverrides(
|
|
204
|
+
configPath,
|
|
205
|
+
allowPatterns,
|
|
206
|
+
denyPatterns
|
|
207
|
+
);
|
|
200
208
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
);
|
|
209
|
+
if (!wrote) {
|
|
210
|
+
console.log(chalk.gray('Patterns already existed; config unchanged.'));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
206
213
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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> {
|
package/src/commands/scan.ts
CHANGED
|
@@ -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(
|
|
58
|
-
|
|
58
|
+
.action(
|
|
59
|
+
withErrorHandling(async (options: ScanOptions) => {
|
|
60
|
+
console.log(chalk.blue('Starting scan...'));
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
+
printCandidateTable(summary.candidates);
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
}
|
|
121
|
-
|
|
122
|
-
process.exitCode = 1;
|
|
123
|
-
}
|
|
124
|
-
});
|
|
126
|
+
})
|
|
127
|
+
);
|
|
125
128
|
}
|