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/src/commands/init.ts
CHANGED
|
@@ -3,9 +3,10 @@ import inquirer from 'inquirer';
|
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import { diagnoseWorkspace, I18nConfig, TranslationConfig, ensureGitignore } from '@i18nsmith/core';
|
|
6
|
+
import { diagnoseWorkspace, I18nConfig, TranslationConfig, ensureGitignore, ProjectIntelligenceService, type ProjectIntelligence, type SuggestedConfig, Scanner, KeyGenerator } from '@i18nsmith/core';
|
|
7
7
|
import { scaffoldTranslationContext, scaffoldI18next } from '../utils/scaffold.js';
|
|
8
8
|
import { hasDependency, readPackageJson } from '../utils/pkg.js';
|
|
9
|
+
import { detectPackageManager, installDependencies } from '../utils/package-manager.js';
|
|
9
10
|
import { CliError, withErrorHandling } from '../utils/errors.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -41,9 +42,14 @@ export function parseGlobList(value: string): string[] {
|
|
|
41
42
|
interface InitCommandOptions {
|
|
42
43
|
merge?: boolean;
|
|
43
44
|
yes?: boolean;
|
|
45
|
+
template?: string;
|
|
46
|
+
scaffold?: boolean;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
interface InitAnswers {
|
|
50
|
+
setupMode?: 'auto' | 'template' | 'manual';
|
|
51
|
+
template?: string;
|
|
52
|
+
confirmAuto?: boolean;
|
|
47
53
|
sourceLanguage: string;
|
|
48
54
|
targetLanguages: string;
|
|
49
55
|
localesDir: string;
|
|
@@ -65,9 +71,22 @@ interface InitAnswers {
|
|
|
65
71
|
seedTargetLocales: boolean;
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Run project intelligence detection to gather smart defaults.
|
|
76
|
+
*/
|
|
77
|
+
async function detectProjectIntelligence(workspaceRoot: string): Promise<ProjectIntelligence | null> {
|
|
78
|
+
try {
|
|
79
|
+
const service = new ProjectIntelligenceService();
|
|
80
|
+
const result = await service.analyze({ workspaceRoot });
|
|
81
|
+
return result;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
/**
|
|
69
88
|
* Run init in non-interactive mode with sensible defaults.
|
|
70
|
-
*
|
|
89
|
+
* Uses ProjectIntelligenceService for smart detection.
|
|
71
90
|
*/
|
|
72
91
|
async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promise<void> {
|
|
73
92
|
const workspaceRoot = process.cwd();
|
|
@@ -85,97 +104,92 @@ async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promis
|
|
|
85
104
|
// Config doesn't exist, proceed
|
|
86
105
|
}
|
|
87
106
|
|
|
88
|
-
|
|
89
|
-
const minimalConfig: I18nConfig = {
|
|
90
|
-
version: 1 as const,
|
|
91
|
-
sourceLanguage: 'en',
|
|
92
|
-
targetLanguages: [],
|
|
93
|
-
localesDir: 'locales',
|
|
94
|
-
include: ['src/**/*.{ts,tsx,js,jsx}'],
|
|
95
|
-
exclude: ['node_modules/**'],
|
|
96
|
-
minTextLength: 1,
|
|
97
|
-
translation: { provider: 'manual' },
|
|
98
|
-
translationAdapter: { module: 'react-i18next', hookName: 'useTranslation' },
|
|
99
|
-
keyGeneration: { namespace: 'common', shortHashLen: 6 },
|
|
100
|
-
seedTargetLocales: false,
|
|
101
|
-
};
|
|
107
|
+
console.log(chalk.blue('🔍 Detecting project configuration...'));
|
|
102
108
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
let detectedLocales: string[] = [];
|
|
109
|
+
// Use ProjectIntelligenceService for detection
|
|
110
|
+
const intelligence = await detectProjectIntelligence(workspaceRoot);
|
|
106
111
|
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
let suggestedConfig: SuggestedConfig;
|
|
113
|
+
|
|
114
|
+
if (intelligence) {
|
|
115
|
+
const { framework, locales, filePatterns, confidence } = intelligence;
|
|
109
116
|
|
|
110
|
-
//
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
console.log(chalk.blue(`Detected adapter: ${detectedAdapter}`));
|
|
117
|
+
// Report detection results
|
|
118
|
+
if (framework.type !== 'unknown') {
|
|
119
|
+
console.log(chalk.green(` ✓ Framework: ${framework.type}`));
|
|
114
120
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
if (framework.adapter) {
|
|
122
|
+
console.log(chalk.green(` ✓ i18n Adapter: ${framework.adapter}`));
|
|
123
|
+
}
|
|
124
|
+
if (locales.existingFiles.length > 0) {
|
|
125
|
+
const langs = [locales.sourceLanguage, ...locales.targetLanguages].filter(Boolean);
|
|
126
|
+
console.log(chalk.green(` ✓ Locales: ${langs.join(', ')}`));
|
|
127
|
+
}
|
|
128
|
+
if (filePatterns.sourceDirectories.length > 0) {
|
|
129
|
+
console.log(chalk.green(` ✓ Source directories: ${filePatterns.sourceDirectories.slice(0, 3).join(', ')}${filePatterns.sourceDirectories.length > 3 ? '...' : ''}`));
|
|
123
130
|
}
|
|
124
|
-
} catch (error) {
|
|
125
|
-
console.log(chalk.dim(`Could not run diagnostics: ${(error as Error).message}`));
|
|
126
|
-
}
|
|
127
131
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// Use 'en' as source if present, otherwise first detected locale
|
|
134
|
-
if (detectedLocales.includes('en')) {
|
|
135
|
-
sourceLanguage = 'en';
|
|
136
|
-
targetLanguages = detectedLocales.filter((l) => l !== 'en');
|
|
132
|
+
// Use template if specified, otherwise use detected config
|
|
133
|
+
if (commandOptions.template) {
|
|
134
|
+
const service = new ProjectIntelligenceService();
|
|
135
|
+
suggestedConfig = service.applyTemplate(commandOptions.template, intelligence);
|
|
136
|
+
console.log(chalk.green(` ✓ Template: ${commandOptions.template}`));
|
|
137
137
|
} else {
|
|
138
|
-
|
|
139
|
-
targetLanguages = detectedLocales.slice(1);
|
|
138
|
+
suggestedConfig = intelligence.suggestedConfig;
|
|
140
139
|
}
|
|
140
|
+
|
|
141
|
+
// Report confidence
|
|
142
|
+
const confidencePercent = Math.round(intelligence.confidence.overall * 100);
|
|
143
|
+
const confidenceColor = intelligence.confidence.level === 'high' ? chalk.green : intelligence.confidence.level === 'medium' ? chalk.yellow : chalk.red;
|
|
144
|
+
console.log(confidenceColor(` Detection confidence: ${confidencePercent}% (${intelligence.confidence.level})`));
|
|
145
|
+
|
|
146
|
+
// Check for low confidence in non-interactive mode
|
|
147
|
+
if (intelligence.confidence.level === 'low' || intelligence.confidence.level === 'uncertain') {
|
|
148
|
+
console.log(chalk.red('\n❌ Detection confidence is too low for automatic setup.'));
|
|
149
|
+
console.log(chalk.dim('This could lead to incorrect configuration choices.'));
|
|
150
|
+
console.log(chalk.blue('\nSuggestions:'));
|
|
151
|
+
console.log(chalk.cyan(' • Use --template <name> to specify your framework explicitly'));
|
|
152
|
+
console.log(chalk.cyan(' • Run without --yes for interactive setup'));
|
|
153
|
+
console.log(chalk.cyan(' • Check that your project has clear framework indicators (package.json, config files)'));
|
|
154
|
+
throw new CliError('Detection confidence too low for non-interactive mode. Use --template or run interactively.');
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// Fallback when detection fails completely
|
|
158
|
+
console.log(chalk.red('❌ Project analysis failed.'));
|
|
159
|
+
console.log(chalk.dim('Unable to detect project configuration automatically.'));
|
|
160
|
+
console.log(chalk.blue('\nSuggestions:'));
|
|
161
|
+
console.log(chalk.cyan(' • Use --template <name> to specify your framework explicitly'));
|
|
162
|
+
console.log(chalk.cyan(' • Run without --yes for interactive setup'));
|
|
163
|
+
console.log(chalk.cyan(' • Ensure package.json exists and contains framework dependencies'));
|
|
164
|
+
throw new CliError('Project analysis failed. Use --template or run interactively.');
|
|
141
165
|
}
|
|
142
166
|
|
|
143
|
-
// Build
|
|
167
|
+
// Build config from suggested values
|
|
144
168
|
const config: I18nConfig = {
|
|
145
169
|
version: 1 as const,
|
|
146
|
-
sourceLanguage,
|
|
147
|
-
targetLanguages,
|
|
148
|
-
localesDir:
|
|
149
|
-
include:
|
|
150
|
-
|
|
151
|
-
'app/**/*.{ts,tsx,js,jsx}',
|
|
152
|
-
'pages/**/*.{ts,tsx,js,jsx}',
|
|
153
|
-
'components/**/*.{ts,tsx,js,jsx}',
|
|
154
|
-
],
|
|
155
|
-
exclude: ['node_modules/**', '**/*.test.*'],
|
|
170
|
+
sourceLanguage: suggestedConfig.sourceLanguage,
|
|
171
|
+
targetLanguages: suggestedConfig.targetLanguages,
|
|
172
|
+
localesDir: suggestedConfig.localesDir,
|
|
173
|
+
include: suggestedConfig.include,
|
|
174
|
+
exclude: suggestedConfig.exclude,
|
|
156
175
|
minTextLength: 1,
|
|
157
176
|
translation: { provider: 'manual' },
|
|
158
177
|
translationAdapter: {
|
|
159
|
-
module:
|
|
160
|
-
hookName:
|
|
161
|
-
},
|
|
162
|
-
keyGeneration: {
|
|
163
|
-
namespace: 'common',
|
|
164
|
-
shortHashLen: 6,
|
|
178
|
+
module: suggestedConfig.translationAdapter.module,
|
|
179
|
+
hookName: suggestedConfig.translationAdapter.hookName,
|
|
165
180
|
},
|
|
181
|
+
keyGeneration: suggestedConfig.keyGeneration,
|
|
166
182
|
seedTargetLocales: false,
|
|
167
183
|
};
|
|
168
184
|
|
|
169
185
|
try {
|
|
170
186
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
171
187
|
console.log(chalk.green(`\n✓ Configuration created at ${configPath}`));
|
|
172
|
-
console.log(chalk.dim(' Source language: ' + sourceLanguage));
|
|
173
|
-
if (targetLanguages.length > 0) {
|
|
174
|
-
console.log(chalk.dim(' Target languages: ' + targetLanguages.join(', ')));
|
|
175
|
-
}
|
|
176
|
-
if (detectedAdapter) {
|
|
177
|
-
console.log(chalk.dim(' Adapter: ' + detectedAdapter));
|
|
188
|
+
console.log(chalk.dim(' Source language: ' + config.sourceLanguage));
|
|
189
|
+
if (config.targetLanguages.length > 0) {
|
|
190
|
+
console.log(chalk.dim(' Target languages: ' + config.targetLanguages.join(', ')));
|
|
178
191
|
}
|
|
192
|
+
console.log(chalk.dim(' Adapter: ' + (config.translationAdapter?.module ?? 'react-i18next')));
|
|
179
193
|
|
|
180
194
|
// Ensure .gitignore has i18nsmith artifacts
|
|
181
195
|
const gitignoreResult = await ensureGitignore(workspaceRoot);
|
|
@@ -183,6 +197,149 @@ async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promis
|
|
|
183
197
|
console.log(chalk.green(`✓ Updated .gitignore with i18nsmith artifacts`));
|
|
184
198
|
}
|
|
185
199
|
|
|
200
|
+
// Create a minimal source locale file to avoid immediate "missing source locale" diagnostics
|
|
201
|
+
try {
|
|
202
|
+
const localesDirPath = path.join(workspaceRoot, config.localesDir || 'locales');
|
|
203
|
+
await fs.mkdir(localesDirPath, { recursive: true });
|
|
204
|
+
const sourceLocalePath = path.join(localesDirPath, `${config.sourceLanguage}.json`);
|
|
205
|
+
// Only create if it doesn't already exist
|
|
206
|
+
try {
|
|
207
|
+
await fs.access(sourceLocalePath);
|
|
208
|
+
} catch {
|
|
209
|
+
await fs.writeFile(sourceLocalePath, JSON.stringify({}, null, 2));
|
|
210
|
+
console.log(chalk.green(`✓ Created source locale file at ${sourceLocalePath}`));
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.log(chalk.yellow('Could not create source locale file automatically. You can create it at <localesDir>/<sourceLanguage>.json'));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Seed source locale with detected keys to avoid immediate "missing-key" diagnostics
|
|
217
|
+
try {
|
|
218
|
+
console.log(chalk.blue('🔍 Scanning for existing hardcoded text...'));
|
|
219
|
+
const scanner = await Scanner.create(config, { workspaceRoot });
|
|
220
|
+
const scanResult = await scanner.scan();
|
|
221
|
+
|
|
222
|
+
if (scanResult.buckets.highConfidence.length > 0) {
|
|
223
|
+
const sourceLocalePath = path.join(workspaceRoot, config.localesDir || 'locales', `${config.sourceLanguage}.json`);
|
|
224
|
+
|
|
225
|
+
// Read existing content or create empty object
|
|
226
|
+
let existingKeys: Record<string, string> = {};
|
|
227
|
+
try {
|
|
228
|
+
const content = await fs.readFile(sourceLocalePath, 'utf-8');
|
|
229
|
+
existingKeys = JSON.parse(content);
|
|
230
|
+
} catch {
|
|
231
|
+
// File doesn't exist or is invalid, start with empty
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Generate keys for high-confidence candidates
|
|
235
|
+
const keyGenerator = new KeyGenerator({
|
|
236
|
+
namespace: config.keyGeneration?.namespace || 'common',
|
|
237
|
+
hashLength: config.keyGeneration?.shortHashLen || 6,
|
|
238
|
+
workspaceRoot,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
let keysAdded = 0;
|
|
242
|
+
for (const candidate of scanResult.buckets.highConfidence) {
|
|
243
|
+
const generated = keyGenerator.generate(candidate.text, {
|
|
244
|
+
filePath: candidate.filePath,
|
|
245
|
+
kind: candidate.kind,
|
|
246
|
+
context: candidate.context,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (!existingKeys[generated.key]) {
|
|
250
|
+
existingKeys[generated.key] = candidate.text;
|
|
251
|
+
keysAdded++;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (keysAdded > 0) {
|
|
256
|
+
await fs.writeFile(sourceLocalePath, JSON.stringify(existingKeys, null, 2));
|
|
257
|
+
console.log(chalk.green(`✓ Seeded source locale with ${keysAdded} detected keys`));
|
|
258
|
+
} else {
|
|
259
|
+
console.log(chalk.dim(' No new keys to seed (all detected keys already exist)'));
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
console.log(chalk.dim(' No high-confidence hardcoded text detected'));
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.log(chalk.yellow(`Could not seed locale with detected keys: ${(error as Error).message}`));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Auto-scaffold adapter if requested and conditions are met
|
|
269
|
+
let scaffolded = false;
|
|
270
|
+
if (commandOptions.scaffold && intelligence && intelligence.confidence.level === 'high' && intelligence.existingSetup.runtimePackages.length === 0) {
|
|
271
|
+
console.log(chalk.blue('\n🔧 Auto-scaffolding translation adapter...'));
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const adapterType = suggestedConfig.translationAdapter.module === 'react-i18next' ? 'react-i18next' : 'custom';
|
|
275
|
+
|
|
276
|
+
if (adapterType === 'react-i18next') {
|
|
277
|
+
const i18nPath = 'src/lib/i18n.ts';
|
|
278
|
+
const providerPath = 'src/components/i18n-provider.tsx';
|
|
279
|
+
|
|
280
|
+
const { i18nResult, providerResult } = await scaffoldI18next(
|
|
281
|
+
i18nPath,
|
|
282
|
+
providerPath,
|
|
283
|
+
config.sourceLanguage,
|
|
284
|
+
config.localesDir || 'locales',
|
|
285
|
+
{ workspaceRoot }
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
console.log(chalk.green('✓ Scaffolded react-i18next runtime:'));
|
|
289
|
+
console.log(chalk.green(` • ${i18nResult.path}`));
|
|
290
|
+
console.log(chalk.green(` • ${providerResult.path}`));
|
|
291
|
+
|
|
292
|
+
// Try to install dependencies
|
|
293
|
+
const pkg = await readPackageJson();
|
|
294
|
+
const missingDeps = ['react-i18next', 'i18next'].filter((dep) => !hasDependency(pkg, dep));
|
|
295
|
+
if (missingDeps.length) {
|
|
296
|
+
try {
|
|
297
|
+
const manager = await detectPackageManager();
|
|
298
|
+
console.log(chalk.blue(`Installing dependencies with ${manager}...`));
|
|
299
|
+
await installDependencies(manager, missingDeps);
|
|
300
|
+
console.log(chalk.green('✓ Dependencies installed successfully.'));
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.log(chalk.yellow('Could not install dependencies automatically. Install them manually:'));
|
|
303
|
+
console.log(chalk.cyan(` pnpm add ${missingDeps.join(' ')}`));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
scaffolded = true;
|
|
308
|
+
} else {
|
|
309
|
+
// Custom adapter for other frameworks
|
|
310
|
+
const contextPath = `src/contexts/translation-context.tsx`;
|
|
311
|
+
const result = await scaffoldTranslationContext(contextPath, config.sourceLanguage, {
|
|
312
|
+
localesDir: config.localesDir || 'locales',
|
|
313
|
+
workspaceRoot
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
console.log(chalk.green(`✓ Scaffolded custom translation context at ${result.path}`));
|
|
317
|
+
|
|
318
|
+
// Update config to use the custom adapter
|
|
319
|
+
config.translationAdapter = {
|
|
320
|
+
module: contextPath.replace(/\.tsx?$/, ''),
|
|
321
|
+
hookName: 'useTranslation',
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// Rewrite config file with updated adapter
|
|
325
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
326
|
+
|
|
327
|
+
scaffolded = true;
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.log(chalk.yellow(`Could not auto-scaffold adapter: ${(error as Error).message}`));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Inform user about next steps
|
|
335
|
+
console.log(chalk.blue('\nNext steps:'));
|
|
336
|
+
if (!scaffolded) {
|
|
337
|
+
console.log(chalk.dim(' • Install and scaffold a runtime (e.g. react-i18next or vue-i18n) to enable in-app translations.'));
|
|
338
|
+
console.log(chalk.cyan(` i18nsmith scaffold-adapter --type ${suggestedConfig.translationAdapter.module} --install-deps`));
|
|
339
|
+
} else {
|
|
340
|
+
console.log(chalk.dim(' • Your translation adapter is ready! Import and use it in your components.'));
|
|
341
|
+
}
|
|
342
|
+
|
|
186
343
|
console.log(chalk.blue('\nRun "i18nsmith check" to verify your setup.'));
|
|
187
344
|
} catch (error) {
|
|
188
345
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -196,6 +353,8 @@ export function registerInit(program: Command) {
|
|
|
196
353
|
.description('Initialize i18nsmith configuration')
|
|
197
354
|
.option('--merge', 'Merge with existing locales/runtimes when detected', false)
|
|
198
355
|
.option('-y, --yes', 'Skip prompts and use defaults (non-interactive mode)', false)
|
|
356
|
+
.option('--template <template>', 'Use a preset template (react, next-app, next-pages, vue3, nuxt3, svelte, minimal)', undefined)
|
|
357
|
+
.option('--scaffold', 'Automatically scaffold translation adapter when confidence is high and runtime is missing', false)
|
|
199
358
|
.action(
|
|
200
359
|
withErrorHandling(async (commandOptions: InitCommandOptions) => {
|
|
201
360
|
console.log(chalk.blue('Initializing i18nsmith configuration...'));
|
|
@@ -206,41 +365,83 @@ export function registerInit(program: Command) {
|
|
|
206
365
|
return;
|
|
207
366
|
}
|
|
208
367
|
|
|
368
|
+
const workspaceRoot = process.cwd();
|
|
369
|
+
let config: I18nConfig | undefined;
|
|
370
|
+
|
|
371
|
+
// Detect project intelligence early to use for suggestions
|
|
372
|
+
const intelligence = await detectProjectIntelligence(workspaceRoot);
|
|
373
|
+
const suggestedValues = intelligence?.suggestedConfig;
|
|
374
|
+
|
|
209
375
|
const answers = await inquirer.prompt<InitAnswers>([
|
|
376
|
+
{
|
|
377
|
+
type: 'list',
|
|
378
|
+
name: 'setupMode',
|
|
379
|
+
message: 'How would you like to set up i18nsmith?',
|
|
380
|
+
choices: [
|
|
381
|
+
{ name: 'Auto-detect (recommended) - Analyze your project and suggest optimal configuration', value: 'auto' },
|
|
382
|
+
{ name: 'Use template - Choose from popular framework presets', value: 'template' },
|
|
383
|
+
{ name: 'Manual setup - Configure everything manually', value: 'manual' },
|
|
384
|
+
],
|
|
385
|
+
default: 'auto',
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
type: 'list',
|
|
389
|
+
name: 'template',
|
|
390
|
+
message: 'Which template matches your project?',
|
|
391
|
+
when: (answers) => answers.setupMode === 'template',
|
|
392
|
+
choices: [
|
|
393
|
+
{ name: 'React with react-i18next', value: 'react' },
|
|
394
|
+
{ name: 'Next.js App Router', value: 'next-app' },
|
|
395
|
+
{ name: 'Next.js Pages Router', value: 'next-pages' },
|
|
396
|
+
{ name: 'Vue 3 with vue-i18n', value: 'vue3' },
|
|
397
|
+
{ name: 'Nuxt 3', value: 'nuxt3' },
|
|
398
|
+
{ name: 'Svelte/SvelteKit', value: 'svelte' },
|
|
399
|
+
{ name: 'Minimal setup', value: 'minimal' },
|
|
400
|
+
],
|
|
401
|
+
default: 'react',
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
// Manual Configuration Questions with Smart Suggestions
|
|
210
405
|
{
|
|
211
406
|
type: 'input',
|
|
212
407
|
name: 'sourceLanguage',
|
|
213
408
|
message: 'What is the source language?',
|
|
214
|
-
|
|
409
|
+
when: (answers) => answers.setupMode === 'manual',
|
|
410
|
+
default: suggestedValues?.sourceLanguage || 'en',
|
|
215
411
|
},
|
|
216
412
|
{
|
|
217
413
|
type: 'input',
|
|
218
414
|
name: 'targetLanguages',
|
|
219
415
|
message: 'Which target languages do you need? (comma separated)',
|
|
220
|
-
|
|
416
|
+
when: (answers) => answers.setupMode === 'manual',
|
|
417
|
+
default: suggestedValues?.targetLanguages?.join(', ') || 'fr',
|
|
221
418
|
},
|
|
222
419
|
{
|
|
223
420
|
type: 'input',
|
|
224
421
|
name: 'localesDir',
|
|
225
422
|
message: 'Where should locale files be stored?',
|
|
226
|
-
|
|
423
|
+
when: (answers) => answers.setupMode === 'manual',
|
|
424
|
+
default: suggestedValues?.localesDir || 'locales',
|
|
227
425
|
},
|
|
228
426
|
{
|
|
229
427
|
type: 'input',
|
|
230
428
|
name: 'include',
|
|
231
429
|
message: 'Which files should be scanned? (comma separated glob patterns)',
|
|
232
|
-
|
|
430
|
+
when: (answers) => answers.setupMode === 'manual',
|
|
431
|
+
default: suggestedValues?.include?.join(', ') || 'src/**/*.{ts,tsx,js,jsx}, app/**/*.{ts,tsx,js,jsx}, pages/**/*.{ts,tsx,js,jsx}, components/**/*.{ts,tsx,js,jsx}',
|
|
233
432
|
},
|
|
234
433
|
{
|
|
235
434
|
type: 'input',
|
|
236
435
|
name: 'exclude',
|
|
237
436
|
message: 'Which files should be excluded? (comma separated glob patterns)',
|
|
238
|
-
|
|
437
|
+
when: (answers) => answers.setupMode === 'manual',
|
|
438
|
+
default: suggestedValues?.exclude?.join(', ') || 'node_modules/**,**/*.test.*',
|
|
239
439
|
},
|
|
240
440
|
{
|
|
241
441
|
type: 'input',
|
|
242
442
|
name: 'minTextLength',
|
|
243
443
|
message: 'Minimum length for translatable text?',
|
|
444
|
+
when: (answers) => answers.setupMode === 'manual',
|
|
244
445
|
default: '1',
|
|
245
446
|
validate: (input) => {
|
|
246
447
|
const num = parseInt(input, 10);
|
|
@@ -251,6 +452,7 @@ export function registerInit(program: Command) {
|
|
|
251
452
|
type: 'list',
|
|
252
453
|
name: 'service',
|
|
253
454
|
message: 'Which translation service do you want to use?',
|
|
455
|
+
when: (answers) => answers.setupMode === 'manual',
|
|
254
456
|
choices: ['google', 'deepl', 'manual'],
|
|
255
457
|
default: 'google',
|
|
256
458
|
},
|
|
@@ -258,7 +460,7 @@ export function registerInit(program: Command) {
|
|
|
258
460
|
type: 'input',
|
|
259
461
|
name: 'translationSecretEnvVar',
|
|
260
462
|
message: 'Name of the environment variable containing your translation API key',
|
|
261
|
-
when: (answers) => answers.service !== 'manual',
|
|
463
|
+
when: (answers) => answers.setupMode === 'manual' && answers.service !== 'manual',
|
|
262
464
|
default: (answers: InitAnswers) =>
|
|
263
465
|
answers.service === 'deepl' ? 'DEEPL_API_KEY' : 'GOOGLE_TRANSLATE_API_KEY',
|
|
264
466
|
},
|
|
@@ -266,168 +468,243 @@ export function registerInit(program: Command) {
|
|
|
266
468
|
type: 'list',
|
|
267
469
|
name: 'adapterPreset',
|
|
268
470
|
message: 'How should transformed components access translations?',
|
|
471
|
+
when: (answers) => answers.setupMode === 'manual',
|
|
269
472
|
choices: [
|
|
270
473
|
{ name: 'react-i18next (default)', value: 'react-i18next' },
|
|
474
|
+
{ name: 'vue-i18n', value: 'vue-i18n' },
|
|
475
|
+
{ name: 'svelte-i18n', value: 'svelte-i18n' },
|
|
476
|
+
{ name: 'next-intl', value: 'next-intl' },
|
|
271
477
|
{ name: 'Custom hook/module', value: 'custom' },
|
|
272
478
|
],
|
|
273
|
-
default: 'react-i18next',
|
|
479
|
+
default: suggestedValues?.translationAdapter?.module || 'react-i18next',
|
|
274
480
|
},
|
|
275
481
|
{
|
|
276
482
|
type: 'input',
|
|
277
483
|
name: 'customAdapterModule',
|
|
278
484
|
message: 'Provide the module specifier for your translation hook (e.g. "@/contexts/translation-context")',
|
|
279
|
-
when: (answers) => answers.adapterPreset === 'custom',
|
|
485
|
+
when: (answers) => answers.setupMode === 'manual' && answers.adapterPreset === 'custom',
|
|
280
486
|
validate: (input) => (input && input.trim().length > 0 ? true : 'Module specifier cannot be empty'),
|
|
281
487
|
},
|
|
282
488
|
{
|
|
283
489
|
type: 'input',
|
|
284
490
|
name: 'customAdapterHook',
|
|
285
491
|
message: 'Name of the hook/function to import (default: useTranslation)',
|
|
286
|
-
when: (answers) => answers.adapterPreset === 'custom',
|
|
492
|
+
when: (answers) => answers.setupMode === 'manual' && answers.adapterPreset === 'custom',
|
|
287
493
|
default: 'useTranslation',
|
|
288
494
|
},
|
|
289
|
-
{
|
|
290
|
-
type: 'confirm',
|
|
291
|
-
name: 'scaffoldAdapter',
|
|
292
|
-
message: 'Scaffold a lightweight translation context file?',
|
|
293
|
-
when: (answers) => answers.adapterPreset === 'custom',
|
|
294
|
-
default: true,
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
type: 'input',
|
|
298
|
-
name: 'scaffoldAdapterPath',
|
|
299
|
-
message: 'Path to scaffold the translation context file (relative to project root)',
|
|
300
|
-
when: (answers) => answers.scaffoldAdapter,
|
|
301
|
-
default: 'src/contexts/translation-context.tsx',
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
type: 'confirm',
|
|
305
|
-
name: 'scaffoldReactRuntime',
|
|
306
|
-
message: 'Scaffold i18next initializer and provider?',
|
|
307
|
-
when: (answers) => answers.adapterPreset === 'react-i18next',
|
|
308
|
-
default: true,
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
type: 'input',
|
|
312
|
-
name: 'reactI18nPath',
|
|
313
|
-
message: 'Path for i18next initializer (e.g. src/lib/i18n.ts)',
|
|
314
|
-
when: (answers) => answers.scaffoldReactRuntime,
|
|
315
|
-
default: 'src/lib/i18n.ts',
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
type: 'input',
|
|
319
|
-
name: 'reactProviderPath',
|
|
320
|
-
message: 'Path for I18nProvider component (e.g. src/components/i18n-provider.tsx)',
|
|
321
|
-
when: (answers) => answers.scaffoldReactRuntime,
|
|
322
|
-
default: 'src/components/i18n-provider.tsx',
|
|
323
|
-
},
|
|
324
495
|
{
|
|
325
496
|
type: 'input',
|
|
326
497
|
name: 'keyNamespace',
|
|
327
498
|
message: 'Namespace prefix for generated keys',
|
|
328
|
-
|
|
499
|
+
when: (answers) => answers.setupMode === 'manual',
|
|
500
|
+
default: suggestedValues?.keyGeneration?.namespace || 'common',
|
|
329
501
|
},
|
|
330
502
|
{
|
|
331
503
|
type: 'input',
|
|
332
504
|
name: 'shortHashLen',
|
|
333
505
|
message: 'Length of short hash suffix for keys',
|
|
334
|
-
|
|
506
|
+
when: (answers) => answers.setupMode === 'manual',
|
|
507
|
+
default: suggestedValues?.keyGeneration?.shortHashLen?.toString() || '6',
|
|
335
508
|
validate: (input) => {
|
|
336
509
|
const num = parseInt(input, 10);
|
|
337
510
|
return !isNaN(num) && num > 0 ? true : 'Please enter a positive number';
|
|
338
511
|
},
|
|
339
512
|
},
|
|
340
|
-
{
|
|
341
|
-
type: 'confirm',
|
|
342
|
-
name: 'seedTargetLocales',
|
|
343
|
-
message: 'Seed target locale files with empty values?',
|
|
344
|
-
default: false,
|
|
345
|
-
},
|
|
346
513
|
]);
|
|
347
514
|
|
|
348
|
-
const adapterModule =
|
|
349
|
-
answers.adapterPreset === 'custom'
|
|
350
|
-
? answers.customAdapterModule?.trim()
|
|
351
|
-
: 'react-i18next';
|
|
352
|
-
const adapterHook =
|
|
353
|
-
answers.adapterPreset === 'custom'
|
|
354
|
-
? (answers.customAdapterHook?.trim() || 'useTranslation')
|
|
355
|
-
: 'useTranslation';
|
|
356
|
-
|
|
357
|
-
const translationConfig: TranslationConfig =
|
|
358
|
-
answers.service === 'manual'
|
|
359
|
-
? { provider: 'manual' }
|
|
360
|
-
: {
|
|
361
|
-
provider: answers.service,
|
|
362
|
-
secretEnvVar: answers.translationSecretEnvVar?.trim() || undefined,
|
|
363
|
-
concurrency: 5,
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
const config: I18nConfig = {
|
|
367
|
-
version: 1 as const,
|
|
368
|
-
sourceLanguage: answers.sourceLanguage,
|
|
369
|
-
targetLanguages: parseGlobList(answers.targetLanguages),
|
|
370
|
-
localesDir: answers.localesDir,
|
|
371
|
-
include: parseGlobList(answers.include),
|
|
372
|
-
exclude: parseGlobList(answers.exclude),
|
|
373
|
-
minTextLength: parseInt(answers.minTextLength, 10),
|
|
374
|
-
translation: translationConfig,
|
|
375
|
-
translationAdapter: {
|
|
376
|
-
module: adapterModule ?? 'react-i18next',
|
|
377
|
-
hookName: adapterHook,
|
|
378
|
-
},
|
|
379
|
-
keyGeneration: {
|
|
380
|
-
namespace: answers.keyNamespace,
|
|
381
|
-
shortHashLen: parseInt(answers.shortHashLen, 10),
|
|
382
|
-
},
|
|
383
|
-
seedTargetLocales: answers.seedTargetLocales,
|
|
384
|
-
};
|
|
385
515
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
console.log(chalk.yellow('Aborting init to avoid overwriting existing i18n assets. Re-run with --merge to bypass.'));
|
|
516
|
+
if (answers.setupMode === 'auto') {
|
|
517
|
+
if (!intelligence) {
|
|
518
|
+
console.log(chalk.yellow('Could not analyze project. Please use Manual setup.'));
|
|
390
519
|
return;
|
|
520
|
+
} else {
|
|
521
|
+
const { framework, locales, filePatterns, confidence } = intelligence;
|
|
522
|
+
|
|
523
|
+
// Report detection results
|
|
524
|
+
if (framework.type !== 'unknown') {
|
|
525
|
+
console.log(chalk.green(` ✓ Framework: ${framework.type}`));
|
|
526
|
+
}
|
|
527
|
+
if (framework.adapter) {
|
|
528
|
+
console.log(chalk.green(` ✓ i18n Adapter: ${framework.adapter}`));
|
|
529
|
+
}
|
|
530
|
+
if (locales.existingFiles.length > 0) {
|
|
531
|
+
const langs = [locales.sourceLanguage, ...locales.targetLanguages].filter(Boolean);
|
|
532
|
+
console.log(chalk.green(` ✓ Locales: ${langs.join(', ')}`));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const confidencePercent = Math.round(confidence.overall * 100);
|
|
536
|
+
const confidenceColor = confidence.level === 'high' ? chalk.green : confidence.level === 'medium' ? chalk.yellow : chalk.red;
|
|
537
|
+
console.log(confidenceColor(` Detection confidence: ${confidencePercent}% (${confidence.level})`));
|
|
538
|
+
|
|
539
|
+
// Create config from intelligence
|
|
540
|
+
config = {
|
|
541
|
+
version: 1 as const,
|
|
542
|
+
sourceLanguage: locales.sourceLanguage || 'en',
|
|
543
|
+
targetLanguages: locales.targetLanguages,
|
|
544
|
+
localesDir: locales.localesDir || 'locales',
|
|
545
|
+
include: filePatterns.include,
|
|
546
|
+
exclude: filePatterns.exclude,
|
|
547
|
+
minTextLength: 1,
|
|
548
|
+
translation: { provider: 'manual' },
|
|
549
|
+
translationAdapter: {
|
|
550
|
+
module: framework.adapter || 'react-i18next',
|
|
551
|
+
hookName: framework.hookName || 'useTranslation',
|
|
552
|
+
},
|
|
553
|
+
keyGeneration: {
|
|
554
|
+
namespace: 'common',
|
|
555
|
+
shortHashLen: 6,
|
|
556
|
+
},
|
|
557
|
+
seedTargetLocales: false,
|
|
558
|
+
};
|
|
391
559
|
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (answers.setupMode === 'template') {
|
|
563
|
+
// Use template
|
|
564
|
+
console.log(chalk.blue(`📋 Applying ${answers.template} template...`));
|
|
565
|
+
const service = new ProjectIntelligenceService();
|
|
566
|
+
// Uses pre-detected intelligence from outer scope
|
|
567
|
+
const suggestedConfig = service.applyTemplate(answers.template!, intelligence || {
|
|
568
|
+
framework: { type: 'unknown', adapter: 'react-i18next', hookName: 'useTranslation', features: [], confidence: 0, evidence: [] },
|
|
569
|
+
locales: { sourceLanguage: 'en', targetLanguages: [], localesDir: 'locales', format: 'flat', existingFiles: [], existingKeyCount: 0, confidence: 0 },
|
|
570
|
+
filePatterns: { include: ['**/*.{ts,tsx,js,jsx}'], exclude: ['node_modules/**', 'dist/**'], sourceDirectories: [], hasTypeScript: false, hasJsx: false, hasVue: false, hasSvelte: false, sourceFileCount: 0, confidence: 0 },
|
|
571
|
+
existingSetup: { hasExistingConfig: false, hasExistingLocales: false, hasI18nProvider: false, runtimePackages: [], translationUsage: { hookName: 'useTranslation', translationIdentifier: 't', filesWithHooks: 0, translationCalls: 0, exampleFiles: [] } },
|
|
572
|
+
confidence: { framework: 0, filePatterns: 0, existingSetup: 0, locales: 0, overall: 0, level: 'low' },
|
|
573
|
+
warnings: [],
|
|
574
|
+
recommendations: [],
|
|
575
|
+
suggestedConfig: {
|
|
576
|
+
sourceLanguage: 'en',
|
|
577
|
+
targetLanguages: [],
|
|
578
|
+
localesDir: 'locales',
|
|
579
|
+
include: ['**/*.{ts,tsx,js,jsx}'],
|
|
580
|
+
exclude: ['node_modules/**', 'dist/**'],
|
|
581
|
+
translationAdapter: { module: 'react-i18next', hookName: 'useTranslation' },
|
|
582
|
+
keyGeneration: { namespace: 'common', shortHashLen: 6 }
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
config = {
|
|
587
|
+
version: 1 as const,
|
|
588
|
+
...suggestedConfig,
|
|
589
|
+
minTextLength: 1,
|
|
590
|
+
translation: { provider: 'manual' },
|
|
591
|
+
seedTargetLocales: false,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
392
594
|
|
|
393
|
-
|
|
595
|
+
if (answers.setupMode === 'manual' || !config) {
|
|
596
|
+
// Manual setup - use the existing prompts
|
|
597
|
+
console.log(chalk.blue('🔧 Manual configuration...'));
|
|
598
|
+
|
|
599
|
+
// Map the selected adapter preset to the correct module name and hook.
|
|
600
|
+
// The prompt offers 'react-i18next', 'vue-i18n', 'svelte-i18n', 'next-intl', and 'custom'.
|
|
601
|
+
// Only 'custom' uses the user-provided module specifier; all known presets
|
|
602
|
+
// are used directly as the adapter module name.
|
|
603
|
+
const adapterModule =
|
|
604
|
+
answers.adapterPreset === 'custom'
|
|
605
|
+
? answers.customAdapterModule?.trim()
|
|
606
|
+
: answers.adapterPreset; // Use actual selection (vue-i18n, svelte-i18n, etc.)
|
|
607
|
+
const ADAPTER_HOOKS: Record<string, string> = {
|
|
608
|
+
'react-i18next': 'useTranslation',
|
|
609
|
+
'vue-i18n': 'useI18n',
|
|
610
|
+
'svelte-i18n': 't',
|
|
611
|
+
'next-intl': 'useTranslations',
|
|
612
|
+
};
|
|
613
|
+
const adapterHook =
|
|
614
|
+
answers.adapterPreset === 'custom'
|
|
615
|
+
? (answers.customAdapterHook?.trim() || 'useTranslation')
|
|
616
|
+
: ADAPTER_HOOKS[answers.adapterPreset] || 'useTranslation';
|
|
617
|
+
|
|
618
|
+
const translationConfig: TranslationConfig =
|
|
619
|
+
answers.service === 'manual'
|
|
620
|
+
? { provider: 'manual' }
|
|
621
|
+
: {
|
|
622
|
+
provider: answers.service,
|
|
623
|
+
secretEnvVar: answers.translationSecretEnvVar?.trim() || undefined,
|
|
624
|
+
concurrency: 5,
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
config = {
|
|
628
|
+
version: 1 as const,
|
|
629
|
+
sourceLanguage: answers.sourceLanguage,
|
|
630
|
+
targetLanguages: parseGlobList(answers.targetLanguages),
|
|
631
|
+
localesDir: answers.localesDir,
|
|
632
|
+
include: parseGlobList(answers.include),
|
|
633
|
+
exclude: parseGlobList(answers.exclude),
|
|
634
|
+
minTextLength: parseInt(answers.minTextLength, 10),
|
|
635
|
+
translation: translationConfig,
|
|
636
|
+
translationAdapter: {
|
|
637
|
+
module: adapterModule ?? 'react-i18next',
|
|
638
|
+
hookName: adapterHook,
|
|
639
|
+
},
|
|
640
|
+
keyGeneration: {
|
|
641
|
+
namespace: answers.keyNamespace,
|
|
642
|
+
shortHashLen: parseInt(answers.shortHashLen, 10),
|
|
643
|
+
},
|
|
644
|
+
seedTargetLocales: answers.seedTargetLocales,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
394
647
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
648
|
+
const mergeDecision = await maybePromptMergeStrategy(config, workspaceRoot, Boolean(commandOptions.merge));
|
|
649
|
+
if (mergeDecision?.aborted) {
|
|
650
|
+
console.log(chalk.yellow('Aborting init to avoid overwriting existing i18n assets. Re-run with --merge to bypass.'));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
398
653
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
654
|
+
const configPath = path.join(workspaceRoot, 'i18n.config.json');
|
|
655
|
+
|
|
656
|
+
try {
|
|
657
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
658
|
+
console.log(chalk.green(`\nConfiguration created at ${configPath}`));
|
|
659
|
+
|
|
660
|
+
// Ensure .gitignore has i18nsmith artifacts
|
|
661
|
+
const gitignoreResult = await ensureGitignore(workspaceRoot);
|
|
662
|
+
if (gitignoreResult.updated) {
|
|
663
|
+
console.log(chalk.green(`Updated .gitignore with i18nsmith artifacts`));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Create locales directory and minimal source locale file so
|
|
667
|
+
// `i18nsmith check` doesn't immediately report missing source locale.
|
|
668
|
+
try {
|
|
669
|
+
const localesDirPath = path.join(workspaceRoot, config!.localesDir || 'locales');
|
|
670
|
+
await fs.mkdir(localesDirPath, { recursive: true });
|
|
671
|
+
const sourceLocalePath = path.join(localesDirPath, `${config!.sourceLanguage}.json`);
|
|
672
|
+
try {
|
|
673
|
+
await fs.access(sourceLocalePath);
|
|
674
|
+
} catch {
|
|
675
|
+
await fs.writeFile(sourceLocalePath, JSON.stringify({}, null, 2));
|
|
676
|
+
console.log(chalk.green(`✓ Created source locale file at ${sourceLocalePath}`));
|
|
403
677
|
}
|
|
678
|
+
} catch {
|
|
679
|
+
console.log(chalk.yellow('Could not create source locale file automatically.'));
|
|
680
|
+
}
|
|
404
681
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
682
|
+
if (answers.scaffoldAdapter && answers.scaffoldAdapterPath) {
|
|
683
|
+
try {
|
|
684
|
+
await scaffoldTranslationContext(answers.scaffoldAdapterPath, answers.sourceLanguage, {
|
|
685
|
+
localesDir: answers.localesDir,
|
|
686
|
+
});
|
|
687
|
+
console.log(chalk.green(`Translation context scaffolded at ${answers.scaffoldAdapterPath}`));
|
|
688
|
+
} catch (error) {
|
|
689
|
+
console.warn(chalk.yellow(`Skipping adapter scaffold: ${(error as Error).message}`));
|
|
414
690
|
}
|
|
691
|
+
}
|
|
415
692
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
693
|
+
if (
|
|
694
|
+
answers.adapterPreset === 'react-i18next' &&
|
|
695
|
+
answers.scaffoldReactRuntime &&
|
|
696
|
+
answers.reactI18nPath &&
|
|
697
|
+
answers.reactProviderPath
|
|
698
|
+
) {
|
|
699
|
+
try {
|
|
700
|
+
await scaffoldI18next(
|
|
701
|
+
answers.reactI18nPath,
|
|
702
|
+
answers.reactProviderPath,
|
|
703
|
+
answers.sourceLanguage,
|
|
704
|
+
answers.localesDir
|
|
705
|
+
);
|
|
706
|
+
console.log(chalk.green('react-i18next runtime scaffolded:'));
|
|
707
|
+
console.log(chalk.green(` • ${answers.reactI18nPath}`));
|
|
431
708
|
console.log(chalk.green(` • ${answers.reactProviderPath}`));
|
|
432
709
|
console.log(chalk.blue('\nWrap your app with the provider (e.g. Next.js providers.tsx):'));
|
|
433
710
|
console.log(
|