i18nsmith 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/audit.d.ts +3 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +180 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/backup.d.ts +6 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +85 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +151 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +235 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/debug-patterns.d.ts +3 -0
- package/dist/commands/debug-patterns.d.ts.map +1 -0
- package/dist/commands/debug-patterns.js +192 -0
- package/dist/commands/debug-patterns.js.map +1 -0
- package/dist/commands/debug-patterns.test.d.ts +2 -0
- package/dist/commands/debug-patterns.test.d.ts.map +1 -0
- package/dist/commands/debug-patterns.test.js +109 -0
- package/dist/commands/debug-patterns.test.js.map +1 -0
- package/dist/commands/diagnose.d.ts +3 -0
- package/dist/commands/diagnose.d.ts.map +1 -0
- package/dist/commands/diagnose.js +117 -0
- package/dist/commands/diagnose.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +450 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +2 -0
- package/dist/commands/init.test.d.ts.map +1 -0
- package/dist/commands/init.test.js +74 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/install-hooks.d.ts +3 -0
- package/dist/commands/install-hooks.d.ts.map +1 -0
- package/dist/commands/install-hooks.js +52 -0
- package/dist/commands/install-hooks.js.map +1 -0
- package/dist/commands/preflight.d.ts +7 -0
- package/dist/commands/preflight.d.ts.map +1 -0
- package/dist/commands/preflight.js +417 -0
- package/dist/commands/preflight.js.map +1 -0
- package/dist/commands/preflight.test.d.ts +5 -0
- package/dist/commands/preflight.test.d.ts.map +1 -0
- package/dist/commands/preflight.test.js +108 -0
- package/dist/commands/preflight.test.js.map +1 -0
- package/dist/commands/rename.d.ts +6 -0
- package/dist/commands/rename.d.ts.map +1 -0
- package/dist/commands/rename.js +204 -0
- package/dist/commands/rename.js.map +1 -0
- package/dist/commands/scaffold-adapter.d.ts +3 -0
- package/dist/commands/scaffold-adapter.d.ts.map +1 -0
- package/dist/commands/scaffold-adapter.js +204 -0
- package/dist/commands/scaffold-adapter.js.map +1 -0
- package/dist/commands/scaffold-adapter.test.d.ts +2 -0
- package/dist/commands/scaffold-adapter.test.d.ts.map +1 -0
- package/dist/commands/scaffold-adapter.test.js +102 -0
- package/dist/commands/scaffold-adapter.test.js.map +1 -0
- package/dist/commands/scan.d.ts +3 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +93 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/sync-seed.test.d.ts +2 -0
- package/dist/commands/sync-seed.test.d.ts.map +1 -0
- package/dist/commands/sync-seed.test.js +86 -0
- package/dist/commands/sync-seed.test.js.map +1 -0
- package/dist/commands/sync.d.ts +3 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +590 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/transform.d.ts +3 -0
- package/dist/commands/transform.d.ts.map +1 -0
- package/dist/commands/transform.js +114 -0
- package/dist/commands/transform.js.map +1 -0
- package/dist/commands/translate/csv-handler.d.ts +21 -0
- package/dist/commands/translate/csv-handler.d.ts.map +1 -0
- package/dist/commands/translate/csv-handler.js +270 -0
- package/dist/commands/translate/csv-handler.js.map +1 -0
- package/dist/commands/translate/executor.d.ts +31 -0
- package/dist/commands/translate/executor.d.ts.map +1 -0
- package/dist/commands/translate/executor.js +117 -0
- package/dist/commands/translate/executor.js.map +1 -0
- package/dist/commands/translate/index.d.ts +10 -0
- package/dist/commands/translate/index.d.ts.map +1 -0
- package/dist/commands/translate/index.js +170 -0
- package/dist/commands/translate/index.js.map +1 -0
- package/dist/commands/translate/reporter.d.ts +29 -0
- package/dist/commands/translate/reporter.d.ts.map +1 -0
- package/dist/commands/translate/reporter.js +103 -0
- package/dist/commands/translate/reporter.js.map +1 -0
- package/dist/commands/translate/types.d.ts +50 -0
- package/dist/commands/translate/types.d.ts.map +1 -0
- package/dist/commands/translate/types.js +5 -0
- package/dist/commands/translate/types.js.map +1 -0
- package/dist/commands/translate.d.ts +7 -0
- package/dist/commands/translate.d.ts.map +1 -0
- package/dist/commands/translate.js +7 -0
- package/dist/commands/translate.js.map +1 -0
- package/dist/commands/translate.test.d.ts +2 -0
- package/dist/commands/translate.test.d.ts.map +1 -0
- package/dist/commands/translate.test.js +118 -0
- package/dist/commands/translate.test.js.map +1 -0
- package/dist/e2e.test.d.ts +6 -0
- package/dist/e2e.test.d.ts.map +1 -0
- package/dist/e2e.test.js +376 -0
- package/dist/e2e.test.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.test.d.ts +6 -0
- package/dist/integration.test.d.ts.map +1 -0
- package/dist/integration.test.js +320 -0
- package/dist/integration.test.js.map +1 -0
- package/dist/utils/diagnostics-exit.d.ts +12 -0
- package/dist/utils/diagnostics-exit.d.ts.map +1 -0
- package/dist/utils/diagnostics-exit.js +49 -0
- package/dist/utils/diagnostics-exit.js.map +1 -0
- package/dist/utils/diagnostics-exit.test.d.ts +2 -0
- package/dist/utils/diagnostics-exit.test.d.ts.map +1 -0
- package/dist/utils/diagnostics-exit.test.js +40 -0
- package/dist/utils/diagnostics-exit.test.js.map +1 -0
- package/dist/utils/diff-utils.d.ts +4 -0
- package/dist/utils/diff-utils.d.ts.map +1 -0
- package/dist/utils/diff-utils.js +30 -0
- package/dist/utils/diff-utils.js.map +1 -0
- package/dist/utils/diff-utils.test.d.ts +2 -0
- package/dist/utils/diff-utils.test.d.ts.map +1 -0
- package/dist/utils/diff-utils.test.js +30 -0
- package/dist/utils/diff-utils.test.js.map +1 -0
- package/dist/utils/exit-codes.d.ts +142 -0
- package/dist/utils/exit-codes.d.ts.map +1 -0
- package/dist/utils/exit-codes.js +168 -0
- package/dist/utils/exit-codes.js.map +1 -0
- package/dist/utils/package-manager.d.ts +4 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +40 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/dist/utils/pkg.d.ts +3 -0
- package/dist/utils/pkg.d.ts.map +1 -0
- package/dist/utils/pkg.js +24 -0
- package/dist/utils/pkg.js.map +1 -0
- package/dist/utils/provider-injector.d.ts +36 -0
- package/dist/utils/provider-injector.d.ts.map +1 -0
- package/dist/utils/provider-injector.js +223 -0
- package/dist/utils/provider-injector.js.map +1 -0
- package/dist/utils/provider-injector.test.d.ts +2 -0
- package/dist/utils/provider-injector.test.d.ts.map +1 -0
- package/dist/utils/provider-injector.test.js +67 -0
- package/dist/utils/provider-injector.test.js.map +1 -0
- package/dist/utils/scaffold.d.ts +20 -0
- package/dist/utils/scaffold.d.ts.map +1 -0
- package/dist/utils/scaffold.js +197 -0
- package/dist/utils/scaffold.js.map +1 -0
- package/package.json +35 -0
- package/src/commands/audit.ts +234 -0
- package/src/commands/backup.ts +96 -0
- package/src/commands/check.ts +191 -0
- package/src/commands/config.ts +263 -0
- package/src/commands/debug-patterns.test.ts +134 -0
- package/src/commands/debug-patterns.ts +257 -0
- package/src/commands/diagnose.ts +136 -0
- package/src/commands/init.test.ts +82 -0
- package/src/commands/init.ts +536 -0
- package/src/commands/install-hooks.ts +66 -0
- package/src/commands/preflight.test.ts +139 -0
- package/src/commands/preflight.ts +488 -0
- package/src/commands/rename.ts +264 -0
- package/src/commands/scaffold-adapter.test.ts +110 -0
- package/src/commands/scaffold-adapter.ts +250 -0
- package/src/commands/scan.ts +125 -0
- package/src/commands/sync-seed.test.ts +116 -0
- package/src/commands/sync.ts +736 -0
- package/src/commands/transform.ts +151 -0
- package/src/commands/translate/README.md +75 -0
- package/src/commands/translate/csv-handler.ts +301 -0
- package/src/commands/translate/executor.ts +188 -0
- package/src/commands/translate/index.ts +220 -0
- package/src/commands/translate/reporter.ts +138 -0
- package/src/commands/translate/types.ts +56 -0
- package/src/commands/translate.test.ts +173 -0
- package/src/commands/translate.ts +6 -0
- package/src/e2e.test.ts +479 -0
- package/src/fixtures/README.md +61 -0
- package/src/fixtures/basic-react/i18n.config.json +15 -0
- package/src/fixtures/basic-react/locales/de.json +8 -0
- package/src/fixtures/basic-react/locales/en.json +8 -0
- package/src/fixtures/basic-react/locales/fr.json +8 -0
- package/src/fixtures/basic-react/src/App.tsx +15 -0
- package/src/fixtures/basic-react/src/Messages.tsx +12 -0
- package/src/fixtures/nested-locales/i18n.config.json +9 -0
- package/src/fixtures/nested-locales/locales/en.json +23 -0
- package/src/fixtures/nested-locales/locales/fr.json +23 -0
- package/src/fixtures/nested-locales/src/HomePage.tsx +13 -0
- package/src/fixtures/suspicious-keys/i18n.config.json +9 -0
- package/src/fixtures/suspicious-keys/locales/en.json +11 -0
- package/src/fixtures/suspicious-keys/locales/fr.json +11 -0
- package/src/fixtures/suspicious-keys/src/BadKeys.tsx +19 -0
- package/src/index.ts +43 -0
- package/src/integration.test.ts +438 -0
- package/src/utils/diagnostics-exit.test.ts +47 -0
- package/src/utils/diagnostics-exit.ts +63 -0
- package/src/utils/diff-utils.test.ts +36 -0
- package/src/utils/diff-utils.ts +42 -0
- package/src/utils/exit-codes.ts +201 -0
- package/src/utils/package-manager.ts +44 -0
- package/src/utils/pkg.ts +23 -0
- package/src/utils/provider-injector.test.ts +79 -0
- package/src/utils/provider-injector.ts +315 -0
- package/src/utils/scaffold.ts +240 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { diagnoseWorkspace, I18nConfig, TranslationConfig, ensureGitignore } from '@i18nsmith/core';
|
|
7
|
+
import { scaffoldTranslationContext, scaffoldI18next } from '../utils/scaffold.js';
|
|
8
|
+
import { hasDependency, readPackageJson } from '../utils/pkg.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a comma-separated list of glob patterns, respecting brace expansions.
|
|
12
|
+
* Brace-expanded globs like `src/**\/*.{ts,tsx}` are kept as a single token.
|
|
13
|
+
*/
|
|
14
|
+
export function parseGlobList(value: string): string[] {
|
|
15
|
+
const result: string[] = [];
|
|
16
|
+
let current = '';
|
|
17
|
+
let braceDepth = 0;
|
|
18
|
+
|
|
19
|
+
for (const char of value) {
|
|
20
|
+
if (char === '{') {
|
|
21
|
+
braceDepth++;
|
|
22
|
+
current += char;
|
|
23
|
+
} else if (char === '}') {
|
|
24
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
25
|
+
current += char;
|
|
26
|
+
} else if (char === ',' && braceDepth === 0) {
|
|
27
|
+
const trimmed = current.trim();
|
|
28
|
+
if (trimmed) result.push(trimmed);
|
|
29
|
+
current = '';
|
|
30
|
+
} else {
|
|
31
|
+
current += char;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const trimmed = current.trim();
|
|
36
|
+
if (trimmed) result.push(trimmed);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface InitCommandOptions {
|
|
41
|
+
merge?: boolean;
|
|
42
|
+
yes?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface InitAnswers {
|
|
46
|
+
sourceLanguage: string;
|
|
47
|
+
targetLanguages: string;
|
|
48
|
+
localesDir: string;
|
|
49
|
+
include: string;
|
|
50
|
+
exclude: string;
|
|
51
|
+
minTextLength: string;
|
|
52
|
+
service: 'google' | 'deepl' | 'manual';
|
|
53
|
+
translationSecretEnvVar?: string;
|
|
54
|
+
adapterPreset: 'react-i18next' | 'custom';
|
|
55
|
+
customAdapterModule?: string;
|
|
56
|
+
customAdapterHook?: string;
|
|
57
|
+
scaffoldAdapter: boolean;
|
|
58
|
+
scaffoldAdapterPath?: string;
|
|
59
|
+
scaffoldReactRuntime?: boolean;
|
|
60
|
+
reactI18nPath?: string;
|
|
61
|
+
reactProviderPath?: string;
|
|
62
|
+
keyNamespace: string;
|
|
63
|
+
shortHashLen: string;
|
|
64
|
+
seedTargetLocales: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Run init in non-interactive mode with sensible defaults.
|
|
69
|
+
* Auto-detects existing adapters and locales.
|
|
70
|
+
*/
|
|
71
|
+
async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promise<void> {
|
|
72
|
+
const workspaceRoot = process.cwd();
|
|
73
|
+
const configPath = path.join(workspaceRoot, 'i18n.config.json');
|
|
74
|
+
|
|
75
|
+
// Check if config already exists
|
|
76
|
+
try {
|
|
77
|
+
await fs.access(configPath);
|
|
78
|
+
if (!commandOptions.merge) {
|
|
79
|
+
console.log(chalk.yellow('Config file already exists. Use --merge to update existing config.'));
|
|
80
|
+
console.log(chalk.dim(` ${configPath}`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Config doesn't exist, proceed
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create a minimal config for diagnostics
|
|
88
|
+
const minimalConfig: I18nConfig = {
|
|
89
|
+
version: 1 as const,
|
|
90
|
+
sourceLanguage: 'en',
|
|
91
|
+
targetLanguages: [],
|
|
92
|
+
localesDir: 'locales',
|
|
93
|
+
include: ['src/**/*.{ts,tsx,js,jsx}'],
|
|
94
|
+
exclude: ['node_modules/**'],
|
|
95
|
+
minTextLength: 1,
|
|
96
|
+
translation: { provider: 'manual' },
|
|
97
|
+
translationAdapter: { module: 'react-i18next', hookName: 'useTranslation' },
|
|
98
|
+
keyGeneration: { namespace: 'common', shortHashLen: 6 },
|
|
99
|
+
seedTargetLocales: false,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Try to detect existing setup
|
|
103
|
+
let detectedAdapter: string | undefined;
|
|
104
|
+
let detectedLocales: string[] = [];
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const report = await diagnoseWorkspace(minimalConfig, { workspaceRoot });
|
|
108
|
+
|
|
109
|
+
// Detect adapter files
|
|
110
|
+
if (report.adapterFiles.length > 0) {
|
|
111
|
+
detectedAdapter = report.adapterFiles[0].path;
|
|
112
|
+
console.log(chalk.blue(`Detected adapter: ${detectedAdapter}`));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Detect existing locales
|
|
116
|
+
const existingLocales = report.localeFiles.filter(
|
|
117
|
+
(entry) => !entry.missing && !entry.parseError
|
|
118
|
+
);
|
|
119
|
+
if (existingLocales.length > 0) {
|
|
120
|
+
detectedLocales = existingLocales.map((entry) => entry.locale);
|
|
121
|
+
console.log(chalk.blue(`Detected locales: ${detectedLocales.join(', ')}`));
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.log(chalk.dim(`Could not run diagnostics: ${(error as Error).message}`));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Determine source and target languages from detected locales
|
|
128
|
+
let sourceLanguage = 'en';
|
|
129
|
+
let targetLanguages: string[] = [];
|
|
130
|
+
|
|
131
|
+
if (detectedLocales.length > 0) {
|
|
132
|
+
// Use 'en' as source if present, otherwise first detected locale
|
|
133
|
+
if (detectedLocales.includes('en')) {
|
|
134
|
+
sourceLanguage = 'en';
|
|
135
|
+
targetLanguages = detectedLocales.filter((l) => l !== 'en');
|
|
136
|
+
} else {
|
|
137
|
+
sourceLanguage = detectedLocales[0];
|
|
138
|
+
targetLanguages = detectedLocales.slice(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Build the config
|
|
143
|
+
const config: I18nConfig = {
|
|
144
|
+
version: 1 as const,
|
|
145
|
+
sourceLanguage,
|
|
146
|
+
targetLanguages,
|
|
147
|
+
localesDir: 'locales',
|
|
148
|
+
include: [
|
|
149
|
+
'src/**/*.{ts,tsx,js,jsx}',
|
|
150
|
+
'app/**/*.{ts,tsx,js,jsx}',
|
|
151
|
+
'pages/**/*.{ts,tsx,js,jsx}',
|
|
152
|
+
'components/**/*.{ts,tsx,js,jsx}',
|
|
153
|
+
],
|
|
154
|
+
exclude: ['node_modules/**', '**/*.test.*'],
|
|
155
|
+
minTextLength: 1,
|
|
156
|
+
translation: { provider: 'manual' },
|
|
157
|
+
translationAdapter: {
|
|
158
|
+
module: detectedAdapter ?? 'react-i18next',
|
|
159
|
+
hookName: 'useTranslation',
|
|
160
|
+
},
|
|
161
|
+
keyGeneration: {
|
|
162
|
+
namespace: 'common',
|
|
163
|
+
shortHashLen: 6,
|
|
164
|
+
},
|
|
165
|
+
seedTargetLocales: false,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
170
|
+
console.log(chalk.green(`\n✓ Configuration created at ${configPath}`));
|
|
171
|
+
console.log(chalk.dim(' Source language: ' + sourceLanguage));
|
|
172
|
+
if (targetLanguages.length > 0) {
|
|
173
|
+
console.log(chalk.dim(' Target languages: ' + targetLanguages.join(', ')));
|
|
174
|
+
}
|
|
175
|
+
if (detectedAdapter) {
|
|
176
|
+
console.log(chalk.dim(' Adapter: ' + detectedAdapter));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Ensure .gitignore has i18nsmith artifacts
|
|
180
|
+
const gitignoreResult = await ensureGitignore(workspaceRoot);
|
|
181
|
+
if (gitignoreResult.updated) {
|
|
182
|
+
console.log(chalk.green(`✓ Updated .gitignore with i18nsmith artifacts`));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log(chalk.blue('\nRun "i18nsmith check" to verify your setup.'));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(chalk.red('Failed to write configuration file:'), error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function registerInit(program: Command) {
|
|
192
|
+
program
|
|
193
|
+
.command('init')
|
|
194
|
+
.description('Initialize i18nsmith configuration')
|
|
195
|
+
.option('--merge', 'Merge with existing locales/runtimes when detected', false)
|
|
196
|
+
.option('-y, --yes', 'Skip prompts and use defaults (non-interactive mode)', false)
|
|
197
|
+
.action(async (commandOptions: InitCommandOptions) => {
|
|
198
|
+
console.log(chalk.blue('Initializing i18nsmith configuration...'));
|
|
199
|
+
|
|
200
|
+
// Non-interactive mode with sensible defaults
|
|
201
|
+
if (commandOptions.yes) {
|
|
202
|
+
await runNonInteractiveInit(commandOptions);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const answers = await inquirer.prompt<InitAnswers>([
|
|
207
|
+
{
|
|
208
|
+
type: 'input',
|
|
209
|
+
name: 'sourceLanguage',
|
|
210
|
+
message: 'What is the source language?',
|
|
211
|
+
default: 'en',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
type: 'input',
|
|
215
|
+
name: 'targetLanguages',
|
|
216
|
+
message: 'Which target languages do you need? (comma separated)',
|
|
217
|
+
default: 'fr',
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
type: 'input',
|
|
221
|
+
name: 'localesDir',
|
|
222
|
+
message: 'Where should locale files be stored?',
|
|
223
|
+
default: 'locales',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
type: 'input',
|
|
227
|
+
name: 'include',
|
|
228
|
+
message: 'Which files should be scanned? (comma separated glob patterns)',
|
|
229
|
+
default: 'src/**/*.{ts,tsx,js,jsx}, app/**/*.{ts,tsx,js,jsx}, pages/**/*.{ts,tsx,js,jsx}, components/**/*.{ts,tsx,js,jsx}',
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
type: 'input',
|
|
233
|
+
name: 'exclude',
|
|
234
|
+
message: 'Which files should be excluded? (comma separated glob patterns)',
|
|
235
|
+
default: 'node_modules/**,**/*.test.*',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
type: 'input',
|
|
239
|
+
name: 'minTextLength',
|
|
240
|
+
message: 'Minimum length for translatable text?',
|
|
241
|
+
default: '1',
|
|
242
|
+
validate: (input) => {
|
|
243
|
+
const num = parseInt(input, 10);
|
|
244
|
+
return !isNaN(num) && num >= 0 ? true : 'Please enter a non-negative number';
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
type: 'list',
|
|
249
|
+
name: 'service',
|
|
250
|
+
message: 'Which translation service do you want to use?',
|
|
251
|
+
choices: ['google', 'deepl', 'manual'],
|
|
252
|
+
default: 'google',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
type: 'input',
|
|
256
|
+
name: 'translationSecretEnvVar',
|
|
257
|
+
message: 'Name of the environment variable containing your translation API key',
|
|
258
|
+
when: (answers) => answers.service !== 'manual',
|
|
259
|
+
default: (answers: InitAnswers) =>
|
|
260
|
+
answers.service === 'deepl' ? 'DEEPL_API_KEY' : 'GOOGLE_TRANSLATE_API_KEY',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
type: 'list',
|
|
264
|
+
name: 'adapterPreset',
|
|
265
|
+
message: 'How should transformed components access translations?',
|
|
266
|
+
choices: [
|
|
267
|
+
{ name: 'react-i18next (default)', value: 'react-i18next' },
|
|
268
|
+
{ name: 'Custom hook/module', value: 'custom' },
|
|
269
|
+
],
|
|
270
|
+
default: 'react-i18next',
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
type: 'input',
|
|
274
|
+
name: 'customAdapterModule',
|
|
275
|
+
message: 'Provide the module specifier for your translation hook (e.g. "@/contexts/translation-context")',
|
|
276
|
+
when: (answers) => answers.adapterPreset === 'custom',
|
|
277
|
+
validate: (input) => (input && input.trim().length > 0 ? true : 'Module specifier cannot be empty'),
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
type: 'input',
|
|
281
|
+
name: 'customAdapterHook',
|
|
282
|
+
message: 'Name of the hook/function to import (default: useTranslation)',
|
|
283
|
+
when: (answers) => answers.adapterPreset === 'custom',
|
|
284
|
+
default: 'useTranslation',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
type: 'confirm',
|
|
288
|
+
name: 'scaffoldAdapter',
|
|
289
|
+
message: 'Scaffold a lightweight translation context file?',
|
|
290
|
+
when: (answers) => answers.adapterPreset === 'custom',
|
|
291
|
+
default: true,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
type: 'input',
|
|
295
|
+
name: 'scaffoldAdapterPath',
|
|
296
|
+
message: 'Path to scaffold the translation context file (relative to project root)',
|
|
297
|
+
when: (answers) => answers.scaffoldAdapter,
|
|
298
|
+
default: 'src/contexts/translation-context.tsx',
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
type: 'confirm',
|
|
302
|
+
name: 'scaffoldReactRuntime',
|
|
303
|
+
message: 'Scaffold i18next initializer and provider?',
|
|
304
|
+
when: (answers) => answers.adapterPreset === 'react-i18next',
|
|
305
|
+
default: true,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
type: 'input',
|
|
309
|
+
name: 'reactI18nPath',
|
|
310
|
+
message: 'Path for i18next initializer (e.g. src/lib/i18n.ts)',
|
|
311
|
+
when: (answers) => answers.scaffoldReactRuntime,
|
|
312
|
+
default: 'src/lib/i18n.ts',
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
type: 'input',
|
|
316
|
+
name: 'reactProviderPath',
|
|
317
|
+
message: 'Path for I18nProvider component (e.g. src/components/i18n-provider.tsx)',
|
|
318
|
+
when: (answers) => answers.scaffoldReactRuntime,
|
|
319
|
+
default: 'src/components/i18n-provider.tsx',
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
type: 'input',
|
|
323
|
+
name: 'keyNamespace',
|
|
324
|
+
message: 'Namespace prefix for generated keys',
|
|
325
|
+
default: 'common',
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
type: 'input',
|
|
329
|
+
name: 'shortHashLen',
|
|
330
|
+
message: 'Length of short hash suffix for keys',
|
|
331
|
+
default: '6',
|
|
332
|
+
validate: (input) => {
|
|
333
|
+
const num = parseInt(input, 10);
|
|
334
|
+
return !isNaN(num) && num > 0 ? true : 'Please enter a positive number';
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
type: 'confirm',
|
|
339
|
+
name: 'seedTargetLocales',
|
|
340
|
+
message: 'Seed target locale files with empty values?',
|
|
341
|
+
default: false,
|
|
342
|
+
},
|
|
343
|
+
]);
|
|
344
|
+
|
|
345
|
+
const adapterModule =
|
|
346
|
+
answers.adapterPreset === 'custom'
|
|
347
|
+
? answers.customAdapterModule?.trim()
|
|
348
|
+
: 'react-i18next';
|
|
349
|
+
const adapterHook =
|
|
350
|
+
answers.adapterPreset === 'custom'
|
|
351
|
+
? (answers.customAdapterHook?.trim() || 'useTranslation')
|
|
352
|
+
: 'useTranslation';
|
|
353
|
+
|
|
354
|
+
const translationConfig: TranslationConfig =
|
|
355
|
+
answers.service === 'manual'
|
|
356
|
+
? { provider: 'manual' }
|
|
357
|
+
: {
|
|
358
|
+
provider: answers.service,
|
|
359
|
+
secretEnvVar: answers.translationSecretEnvVar?.trim() || undefined,
|
|
360
|
+
concurrency: 5,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const config: I18nConfig = {
|
|
364
|
+
version: 1 as const,
|
|
365
|
+
sourceLanguage: answers.sourceLanguage,
|
|
366
|
+
targetLanguages: parseGlobList(answers.targetLanguages),
|
|
367
|
+
localesDir: answers.localesDir,
|
|
368
|
+
include: parseGlobList(answers.include),
|
|
369
|
+
exclude: parseGlobList(answers.exclude),
|
|
370
|
+
minTextLength: parseInt(answers.minTextLength, 10),
|
|
371
|
+
translation: translationConfig,
|
|
372
|
+
translationAdapter: {
|
|
373
|
+
module: adapterModule ?? 'react-i18next',
|
|
374
|
+
hookName: adapterHook,
|
|
375
|
+
},
|
|
376
|
+
keyGeneration: {
|
|
377
|
+
namespace: answers.keyNamespace,
|
|
378
|
+
shortHashLen: parseInt(answers.shortHashLen, 10),
|
|
379
|
+
},
|
|
380
|
+
seedTargetLocales: answers.seedTargetLocales,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const workspaceRoot = process.cwd();
|
|
384
|
+
const mergeDecision = await maybePromptMergeStrategy(config, workspaceRoot, Boolean(commandOptions.merge));
|
|
385
|
+
if (mergeDecision?.aborted) {
|
|
386
|
+
console.log(chalk.yellow('Aborting init to avoid overwriting existing i18n assets. Re-run with --merge to bypass.'));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const configPath = path.join(workspaceRoot, 'i18n.config.json');
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
394
|
+
console.log(chalk.green(`\nConfiguration created at ${configPath}`));
|
|
395
|
+
|
|
396
|
+
// Ensure .gitignore has i18nsmith artifacts
|
|
397
|
+
const gitignoreResult = await ensureGitignore(workspaceRoot);
|
|
398
|
+
if (gitignoreResult.updated) {
|
|
399
|
+
console.log(chalk.green(`Updated .gitignore with i18nsmith artifacts`));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (answers.scaffoldAdapter && answers.scaffoldAdapterPath) {
|
|
403
|
+
try {
|
|
404
|
+
await scaffoldTranslationContext(answers.scaffoldAdapterPath, answers.sourceLanguage, {
|
|
405
|
+
localesDir: answers.localesDir,
|
|
406
|
+
});
|
|
407
|
+
console.log(chalk.green(`Translation context scaffolded at ${answers.scaffoldAdapterPath}`));
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.warn(chalk.yellow(`Skipping adapter scaffold: ${(error as Error).message}`));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (
|
|
414
|
+
answers.adapterPreset === 'react-i18next' &&
|
|
415
|
+
answers.scaffoldReactRuntime &&
|
|
416
|
+
answers.reactI18nPath &&
|
|
417
|
+
answers.reactProviderPath
|
|
418
|
+
) {
|
|
419
|
+
try {
|
|
420
|
+
await scaffoldI18next(
|
|
421
|
+
answers.reactI18nPath,
|
|
422
|
+
answers.reactProviderPath,
|
|
423
|
+
answers.sourceLanguage,
|
|
424
|
+
answers.localesDir
|
|
425
|
+
);
|
|
426
|
+
console.log(chalk.green('react-i18next runtime scaffolded:'));
|
|
427
|
+
console.log(chalk.green(` • ${answers.reactI18nPath}`));
|
|
428
|
+
console.log(chalk.green(` • ${answers.reactProviderPath}`));
|
|
429
|
+
console.log(chalk.blue('\nWrap your app with the provider (e.g. Next.js providers.tsx):'));
|
|
430
|
+
console.log(
|
|
431
|
+
chalk.cyan(
|
|
432
|
+
`import { I18nProvider } from '${answers.reactProviderPath.replace(/\\/g, '/').replace(/\.tsx?$/, '')}';\n<I18nProvider>{children}</I18nProvider>`
|
|
433
|
+
)
|
|
434
|
+
);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.warn(chalk.yellow(`Skipping i18next scaffold: ${(error as Error).message}`));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (answers.adapterPreset === 'react-i18next') {
|
|
441
|
+
const pkg = await readPackageJson();
|
|
442
|
+
const missingDeps = ['react-i18next', 'i18next'].filter((dep) => !hasDependency(pkg, dep));
|
|
443
|
+
if (missingDeps.length) {
|
|
444
|
+
console.log(chalk.yellow('\nDependencies missing for react-i18next adapter:'));
|
|
445
|
+
missingDeps.forEach((dep) => console.log(chalk.yellow(` • ${dep}`)));
|
|
446
|
+
console.log(chalk.blue('Install them with:'));
|
|
447
|
+
console.log(chalk.cyan(' pnpm add react-i18next i18next'));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (mergeDecision?.strategy) {
|
|
452
|
+
console.log(
|
|
453
|
+
chalk.blue(
|
|
454
|
+
`Merge strategy selected: ${mergeDecision.strategy}. Use this when running i18nsmith sync or diagnose to reconcile locales.`
|
|
455
|
+
)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error(chalk.red('Failed to write configuration file:'), error);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
type MergeStrategy = 'keep-source' | 'overwrite' | 'interactive';
|
|
465
|
+
|
|
466
|
+
interface MergeDecision {
|
|
467
|
+
strategy: MergeStrategy | null;
|
|
468
|
+
aborted: boolean;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function maybePromptMergeStrategy(
|
|
472
|
+
config: I18nConfig,
|
|
473
|
+
workspaceRoot: string,
|
|
474
|
+
mergeRequested: boolean
|
|
475
|
+
): Promise<MergeDecision | null> {
|
|
476
|
+
try {
|
|
477
|
+
const report = await diagnoseWorkspace(config, { workspaceRoot });
|
|
478
|
+
type LocaleInsight = (typeof report.localeFiles)[number];
|
|
479
|
+
type ProviderInsight = (typeof report.providerFiles)[number];
|
|
480
|
+
const existingLocales = report.localeFiles.filter((entry: LocaleInsight) => !entry.missing && !entry.parseError);
|
|
481
|
+
const hasRuntime =
|
|
482
|
+
report.adapterFiles.length > 0 || report.providerFiles.some((provider: ProviderInsight) => provider.hasI18nProvider);
|
|
483
|
+
|
|
484
|
+
if (!existingLocales.length && !hasRuntime) {
|
|
485
|
+
return { strategy: null, aborted: false };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
console.log(chalk.yellow('\nExisting i18n assets detected:'));
|
|
489
|
+
if (existingLocales.length) {
|
|
490
|
+
const localeList = existingLocales.map((entry: LocaleInsight) => entry.locale).join(', ');
|
|
491
|
+
console.log(` • Locales: ${localeList}`);
|
|
492
|
+
}
|
|
493
|
+
if (hasRuntime) {
|
|
494
|
+
console.log(' • Runtime files already present.');
|
|
495
|
+
}
|
|
496
|
+
if (report.conflicts.length) {
|
|
497
|
+
for (const conflict of report.conflicts) {
|
|
498
|
+
console.log(chalk.red(` • ${conflict.message}`));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!mergeRequested) {
|
|
503
|
+
const { proceed } = await inquirer.prompt<{ proceed: boolean }>([
|
|
504
|
+
{
|
|
505
|
+
type: 'confirm',
|
|
506
|
+
name: 'proceed',
|
|
507
|
+
message: 'Merge with the existing setup instead of overwriting?',
|
|
508
|
+
default: true,
|
|
509
|
+
},
|
|
510
|
+
]);
|
|
511
|
+
if (!proceed) {
|
|
512
|
+
return { strategy: null, aborted: true };
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const { strategy } = await inquirer.prompt<{ strategy: MergeStrategy }>([
|
|
517
|
+
{
|
|
518
|
+
type: 'list',
|
|
519
|
+
name: 'strategy',
|
|
520
|
+
message: 'Choose a merge strategy for existing locale keys',
|
|
521
|
+
choices: [
|
|
522
|
+
{ name: 'Keep source values (append new keys only)', value: 'keep-source' },
|
|
523
|
+
{ name: 'Overwrite with placeholders (backup first)', value: 'overwrite' },
|
|
524
|
+
{ name: 'Interactive review during sync', value: 'interactive' },
|
|
525
|
+
],
|
|
526
|
+
default: 'keep-source',
|
|
527
|
+
},
|
|
528
|
+
]);
|
|
529
|
+
|
|
530
|
+
return { strategy, aborted: false };
|
|
531
|
+
} catch (error) {
|
|
532
|
+
console.warn(chalk.gray(`Skipping merge diagnostics: ${(error as Error).message}`));
|
|
533
|
+
return { strategy: null, aborted: false };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { detectPackageManager } from '../utils/package-manager.js';
|
|
5
|
+
|
|
6
|
+
interface InstallHooksOptions {
|
|
7
|
+
yes?: boolean;
|
|
8
|
+
force?: boolean;
|
|
9
|
+
skip?: boolean;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function huskyInstalled(root: string) {
|
|
14
|
+
try {
|
|
15
|
+
const pkgPath = path.join(root, 'package.json');
|
|
16
|
+
const pkgRaw = await fs.readFile(pkgPath, 'utf8');
|
|
17
|
+
return /"husky"/.test(pkgRaw);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function ensureHusky(root: string, pm: string, force: boolean) {
|
|
24
|
+
if (!force && await huskyInstalled(root)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const installCmd = pm === 'pnpm' ? 'pnpm add -D husky' : pm === 'yarn' ? 'yarn add -D husky' : 'npm install --save-dev husky';
|
|
28
|
+
console.log(`→ Installing husky: ${installCmd}`);
|
|
29
|
+
console.log(' (Run manually; automatic exec intentionally omitted in prototype)');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function writeHook(root: string, file: string, content: string) {
|
|
33
|
+
const hookPath = path.join(root, '.husky', file);
|
|
34
|
+
await fs.mkdir(path.dirname(hookPath), { recursive: true });
|
|
35
|
+
await fs.writeFile(hookPath, content, 'utf8');
|
|
36
|
+
console.log(`✔ Hook created: ${path.relative(root, hookPath)}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function registerInstallHooks(program: Command) {
|
|
40
|
+
const cmd = new Command('install-hooks')
|
|
41
|
+
.description('Prototype: scaffold Husky git hooks for i18nsmith checks')
|
|
42
|
+
.option('-y, --yes', 'Skip confirmations')
|
|
43
|
+
.option('--force', 'Force re-install husky even if present')
|
|
44
|
+
.option('--skip', 'Skip husky installation (just create hooks)')
|
|
45
|
+
.action(async (opts: InstallHooksOptions) => {
|
|
46
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
47
|
+
const pm = await detectPackageManager();
|
|
48
|
+
const hasHusky = await huskyInstalled(cwd);
|
|
49
|
+
|
|
50
|
+
if (!hasHusky && !opts.skip) {
|
|
51
|
+
await ensureHusky(cwd, pm, !!opts.force);
|
|
52
|
+
console.log('Add "prepare": "husky install" to package.json scripts if missing.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const preCommitContent = `#!/bin/sh\n. \"$(dirname "$0")/_/husky.sh\"\n[ -n \"$I18NSMITH_SKIP_HOOKS\" ] && exit 0\nNOCOLOR=1 npx i18nsmith check --fail-on conflicts || exit 1\n`;
|
|
56
|
+
const prePushContent = `#!/bin/sh\n. \"$(dirname "$0")/_/husky.sh\"\n[ -n \"$I18NSMITH_SKIP_HOOKS\" ] && exit 0\nNOCOLOR=1 npx i18nsmith sync --dry-run --check || exit 1\n`;
|
|
57
|
+
|
|
58
|
+
await writeHook(cwd, 'pre-commit', preCommitContent);
|
|
59
|
+
await writeHook(cwd, 'pre-push', prePushContent);
|
|
60
|
+
|
|
61
|
+
console.log('\nHooks added. Set I18NSMITH_SKIP_HOOKS=1 to bypass.');
|
|
62
|
+
console.log('Prototype complete – future versions will offer interactive selection & monorepo scoping.');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
program.addCommand(cmd);
|
|
66
|
+
}
|