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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkBpC,eAAO,MAAM,OAAO,SAAgB,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":"AAUA,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBnE"}
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"}
@@ -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.3",
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