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.
- package/build.mjs +1 -1
- package/dist/commands/detect.d.ts +3 -0
- package/dist/commands/detect.d.ts.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/rename.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/commands/translate/csv-handler.d.ts.map +1 -1
- package/dist/index.cjs +47711 -42734
- package/dist/index.d.ts.map +1 -1
- package/dist/test-helpers/ensure-cli-built.d.ts.map +1 -1
- package/dist/utils/adapter-preflight.d.ts +10 -0
- package/dist/utils/adapter-preflight.d.ts.map +1 -0
- package/i18n.config.json +14 -0
- package/package.json +4 -2
- package/src/commands/detect.ts +342 -0
- package/src/commands/init.test.ts +208 -1
- package/src/commands/init.ts +472 -195
- package/src/commands/rename.ts +13 -0
- package/src/commands/review.ts +1 -1
- package/src/commands/scan.ts +4 -1
- package/src/commands/sync.ts +23 -3
- package/src/commands/transform.ts +54 -2
- package/src/commands/translate/csv-handler.ts +2 -1
- package/src/e2e.test.ts +4 -4
- package/src/fixtures/suspicious-keys/locales/en.json +8 -8
- package/src/fixtures/suspicious-keys/locales/fr.json +8 -8
- package/src/fixtures/suspicious-keys/preview.json +419 -0
- package/src/fixtures/suspicious-keys/src/BadKeys.tsx.backup +19 -0
- package/src/index.ts +3 -1
- package/src/integration.test.ts +2 -6
- package/src/rename-suspicious.test.ts +3 -3
- package/src/test-helpers/ensure-cli-built.ts +18 -0
- package/src/utils/adapter-preflight.ts +53 -0
- package/test.vue +33 -0
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBpC,eAAO,MAAM,OAAO,SAAgB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ensure-cli-built.d.ts","sourceRoot":"","sources":["../../src/test-helpers/ensure-cli-built.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ensure-cli-built.d.ts","sourceRoot":"","sources":["../../src/test-helpers/ensure-cli-built.ts"],"names":[],"mappings":"AAaA,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCnE"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter preflight utilities for CLI commands.
|
|
3
|
+
* Validates framework adapter dependencies before write operations.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Run preflight checks for all registered framework adapters.
|
|
7
|
+
* Throws an error if any required dependencies are missing.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runAdapterPreflight(): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=adapter-preflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-preflight.d.ts","sourceRoot":"","sources":["../../src/utils/adapter-preflight.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAeH;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CA8BzD"}
|
package/i18n.config.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sourceLanguage": "en",
|
|
3
|
+
"targetLanguages": ["es", "fr"],
|
|
4
|
+
"localesDir": "locales",
|
|
5
|
+
"extraction": {
|
|
6
|
+
"minTextLength": 3,
|
|
7
|
+
"minLetterCount": 2,
|
|
8
|
+
"minLetterRatio": 0.5
|
|
9
|
+
},
|
|
10
|
+
"translationAdapter": {
|
|
11
|
+
"module": "@i18nsmith/translation",
|
|
12
|
+
"hookName": "t"
|
|
13
|
+
}
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18nsmith",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI for i18nsmith",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
"chalk": "^5.0.0",
|
|
25
25
|
"commander": "^11.0.0",
|
|
26
26
|
"diff": "^8.0.2",
|
|
27
|
+
"eslint": "^8.57.1",
|
|
27
28
|
"fast-glob": "^3.3.3",
|
|
28
29
|
"inquirer": "^9.0.0",
|
|
29
30
|
"p-limit": "^7.2.0",
|
|
30
31
|
"p-retry": "^7.1.0",
|
|
31
|
-
"ts-morph": "^21.0.0"
|
|
32
|
+
"ts-morph": "^21.0.0",
|
|
33
|
+
"vue-eslint-parser": "^10.2.0"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
34
36
|
"@i18nsmith/core": "workspace:*",
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import type { Command } from 'commander';
|
|
5
|
+
import {
|
|
6
|
+
ProjectIntelligenceService,
|
|
7
|
+
type ProjectIntelligence,
|
|
8
|
+
type ConfidenceLevel,
|
|
9
|
+
} from '@i18nsmith/core';
|
|
10
|
+
import { withErrorHandling } from '../utils/errors.js';
|
|
11
|
+
|
|
12
|
+
interface DetectOptions {
|
|
13
|
+
json?: boolean;
|
|
14
|
+
report?: string;
|
|
15
|
+
verbose?: boolean;
|
|
16
|
+
showConfig?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get colored confidence indicator based on level
|
|
21
|
+
*/
|
|
22
|
+
function getConfidenceIndicator(level: ConfidenceLevel): string {
|
|
23
|
+
switch (level) {
|
|
24
|
+
case 'high':
|
|
25
|
+
return chalk.green('●');
|
|
26
|
+
case 'medium':
|
|
27
|
+
return chalk.yellow('●');
|
|
28
|
+
case 'low':
|
|
29
|
+
return chalk.red('●');
|
|
30
|
+
case 'uncertain':
|
|
31
|
+
default:
|
|
32
|
+
return chalk.gray('○');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format percentage for display
|
|
38
|
+
*/
|
|
39
|
+
function formatPercent(value: number): string {
|
|
40
|
+
return `${Math.round(value * 100)}%`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Print framework detection results
|
|
45
|
+
*/
|
|
46
|
+
function printFrameworkDetection(result: ProjectIntelligence, verbose: boolean) {
|
|
47
|
+
console.log(chalk.blue('\n📦 Framework Detection'));
|
|
48
|
+
|
|
49
|
+
const { framework, confidence } = result;
|
|
50
|
+
const level = getConfidenceLevel(confidence.framework);
|
|
51
|
+
const indicator = getConfidenceIndicator(level);
|
|
52
|
+
|
|
53
|
+
if (framework.type === 'unknown') {
|
|
54
|
+
console.log(chalk.yellow(' • No framework detected'));
|
|
55
|
+
} else {
|
|
56
|
+
console.log(` ${indicator} Framework: ${chalk.bold(framework.type)}`);
|
|
57
|
+
|
|
58
|
+
if (framework.adapter) {
|
|
59
|
+
console.log(` i18n Adapter: ${chalk.cyan(framework.adapter)}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (framework.routerType && framework.routerType !== 'unknown') {
|
|
63
|
+
console.log(` Router: ${chalk.cyan(framework.routerType)} router`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(` Confidence: ${formatPercent(confidence.framework)}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (verbose && framework.evidence.length > 0) {
|
|
70
|
+
console.log(chalk.gray('\n Evidence:'));
|
|
71
|
+
for (const evidence of framework.evidence) {
|
|
72
|
+
const sourceLabel = evidence.type === 'package' ? '📦' : '📄';
|
|
73
|
+
console.log(chalk.gray(` ${sourceLabel} ${evidence.description} (weight: ${evidence.weight})`));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get confidence level from score
|
|
80
|
+
*/
|
|
81
|
+
function getConfidenceLevel(score: number): ConfidenceLevel {
|
|
82
|
+
if (score >= 0.8) return 'high';
|
|
83
|
+
if (score >= 0.5) return 'medium';
|
|
84
|
+
if (score >= 0.3) return 'low';
|
|
85
|
+
return 'uncertain';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Print file patterns detection results
|
|
90
|
+
*/
|
|
91
|
+
function printFilePatterns(result: ProjectIntelligence, verbose: boolean) {
|
|
92
|
+
console.log(chalk.blue('\n📂 File Patterns'));
|
|
93
|
+
|
|
94
|
+
const { filePatterns, confidence } = result;
|
|
95
|
+
const level = getConfidenceLevel(confidence.filePatterns);
|
|
96
|
+
const indicator = getConfidenceIndicator(level);
|
|
97
|
+
|
|
98
|
+
console.log(` ${indicator} Source directories:`);
|
|
99
|
+
for (const dir of filePatterns.sourceDirectories) {
|
|
100
|
+
console.log(` • ${chalk.cyan(dir)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const extensions: string[] = [];
|
|
104
|
+
if (filePatterns.hasTypeScript) extensions.push('.ts', '.tsx');
|
|
105
|
+
if (filePatterns.hasJsx && !filePatterns.hasTypeScript) extensions.push('.jsx');
|
|
106
|
+
if (filePatterns.hasVue) extensions.push('.vue');
|
|
107
|
+
if (filePatterns.hasSvelte) extensions.push('.svelte');
|
|
108
|
+
if (extensions.length === 0) extensions.push('.js');
|
|
109
|
+
|
|
110
|
+
console.log(` ${indicator} Extensions: ${extensions.map((e: string) => chalk.cyan(e)).join(', ')}`);
|
|
111
|
+
console.log(` Files: ~${filePatterns.sourceFileCount} source files`);
|
|
112
|
+
console.log(` Confidence: ${formatPercent(confidence.filePatterns)}`);
|
|
113
|
+
|
|
114
|
+
if (verbose) {
|
|
115
|
+
console.log(chalk.gray('\n Suggested include patterns:'));
|
|
116
|
+
for (const pattern of filePatterns.include) {
|
|
117
|
+
console.log(chalk.gray(` + ${pattern}`));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(chalk.gray('\n Suggested exclude patterns:'));
|
|
121
|
+
for (const pattern of filePatterns.exclude.slice(0, 5)) {
|
|
122
|
+
console.log(chalk.gray(` - ${pattern}`));
|
|
123
|
+
}
|
|
124
|
+
if (filePatterns.exclude.length > 5) {
|
|
125
|
+
console.log(chalk.gray(` ... and ${filePatterns.exclude.length - 5} more`));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Print locale detection results
|
|
132
|
+
*/
|
|
133
|
+
function printLocaleDetection(result: ProjectIntelligence, verbose: boolean) {
|
|
134
|
+
console.log(chalk.blue('\n🌍 Locale Detection'));
|
|
135
|
+
|
|
136
|
+
const { locales, confidence } = result;
|
|
137
|
+
const level = getConfidenceLevel(confidence.locales);
|
|
138
|
+
const indicator = getConfidenceIndicator(level);
|
|
139
|
+
|
|
140
|
+
if (locales.existingFiles.length === 0) {
|
|
141
|
+
console.log(chalk.yellow(' • No existing locale files detected'));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(` ${indicator} Locales directory: ${chalk.cyan(locales.localesDir || 'N/A')}`);
|
|
146
|
+
console.log(` Format: ${chalk.cyan(locales.format)}`);
|
|
147
|
+
console.log(` Source locale: ${chalk.cyan(locales.sourceLanguage || 'en')}`);
|
|
148
|
+
|
|
149
|
+
const allLangs = [locales.sourceLanguage, ...locales.targetLanguages].filter(Boolean);
|
|
150
|
+
console.log(` Languages: ${allLangs.map((l: string) => chalk.cyan(l)).join(', ')}`);
|
|
151
|
+
console.log(` Total keys: ${locales.existingKeyCount}`);
|
|
152
|
+
console.log(` Confidence: ${formatPercent(confidence.locales)}`);
|
|
153
|
+
|
|
154
|
+
if (verbose && locales.existingFiles.length > 0) {
|
|
155
|
+
console.log(chalk.gray('\n Locale files:'));
|
|
156
|
+
for (const file of locales.existingFiles.slice(0, 10)) {
|
|
157
|
+
const relativePath = file.path.includes('/') ? file.path.split('/').slice(-2).join('/') : file.path;
|
|
158
|
+
console.log(chalk.gray(` • ${file.locale}: ${relativePath} (${file.keyCount} keys)`));
|
|
159
|
+
}
|
|
160
|
+
if (locales.existingFiles.length > 10) {
|
|
161
|
+
console.log(chalk.gray(` ... and ${locales.existingFiles.length - 10} more files`));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Print existing setup detection results
|
|
168
|
+
*/
|
|
169
|
+
function printExistingSetup(result: ProjectIntelligence, verbose: boolean) {
|
|
170
|
+
const { existingSetup, confidence } = result;
|
|
171
|
+
|
|
172
|
+
if (!existingSetup.hasExistingConfig && existingSetup.runtimePackages.length === 0) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(chalk.blue('\n⚙️ Existing i18n Setup'));
|
|
177
|
+
|
|
178
|
+
const level = getConfidenceLevel(confidence.existingSetup);
|
|
179
|
+
const indicator = getConfidenceIndicator(level);
|
|
180
|
+
|
|
181
|
+
if (existingSetup.hasExistingConfig && existingSetup.configPath) {
|
|
182
|
+
console.log(` ${indicator} Existing config: ${chalk.cyan(existingSetup.configPath)}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const pkg of existingSetup.runtimePackages) {
|
|
186
|
+
console.log(` ${indicator} ${chalk.cyan(pkg.name)}@${pkg.version || 'latest'}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (existingSetup.translationUsage) {
|
|
190
|
+
const usage = existingSetup.translationUsage;
|
|
191
|
+
console.log(` Hook: ${usage.hookName}() (${usage.filesWithHooks} files)`);
|
|
192
|
+
console.log(` t() calls: ${usage.translationCalls} across ${usage.filesWithHooks} files`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (verbose && existingSetup.hasI18nProvider && existingSetup.providerPath) {
|
|
196
|
+
console.log(chalk.gray(`\n Provider: ${existingSetup.providerPath}`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Print overall confidence summary
|
|
202
|
+
*/
|
|
203
|
+
function printConfidenceSummary(result: ProjectIntelligence) {
|
|
204
|
+
console.log(chalk.blue('\n📊 Overall Confidence'));
|
|
205
|
+
|
|
206
|
+
const { confidence } = result;
|
|
207
|
+
const indicator = getConfidenceIndicator(confidence.level);
|
|
208
|
+
|
|
209
|
+
console.log(` ${indicator} Overall: ${chalk.bold(formatPercent(confidence.overall))} (${confidence.level})`);
|
|
210
|
+
|
|
211
|
+
// Visual bar
|
|
212
|
+
const barLength = 30;
|
|
213
|
+
const filledLength = Math.round(confidence.overall * barLength);
|
|
214
|
+
const emptyLength = barLength - filledLength;
|
|
215
|
+
const bar = chalk.green('█'.repeat(filledLength)) + chalk.gray('░'.repeat(emptyLength));
|
|
216
|
+
console.log(` ${bar}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Print warnings if any
|
|
221
|
+
*/
|
|
222
|
+
function printWarnings(result: ProjectIntelligence) {
|
|
223
|
+
if (result.warnings.length === 0) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(chalk.blue('\n⚠️ Warnings'));
|
|
228
|
+
for (const warning of result.warnings) {
|
|
229
|
+
const label =
|
|
230
|
+
warning.severity === 'error'
|
|
231
|
+
? chalk.red('ERROR')
|
|
232
|
+
: warning.severity === 'warn'
|
|
233
|
+
? chalk.yellow('WARN')
|
|
234
|
+
: chalk.cyan('INFO');
|
|
235
|
+
console.log(` • [${label}] ${warning.message}`);
|
|
236
|
+
if (warning.suggestion) {
|
|
237
|
+
console.log(chalk.gray(` → ${warning.suggestion}`));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Print suggested configuration
|
|
244
|
+
*/
|
|
245
|
+
function printSuggestedConfig(result: ProjectIntelligence) {
|
|
246
|
+
console.log(chalk.blue('\n📝 Suggested Configuration'));
|
|
247
|
+
|
|
248
|
+
const { suggestedConfig } = result;
|
|
249
|
+
|
|
250
|
+
console.log(` Source Language: ${chalk.cyan(suggestedConfig.sourceLanguage)}`);
|
|
251
|
+
if (suggestedConfig.targetLanguages.length > 0) {
|
|
252
|
+
console.log(` Target Languages: ${suggestedConfig.targetLanguages.map((l: string) => chalk.cyan(l)).join(', ')}`);
|
|
253
|
+
}
|
|
254
|
+
console.log(` Locales Dir: ${chalk.cyan(suggestedConfig.localesDir)}`);
|
|
255
|
+
console.log(` Adapter: ${chalk.cyan(suggestedConfig.translationAdapter.module)}`);
|
|
256
|
+
console.log(` Hook: ${chalk.cyan(suggestedConfig.translationAdapter.hookName)}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Print full config preview as JSON
|
|
261
|
+
*/
|
|
262
|
+
function printConfigPreview(result: ProjectIntelligence) {
|
|
263
|
+
console.log(chalk.blue('\n🔧 Full Config Preview'));
|
|
264
|
+
|
|
265
|
+
const config = {
|
|
266
|
+
localesDir: result.suggestedConfig.localesDir,
|
|
267
|
+
defaultLocale: result.suggestedConfig.sourceLanguage,
|
|
268
|
+
locales: [result.suggestedConfig.sourceLanguage, ...result.suggestedConfig.targetLanguages],
|
|
269
|
+
include: result.suggestedConfig.include,
|
|
270
|
+
exclude: result.suggestedConfig.exclude,
|
|
271
|
+
adapter: result.suggestedConfig.translationAdapter.module,
|
|
272
|
+
translationFunctionName: result.suggestedConfig.translationAdapter.hookName,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
console.log(chalk.gray(JSON.stringify(config, null, 2)));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Print recommendations if any
|
|
280
|
+
*/
|
|
281
|
+
function printRecommendations(result: ProjectIntelligence) {
|
|
282
|
+
if (result.recommendations.length === 0) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log(chalk.blue('\n💡 Recommendations'));
|
|
287
|
+
for (const rec of result.recommendations) {
|
|
288
|
+
console.log(` • ${rec}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function registerDetect(program: Command) {
|
|
293
|
+
program
|
|
294
|
+
.command('detect')
|
|
295
|
+
.description('Detect project configuration automatically')
|
|
296
|
+
.option('--json', 'Output results as JSON', false)
|
|
297
|
+
.option('--report <path>', 'Write JSON report to a file')
|
|
298
|
+
.option('-v, --verbose', 'Show detailed evidence and suggestions', false)
|
|
299
|
+
.option('--show-config', 'Show full suggested configuration', false)
|
|
300
|
+
.action(
|
|
301
|
+
withErrorHandling(async (options: DetectOptions) => {
|
|
302
|
+
const workspaceRoot = process.cwd();
|
|
303
|
+
|
|
304
|
+
console.log(chalk.blue('🔍 Analyzing project...'));
|
|
305
|
+
console.log(chalk.gray(` Working directory: ${workspaceRoot}\n`));
|
|
306
|
+
|
|
307
|
+
const service = new ProjectIntelligenceService();
|
|
308
|
+
const result = await service.analyze({ workspaceRoot });
|
|
309
|
+
|
|
310
|
+
// JSON output
|
|
311
|
+
if (options.json) {
|
|
312
|
+
console.log(JSON.stringify(result, null, 2));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Write report file
|
|
317
|
+
if (options.report) {
|
|
318
|
+
const outputPath = path.resolve(workspaceRoot, options.report);
|
|
319
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
320
|
+
await fs.writeFile(outputPath, JSON.stringify(result, null, 2));
|
|
321
|
+
console.log(chalk.green(`Report written to ${outputPath}\n`));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Pretty print results
|
|
325
|
+
printFrameworkDetection(result, options.verbose ?? false);
|
|
326
|
+
printFilePatterns(result, options.verbose ?? false);
|
|
327
|
+
printLocaleDetection(result, options.verbose ?? false);
|
|
328
|
+
printExistingSetup(result, options.verbose ?? false);
|
|
329
|
+
printConfidenceSummary(result);
|
|
330
|
+
printWarnings(result);
|
|
331
|
+
printSuggestedConfig(result);
|
|
332
|
+
printRecommendations(result);
|
|
333
|
+
|
|
334
|
+
if (options.showConfig) {
|
|
335
|
+
printConfigPreview(result);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Helpful tip
|
|
339
|
+
console.log(chalk.gray('\n💡 Tip: Run `i18nsmith init` to generate configuration based on this analysis.'));
|
|
340
|
+
})
|
|
341
|
+
);
|
|
342
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { registerInit, parseGlobList } from './init.js';
|
|
4
|
+
import fs from 'fs/promises';
|
|
4
5
|
|
|
5
6
|
vi.mock('@i18nsmith/core', () => ({
|
|
6
7
|
diagnoseWorkspace: vi.fn().mockResolvedValue({
|
|
@@ -23,6 +24,134 @@ vi.mock('@i18nsmith/core', () => ({
|
|
|
23
24
|
conflicts: [],
|
|
24
25
|
recommendations: [],
|
|
25
26
|
}),
|
|
27
|
+
ensureGitignore: vi.fn().mockResolvedValue({ updated: false }),
|
|
28
|
+
ProjectIntelligenceService: vi.fn().mockImplementation(() => ({
|
|
29
|
+
analyze: vi.fn().mockResolvedValue({
|
|
30
|
+
framework: {
|
|
31
|
+
type: 'react',
|
|
32
|
+
adapter: 'react-i18next',
|
|
33
|
+
hookName: 'useTranslation',
|
|
34
|
+
features: [],
|
|
35
|
+
confidence: 0.8,
|
|
36
|
+
evidence: []
|
|
37
|
+
},
|
|
38
|
+
locales: {
|
|
39
|
+
sourceLanguage: 'en',
|
|
40
|
+
targetLanguages: [],
|
|
41
|
+
localesDir: 'locales',
|
|
42
|
+
format: 'flat',
|
|
43
|
+
existingFiles: [],
|
|
44
|
+
existingKeyCount: 0,
|
|
45
|
+
confidence: 0.5
|
|
46
|
+
},
|
|
47
|
+
filePatterns: {
|
|
48
|
+
include: ['src/**/*.{ts,tsx,js,jsx}'],
|
|
49
|
+
exclude: ['node_modules/**', 'dist/**'],
|
|
50
|
+
sourceDirectories: ['src'],
|
|
51
|
+
hasTypeScript: true,
|
|
52
|
+
hasJsx: true,
|
|
53
|
+
hasVue: false,
|
|
54
|
+
hasSvelte: false,
|
|
55
|
+
sourceFileCount: 10,
|
|
56
|
+
confidence: 0.7
|
|
57
|
+
},
|
|
58
|
+
existingSetup: {
|
|
59
|
+
hasExistingConfig: false,
|
|
60
|
+
hasExistingLocales: false,
|
|
61
|
+
hasI18nProvider: false,
|
|
62
|
+
runtimePackages: [],
|
|
63
|
+
translationUsage: {
|
|
64
|
+
hookName: 'useTranslation',
|
|
65
|
+
translationIdentifier: 't',
|
|
66
|
+
filesWithHooks: 0,
|
|
67
|
+
translationCalls: 0,
|
|
68
|
+
exampleFiles: []
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
confidence: {
|
|
72
|
+
framework: 0.8,
|
|
73
|
+
filePatterns: 0.7,
|
|
74
|
+
existingSetup: 0.3,
|
|
75
|
+
locales: 0.5,
|
|
76
|
+
overall: 0.75,
|
|
77
|
+
level: 'high'
|
|
78
|
+
},
|
|
79
|
+
warnings: [],
|
|
80
|
+
recommendations: [],
|
|
81
|
+
suggestedConfig: {
|
|
82
|
+
sourceLanguage: 'en',
|
|
83
|
+
targetLanguages: [],
|
|
84
|
+
localesDir: 'locales',
|
|
85
|
+
include: ['src/**/*.{ts,tsx,js,jsx}'],
|
|
86
|
+
exclude: ['node_modules/**', 'dist/**'],
|
|
87
|
+
translationAdapter: {
|
|
88
|
+
module: 'react-i18next',
|
|
89
|
+
hookName: 'useTranslation'
|
|
90
|
+
},
|
|
91
|
+
keyGeneration: {
|
|
92
|
+
namespace: 'common',
|
|
93
|
+
shortHashLen: 6
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
})),
|
|
98
|
+
Scanner: {
|
|
99
|
+
create: vi.fn().mockResolvedValue({
|
|
100
|
+
scan: vi.fn().mockResolvedValue({
|
|
101
|
+
buckets: {
|
|
102
|
+
highConfidence: [
|
|
103
|
+
{
|
|
104
|
+
id: 'test-1',
|
|
105
|
+
filePath: 'src/components/Button.tsx',
|
|
106
|
+
kind: 'jsx-text',
|
|
107
|
+
text: 'Hello World',
|
|
108
|
+
context: 'Button component',
|
|
109
|
+
position: { line: 5, column: 10 }
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
needsReview: [],
|
|
113
|
+
skipped: []
|
|
114
|
+
},
|
|
115
|
+
candidates: [],
|
|
116
|
+
filesScanned: 5,
|
|
117
|
+
filesExamined: ['src/components/Button.tsx'],
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
KeyGenerator: vi.fn().mockImplementation(() => ({
|
|
122
|
+
generate: vi.fn().mockReturnValue({
|
|
123
|
+
key: 'common.button.hello-world.abc123',
|
|
124
|
+
hash: 'abc123',
|
|
125
|
+
preview: 'Hello World'
|
|
126
|
+
})
|
|
127
|
+
}))
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
vi.mock('../utils/scaffold.js', () => ({
|
|
131
|
+
scaffoldTranslationContext: vi.fn().mockResolvedValue({
|
|
132
|
+
path: 'src/contexts/translation-context.tsx',
|
|
133
|
+
content: '// mock content',
|
|
134
|
+
written: true
|
|
135
|
+
}),
|
|
136
|
+
scaffoldI18next: vi.fn().mockResolvedValue({
|
|
137
|
+
i18nPath: 'src/lib/i18n.ts',
|
|
138
|
+
providerPath: 'src/components/i18n-provider.tsx',
|
|
139
|
+
i18nResult: { path: 'src/lib/i18n.ts', content: '// i18n content', written: true },
|
|
140
|
+
providerResult: { path: 'src/components/i18n-provider.tsx', content: '// provider content', written: true }
|
|
141
|
+
})
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
vi.mock('../utils/package-manager.js', () => ({
|
|
145
|
+
detectPackageManager: vi.fn().mockResolvedValue('pnpm'),
|
|
146
|
+
installDependencies: vi.fn().mockResolvedValue(undefined)
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
vi.mock('../utils/pkg.js', () => ({
|
|
150
|
+
hasDependency: vi.fn().mockReturnValue(false),
|
|
151
|
+
readPackageJson: vi.fn().mockResolvedValue({
|
|
152
|
+
dependencies: {},
|
|
153
|
+
devDependencies: {}
|
|
154
|
+
})
|
|
26
155
|
}));
|
|
27
156
|
|
|
28
157
|
vi.mock('inquirer', () => ({
|
|
@@ -35,19 +164,97 @@ vi.mock('inquirer', () => ({
|
|
|
35
164
|
},
|
|
36
165
|
}));
|
|
37
166
|
|
|
167
|
+
vi.mock('chalk', () => ({
|
|
168
|
+
default: {
|
|
169
|
+
blue: vi.fn((text) => text),
|
|
170
|
+
green: vi.fn((text) => text),
|
|
171
|
+
yellow: vi.fn((text) => text),
|
|
172
|
+
red: vi.fn((text) => text),
|
|
173
|
+
dim: vi.fn((text) => text),
|
|
174
|
+
cyan: vi.fn((text) => text),
|
|
175
|
+
}
|
|
176
|
+
}));
|
|
177
|
+
|
|
38
178
|
vi.mock('fs/promises', () => ({
|
|
39
179
|
default: {
|
|
40
180
|
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
41
181
|
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
182
|
+
readFile: vi.fn().mockResolvedValue('{}'),
|
|
183
|
+
access: vi.fn().mockResolvedValue(undefined),
|
|
42
184
|
},
|
|
43
185
|
}));
|
|
44
186
|
|
|
45
187
|
describe('init command', () => {
|
|
188
|
+
beforeEach(() => {
|
|
189
|
+
vi.clearAllMocks();
|
|
190
|
+
});
|
|
191
|
+
|
|
46
192
|
it('should register the init command', () => {
|
|
47
193
|
const program = new Command();
|
|
48
194
|
registerInit(program);
|
|
49
195
|
const command = program.commands.find((cmd) => cmd.name() === 'init');
|
|
50
196
|
expect(command).toBeDefined();
|
|
197
|
+
expect(command?.options.some(opt => opt.flags === '--scaffold')).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('parseGlobList', () => {
|
|
201
|
+
it('treats brace-expanded globs as atomic tokens', () => {
|
|
202
|
+
const input = 'src/**/*.{ts,tsx,js,jsx}, app/**/*.{ts,tsx}';
|
|
203
|
+
expect(parseGlobList(input)).toEqual([
|
|
204
|
+
'src/**/*.{ts,tsx,js,jsx}',
|
|
205
|
+
'app/**/*.{ts,tsx}',
|
|
206
|
+
]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('handles nested braces', () => {
|
|
210
|
+
const input = 'src/**/*.{ts,tsx,{spec,test}.ts}';
|
|
211
|
+
expect(parseGlobList(input)).toEqual(['src/**/*.{ts,tsx,{spec,test}.ts}']);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('splits simple comma-separated values', () => {
|
|
215
|
+
const input = 'en, fr, es';
|
|
216
|
+
expect(parseGlobList(input)).toEqual(['en', 'fr', 'es']);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('handles empty input', () => {
|
|
220
|
+
expect(parseGlobList('')).toEqual([]);
|
|
221
|
+
expect(parseGlobList(' ')).toEqual([]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('trims whitespace around entries', () => {
|
|
225
|
+
const input = ' src/**/* , app/**/* ';
|
|
226
|
+
expect(parseGlobList(input)).toEqual(['src/**/*', 'app/**/*']);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('non-interactive mode', () => {
|
|
231
|
+
it('should seed source locale with detected keys', async () => {
|
|
232
|
+
const program = new Command();
|
|
233
|
+
registerInit(program);
|
|
234
|
+
const command = program.commands.find((cmd) => cmd.name() === 'init')!;
|
|
235
|
+
|
|
236
|
+
// Mock process.cwd to return a test directory
|
|
237
|
+
const originalCwd = process.cwd;
|
|
238
|
+
process.cwd = vi.fn().mockReturnValue('/test/project');
|
|
239
|
+
|
|
240
|
+
// Mock fs.access to throw (config doesn't exist)
|
|
241
|
+
const originalAccess = fs.access;
|
|
242
|
+
vi.mocked(fs.access).mockRejectedValue(new Error('File not found'));
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
// Execute command with --yes
|
|
246
|
+
await (command as any).parseAsync(['--yes'], { from: 'user' });
|
|
247
|
+
} finally {
|
|
248
|
+
process.cwd = originalCwd;
|
|
249
|
+
vi.mocked(fs.access).mockRestore();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Should have written the seeded locale file
|
|
253
|
+
expect(vi.mocked(fs.writeFile)).toHaveBeenCalledWith(
|
|
254
|
+
expect.stringContaining('locales/en.json'),
|
|
255
|
+
expect.stringContaining('common.button.hello-world.abc123')
|
|
256
|
+
);
|
|
257
|
+
});
|
|
51
258
|
});
|
|
52
259
|
});
|
|
53
260
|
|