i18nizer 0.2.0 → 0.4.0-b

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/README.md CHANGED
@@ -51,7 +51,80 @@ Keys are stored inside:
51
51
 
52
52
  ---
53
53
 
54
- ## ⚡ Usage
54
+ ## ⚡ Quick Start
55
+
56
+ ### Initialize Your Project
57
+
58
+ #### Interactive Mode (Recommended)
59
+
60
+ ```bash
61
+ i18nizer start
62
+ ```
63
+
64
+ This launches an **interactive setup** that will:
65
+ - 🔍 Auto-detect your framework (Next.js or React)
66
+ - 🔍 Auto-detect your i18n library (next-intl, react-i18next, i18next)
67
+ - ❓ Ask you to confirm or change the detected settings
68
+ - ✅ Create `i18nizer.config.yml` with optimal defaults
69
+ - 📁 Set up `.i18nizer/` directory for caching and project data
70
+ - 📂 Create `messages/` directory for translation files
71
+
72
+ #### Non-Interactive Mode (CI/Automation)
73
+
74
+ ```bash
75
+ i18nizer start --yes
76
+ ```
77
+
78
+ Auto-detects and uses default values without prompts.
79
+
80
+ #### Manual Configuration
81
+
82
+ Specify framework and i18n library explicitly:
83
+
84
+ ```bash
85
+ # Next.js with next-intl
86
+ i18nizer start --framework nextjs --i18n next-intl
87
+
88
+ # React with react-i18next
89
+ i18nizer start --framework react --i18n react-i18next
90
+
91
+ # Custom setup
92
+ i18nizer start --framework custom --i18n custom
93
+ ```
94
+
95
+ **Available options:**
96
+ - `--framework`: `nextjs`, `react`, `custom`
97
+ - `--i18n`: `next-intl`, `react-i18next`, `i18next`, `custom`
98
+ - `--yes`, `-y`: Skip interactive prompts
99
+ - `--force`, `-f`: Re-initialize existing project
100
+
101
+ ### Translate Your Components
102
+
103
+ **Translate a single file:**
104
+
105
+ ```bash
106
+ i18nizer translate src/components/Login.tsx --locales en,es,fr
107
+ ```
108
+
109
+ **Translate all components in your project:**
110
+
111
+ ```bash
112
+ i18nizer translate --all --locales en,es,fr
113
+ ```
114
+
115
+ **Preview changes without modifying files:**
116
+
117
+ ```bash
118
+ i18nizer translate <file> --dry-run
119
+ ```
120
+
121
+ **Show generated JSON output:**
122
+
123
+ ```bash
124
+ i18nizer translate <file> --show-json
125
+ ```
126
+
127
+ ### Legacy Command (Still Supported)
55
128
 
56
129
  ```bash
57
130
  i18nizer extract <file-path> --locales en,es,fr --provider openai
@@ -102,7 +175,7 @@ export function Login() {
102
175
 
103
176
  ---
104
177
 
105
- ### Generated JSON (`.i18nizer/messages/en/Login.json`)
178
+ ### Generated JSON (`messages/en/Login.json`)
106
179
 
107
180
  ```json
108
181
  {
@@ -116,10 +189,30 @@ export function Login() {
116
189
 
117
190
  ---
118
191
 
119
- ## 📂 Output Structure
192
+ ## 📂 Project Structure
193
+
194
+ When initialized with `i18nizer start`:
195
+
196
+ ```
197
+ your-project/
198
+ ├─ i18nizer.config.yml # Configuration file
199
+ ├─ .i18nizer/
200
+ │ ├─ cache/
201
+ │ │ └─ translations.json # Translation cache
202
+ │ └─ ...
203
+ └─ messages/ # Translation files (configurable path)
204
+ ├─ en/
205
+ │ └─ Login.json
206
+ ├─ es/
207
+ │ └─ Login.json
208
+ └─ fr/
209
+ └─ Login.json
210
+ ```
211
+
212
+ Legacy standalone mode (without `i18nizer start`):
120
213
 
121
214
  ```
122
- .i18nizer/
215
+ [HOME]/.i18nizer/
123
216
  ├─ api-keys.json
124
217
  ├─ tsconfig.json
125
218
  └─ messages/
@@ -135,29 +228,87 @@ export function Login() {
135
228
 
136
229
  ## ✨ Features
137
230
 
231
+ ### Phase 1 (Current)
232
+
233
+ - **Project-level integration** with `i18nizer start` and `i18nizer translate`
234
+ - **Configuration system** with `i18nizer.config.yml`
235
+ - **Framework presets** (Next.js + next-intl, React + react-i18next)
236
+ - **Intelligent caching** to avoid redundant AI translation requests
237
+ - **String deduplication** with deterministic key reuse
238
+ - **Configurable behavior** (allowed functions, props, member functions)
239
+ - **Dry-run mode** to preview changes
240
+ - **JSON output preview** with `--show-json`
241
+ - Project-wide or single-file translation
138
242
  - Works with **JSX & TSX**
139
243
  - Rewrites components automatically (`t("key")`)
140
244
  - Always generates **English camelCase keys**
141
245
  - Supports **any number of locales**
142
246
  - Isolated TypeScript parsing (no project tsconfig required)
143
- - Friendly logs, and errors
247
+ - Friendly logs with colors and spinners
248
+
249
+ ### Supported Extraction Cases
250
+
251
+ - **JSX text children**: `<div>Hello</div>`
252
+ - **JSX attributes**: `placeholder`, `title`, `alt`, `aria-label`, `aria-placeholder`, `label`, `text`, `tooltip`, `helperText`
253
+ - **String literals**: `placeholder="Enter name"`
254
+ - **Curly-braced strings**: `placeholder={"Enter name"}`
255
+ - **Template literals**: `` placeholder={`Enter name`} ``
256
+ - **Template literals with placeholders**: `` <p>{`Hello ${name}`}</p> ``
257
+ - **Ternary operators**: `placeholder={condition ? "Text A" : "Text B"}`
258
+ - **Logical AND**: `{condition && "Visible text"}`
259
+ - **Logical OR**: `{condition || "Fallback text"}`
260
+ - **Nested expressions**: Complex combinations of the above
261
+
262
+ ### Filtering & Quality
263
+
264
+ - **Skips non-translatable content**: Single symbols (`*`, `|`), punctuation-only strings (`...`), whitespace
265
+ - **Deterministic key generation**: Same input always produces the same key
266
+ - **Stable JSON output**: Alphabetically sorted keys, consistent formatting
267
+ - **2-space indentation**: Clean and diff-friendly JSON files
144
268
 
145
269
  ---
146
270
 
147
271
  ## 🔮 Roadmap
148
272
 
149
- - Configurable output directory
150
- - Framework support (Vue, Svelte)
151
- - i18n library presets (`next-intl`, `react-i18next`)
152
- - Watch mode
153
- - Non-AI fallback mode
273
+ ### Phase 0: Foundation & Reliability (Complete)
274
+ - Stable extraction and replacement
275
+ - Deterministic key generation
276
+ - Comprehensive test coverage
277
+ - JSON output quality
278
+
279
+ ### ✅ Phase 1: Project Integration (Complete)
280
+ - `i18nizer start` command for project initialization
281
+ - `i18nizer translate` command with `--all` flag
282
+ - Configuration system with YAML
283
+ - Framework detection and presets
284
+ - Intelligent caching system
285
+ - Cross-file string deduplication
286
+ - Configurable behavior (allowed props, functions, etc.)
287
+ - Dry-run and JSON preview modes
288
+
289
+ ### 🚧 Phase 2: Advanced Features (Planned)
290
+ - [ ] Watch mode for continuous translation
291
+ - [ ] Non-AI fallback mode
292
+ - [ ] Framework support (Vue, Svelte)
293
+ - [ ] Additional i18n library presets
294
+ - [ ] Pluralization support
295
+ - [ ] Context-aware translations
296
+ - [ ] Translation memory and glossary
297
+
298
+ ---
299
+
300
+ ## ⚠️ Current Limitations
301
+
302
+ - AI-generated keys may vary between runs (deterministic fallback available)
303
+ - Only supports React JSX/TSX (no Vue, Svelte yet)
304
+ - Does not handle runtime-only string generation
154
305
 
155
306
  ---
156
307
 
157
308
  ## ⚠️ Notes
158
309
 
159
310
  - API keys are **never committed**
160
- - JSON files are stored per project in `.i18nizer/`
311
+ - JSON files are stored in `/{HOME}/.i18nizer/`
161
312
  - Designed for incremental adoption
162
313
 
163
314
  ---
package/bin/dev.js CHANGED
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
2
-
3
- import {execute} from '@oclif/core'
4
-
5
- await execute({development: true, dir: import.meta.url})
1
+ #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
2
+
3
+ import {execute} from '@oclif/core'
4
+
5
+ await execute({development: true, dir: import.meta.url})
@@ -0,0 +1,154 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import chalk from "chalk";
3
+ import inquirer from "inquirer";
4
+ import ora from "ora";
5
+ import { detectFramework, detectI18nLibrary, generateConfig, getMessagesDir, getProjectDir, isProjectInitialized, normalizeI18nLibrary, writeConfig, } from "../core/config/config-manager.js";
6
+ export default class Start extends Command {
7
+ static description = "🚀 Initialize i18nizer in your project with framework presets";
8
+ static flags = {
9
+ force: Flags.boolean({
10
+ char: "f",
11
+ default: false,
12
+ description: "Force re-initialization even if config exists",
13
+ }),
14
+ framework: Flags.string({
15
+ description: "Framework (nextjs, react, custom)",
16
+ options: ["nextjs", "react", "custom"],
17
+ }),
18
+ i18n: Flags.string({
19
+ description: "i18n library (next-intl, react-i18next, i18next, custom)",
20
+ options: ["next-intl", "react-i18next", "i18next", "custom"],
21
+ }),
22
+ preset: Flags.string({
23
+ char: "p",
24
+ deprecated: {
25
+ message: "Use --framework instead",
26
+ version: "0.4.0",
27
+ },
28
+ description: "Framework preset (deprecated, use --framework)",
29
+ options: ["nextjs", "react", "custom"],
30
+ }),
31
+ yes: Flags.boolean({
32
+ char: "y",
33
+ default: false,
34
+ description: "Skip interactive prompts and use detected/default values",
35
+ }),
36
+ };
37
+ async run() {
38
+ const { flags } = await this.parse(Start);
39
+ const cwd = process.cwd();
40
+ // Check if already initialized
41
+ if (isProjectInitialized(cwd) && !flags.force) {
42
+ this.log(chalk.yellow("⚠️ Project is already initialized!"));
43
+ this.log(chalk.cyan("💡 Use"), chalk.bold("--force"), chalk.cyan("to re-initialize"));
44
+ return;
45
+ }
46
+ let framework;
47
+ let i18nLibrary;
48
+ // Handle deprecated --preset flag
49
+ if (flags.preset) {
50
+ framework = flags.preset;
51
+ }
52
+ else if (flags.framework) {
53
+ framework = flags.framework;
54
+ }
55
+ else {
56
+ // Detect framework and i18n library
57
+ const detectedFramework = detectFramework(cwd);
58
+ const detectedI18n = detectI18nLibrary(cwd);
59
+ if (flags.yes) {
60
+ // Non-interactive mode: use detected values
61
+ framework = detectedFramework;
62
+ i18nLibrary = detectedI18n ?? undefined;
63
+ this.log(chalk.cyan("🔍 Auto-detected:"));
64
+ this.log(` Framework: ${chalk.bold(framework)}`);
65
+ if (i18nLibrary) {
66
+ this.log(` i18n Library: ${chalk.bold(i18nLibrary)}`);
67
+ }
68
+ }
69
+ else {
70
+ // Interactive mode: ask user
71
+ const answers = await inquirer.prompt([
72
+ {
73
+ choices: [
74
+ { name: "Next.js", value: "nextjs" },
75
+ { name: "React", value: "react" },
76
+ { name: "Custom", value: "custom" },
77
+ ],
78
+ default: detectedFramework,
79
+ message: "What framework are you using?",
80
+ name: "framework",
81
+ type: "list",
82
+ },
83
+ {
84
+ choices: [
85
+ { name: "next-intl", value: "next-intl" },
86
+ { name: "react-i18next", value: "react-i18next" },
87
+ { name: "i18next", value: "i18next" },
88
+ { name: "Custom / None", value: "custom" },
89
+ ],
90
+ default: detectedI18n ?? "custom",
91
+ message: "Which i18n library are you using?",
92
+ name: "i18nLibrary",
93
+ type: "list",
94
+ },
95
+ ]);
96
+ framework = answers.framework;
97
+ i18nLibrary = normalizeI18nLibrary(answers.i18nLibrary);
98
+ }
99
+ }
100
+ // Override with CLI flag if provided
101
+ if (flags.i18n) {
102
+ i18nLibrary = normalizeI18nLibrary(flags.i18n);
103
+ }
104
+ const spinner = ora("Initializing i18nizer...").start();
105
+ try {
106
+ // Generate config
107
+ spinner.text = "Generating configuration...";
108
+ const config = generateConfig(framework, i18nLibrary);
109
+ // Create project directory
110
+ const projectDir = getProjectDir(cwd);
111
+ spinner.succeed(`✅ Created ${chalk.cyan(".i18nizer/")} directory`);
112
+ // Write config file
113
+ spinner.start("Writing configuration file...");
114
+ writeConfig(cwd, config);
115
+ let presetDescription = framework;
116
+ if (i18nLibrary) {
117
+ presetDescription += ` + ${i18nLibrary}`;
118
+ }
119
+ spinner.succeed(`✅ Created ${chalk.cyan("i18nizer.config.yml")} with ${chalk.bold(presetDescription)} preset`);
120
+ // Create messages directory
121
+ spinner.start("Setting up messages directory...");
122
+ const messagesDir = getMessagesDir(cwd, config);
123
+ spinner.succeed(`✅ Created ${chalk.cyan(config.messages.path + "/")} directory`);
124
+ // Summary
125
+ this.log("");
126
+ this.log(chalk.green("🎉 i18nizer initialized successfully!"));
127
+ this.log("");
128
+ this.log(chalk.bold("📋 Configuration:"));
129
+ this.log(` Framework: ${chalk.cyan(config.framework)}`);
130
+ if (config.i18nLibrary) {
131
+ this.log(` i18n Library: ${chalk.cyan(config.i18nLibrary)}`);
132
+ }
133
+ this.log(` i18n Function: ${chalk.cyan(config.i18n.function)}`);
134
+ this.log(` Import: ${chalk.cyan(config.i18n.import.named)} from "${chalk.cyan(config.i18n.import.source)}"`);
135
+ this.log(` Messages Path: ${chalk.cyan(config.messages.path)}`);
136
+ this.log(` Default Locale: ${chalk.cyan(config.messages.defaultLocale)}`);
137
+ this.log("");
138
+ this.log(chalk.bold("🚀 Next steps:"));
139
+ this.log(` 1. Review and customize ${chalk.cyan("i18nizer.config.yml")}`);
140
+ this.log(` 2. Run ${chalk.cyan("i18nizer translate --all")} to translate all components`);
141
+ this.log(` 3. Or translate a single file: ${chalk.cyan("i18nizer translate <file>")}`);
142
+ this.log("");
143
+ }
144
+ catch (error) {
145
+ spinner.fail("❌ Initialization failed");
146
+ if (error instanceof Error) {
147
+ this.error(error.message);
148
+ }
149
+ else {
150
+ this.error("An unknown error occurred");
151
+ }
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,249 @@
1
+ import { Args, Command, Flags } from "@oclif/core";
2
+ import chalk from "chalk";
3
+ import path from "node:path";
4
+ import ora from "ora";
5
+ import { generateTranslations } from "../core/ai/client.js";
6
+ import { buildPrompt } from "../core/ai/promt.js";
7
+ import { extractTexts } from "../core/ast/extract-text.js";
8
+ import { insertUseTranslations } from "../core/ast/insert-user-translations.js";
9
+ import { parseFile } from "../core/ast/parse-file.js";
10
+ import { replaceTempKeysWithT } from "../core/ast/replace-text-with-text.js";
11
+ import { TranslationCache } from "../core/cache/translation-cache.js";
12
+ import { detectFramework, generateConfig, getMessagesDir, getProjectDir, isProjectInitialized, loadConfig, } from "../core/config/config-manager.js";
13
+ import { Deduplicator } from "../core/deduplication/deduplicator.js";
14
+ import { parseAiJson } from "../core/i18n/parse-ai-json.js";
15
+ import { saveSourceFile } from "../core/i18n/sace-source-file.js";
16
+ import { writeLocaleFiles } from "../core/i18n/write-files.js";
17
+ import { findProjectComponents } from "../core/scanner/file-scanner.js";
18
+ const VALID_PROVIDERS = ["gemini", "huggingface", "openai"];
19
+ export default class Translate extends Command {
20
+ static args = {
21
+ file: Args.string({
22
+ description: "Path to a specific TSX/JSX file (optional, use --all for project-level)",
23
+ }),
24
+ };
25
+ static description = "🌍 Extract and translate strings from components (project-level or standalone)";
26
+ static flags = {
27
+ all: Flags.boolean({
28
+ char: "a",
29
+ default: false,
30
+ description: "Translate all components in the project",
31
+ }),
32
+ "dry-run": Flags.boolean({
33
+ char: "d",
34
+ default: false,
35
+ description: "Preview changes without writing files",
36
+ }),
37
+ locales: Flags.string({
38
+ char: "l",
39
+ description: "Locales to generate (comma-separated, e.g., en,es,fr)",
40
+ }),
41
+ provider: Flags.string({
42
+ char: "p",
43
+ description: "AI provider (gemini | huggingface | openai)",
44
+ }),
45
+ "show-json": Flags.boolean({
46
+ char: "j",
47
+ default: false,
48
+ description: "Display generated translation JSON output",
49
+ }),
50
+ };
51
+ async run() {
52
+ const { args, flags } = await this.parse(Translate);
53
+ const cwd = process.cwd();
54
+ // Validate input
55
+ if (!flags.all && !args.file) {
56
+ this.error("❌ Please specify a file or use --all to translate all components");
57
+ }
58
+ if (flags.all && args.file) {
59
+ this.error("❌ Cannot specify both --all and a file path");
60
+ }
61
+ // Load or generate config
62
+ let config;
63
+ const isInitialized = isProjectInitialized(cwd);
64
+ if (isInitialized) {
65
+ config = loadConfig(cwd);
66
+ this.log(chalk.cyan("📋 Using project configuration"));
67
+ }
68
+ else {
69
+ // Standalone mode: use defaults
70
+ const framework = detectFramework(cwd);
71
+ config = generateConfig(framework);
72
+ this.log(chalk.yellow("⚠️ Project not initialized, using defaults"));
73
+ this.log(chalk.cyan("💡 Run"), chalk.bold("i18nizer start"), chalk.cyan("to initialize the project"));
74
+ }
75
+ // Override config with flags if provided
76
+ const locales = flags.locales
77
+ ? flags.locales.split(",")
78
+ : [config.messages.defaultLocale, "es"]; // Default fallback
79
+ let provider = "huggingface";
80
+ if (flags.provider) {
81
+ const p = flags.provider.toLowerCase();
82
+ if (!VALID_PROVIDERS.includes(p)) {
83
+ this.error(`❌ Invalid provider: ${flags.provider}. Valid options: ${VALID_PROVIDERS.join(", ")}`);
84
+ }
85
+ provider = p;
86
+ }
87
+ // Get files to process
88
+ const filesToProcess = flags.all
89
+ ? findProjectComponents(cwd)
90
+ : [path.resolve(cwd, args.file)];
91
+ if (filesToProcess.length === 0) {
92
+ this.log(chalk.yellow("⚠️ No component files found"));
93
+ return;
94
+ }
95
+ this.log(chalk.cyan("📂 Files to process:"), chalk.bold(filesToProcess.length));
96
+ this.log(chalk.cyan("🌐 Locales:"), locales.join(", "));
97
+ this.log(chalk.cyan("🤖 Provider:"), provider);
98
+ if (flags["dry-run"]) {
99
+ this.log(chalk.yellow("\n🔍 DRY RUN MODE - No files will be modified\n"));
100
+ }
101
+ // Initialize cache and deduplicator
102
+ const projectDir = getProjectDir(cwd);
103
+ const cache = new TranslationCache(projectDir);
104
+ const deduplicator = new Deduplicator(cache);
105
+ let totalExtracted = 0;
106
+ let totalReused = 0;
107
+ let totalCached = 0;
108
+ // Process each file
109
+ for (const filePath of filesToProcess) {
110
+ const componentName = path.basename(filePath).replace(/\.(tsx|jsx)$/, "");
111
+ const spinner = ora(`Processing ${componentName}...`).start();
112
+ try {
113
+ // Parse and extract
114
+ const sourceFile = parseFile(filePath);
115
+ const texts = extractTexts(sourceFile, {
116
+ allowedFunctions: config.behavior.allowedFunctions,
117
+ allowedMemberFunctions: config.behavior.allowedMemberFunctions,
118
+ allowedProps: config.behavior.allowedProps,
119
+ });
120
+ if (texts.length === 0) {
121
+ spinner.info(`⏭️ ${componentName}: No translatable texts found`);
122
+ continue;
123
+ }
124
+ totalExtracted += texts.length;
125
+ // Deduplicate and assign keys
126
+ const mappedTexts = texts.map((t) => {
127
+ const result = deduplicator.deduplicate(t.text, componentName, config.behavior.detectDuplicates);
128
+ if (result.isReused)
129
+ totalReused++;
130
+ if (result.isCached)
131
+ totalCached++;
132
+ return {
133
+ key: result.key,
134
+ node: t.node,
135
+ placeholders: t.placeholders,
136
+ tempKey: t.tempKey,
137
+ text: t.text,
138
+ isCached: result.isCached,
139
+ };
140
+ });
141
+ // Build translations JSON
142
+ const i18nJson = {};
143
+ for (const mapped of mappedTexts) {
144
+ i18nJson[mapped.key] = {};
145
+ if (mapped.isCached) {
146
+ // Use cached translations
147
+ const cached = cache.get(mapped.text);
148
+ for (const locale of locales) {
149
+ i18nJson[mapped.key][locale] =
150
+ cached.locales[locale] ?? mapped.text;
151
+ }
152
+ }
153
+ else {
154
+ // Will need AI translation
155
+ for (const locale of locales) {
156
+ i18nJson[mapped.key][locale] = ""; // Placeholder
157
+ }
158
+ }
159
+ }
160
+ // Get AI translations for non-cached texts
161
+ const textsNeedingTranslation = mappedTexts.filter((t) => !t.isCached);
162
+ if (textsNeedingTranslation.length > 0) {
163
+ spinner.text = `${componentName}: Generating translations with ${provider}...`;
164
+ const prompt = buildPrompt({
165
+ componentName,
166
+ locales,
167
+ texts: textsNeedingTranslation.map((t) => ({
168
+ tempKey: t.tempKey,
169
+ text: t.text,
170
+ })),
171
+ });
172
+ const raw = await generateTranslations(prompt, provider);
173
+ if (!raw)
174
+ throw new Error("AI did not return any data");
175
+ const aiJson = parseAiJson(raw);
176
+ const namespace = aiJson[componentName];
177
+ // Merge AI translations
178
+ for (const mapped of textsNeedingTranslation) {
179
+ const aiTranslations = namespace[mapped.tempKey];
180
+ if (aiTranslations) {
181
+ const aiKey = aiTranslations.key;
182
+ for (const locale of locales) {
183
+ i18nJson[mapped.key][locale] = aiTranslations[locale] ?? mapped.text;
184
+ }
185
+ // Update cache
186
+ cache.set({
187
+ componentName,
188
+ key: mapped.key,
189
+ locales: i18nJson[mapped.key],
190
+ text: mapped.text,
191
+ });
192
+ }
193
+ }
194
+ }
195
+ // Show JSON if requested
196
+ if (flags["show-json"]) {
197
+ this.log("\n" + chalk.cyan(`${componentName} JSON:`));
198
+ this.log(JSON.stringify({ [componentName]: i18nJson }, null, 2));
199
+ this.log("");
200
+ }
201
+ // Write files (unless dry-run)
202
+ if (!flags["dry-run"]) {
203
+ // Write locale files
204
+ if (isInitialized) {
205
+ const messagesDir = getMessagesDir(cwd, config);
206
+ writeLocaleFiles(componentName, { [componentName]: i18nJson }, locales, messagesDir);
207
+ }
208
+ else {
209
+ // Standalone mode: use home directory
210
+ writeLocaleFiles(componentName, { [componentName]: i18nJson }, locales);
211
+ }
212
+ // Rewrite component
213
+ insertUseTranslations(sourceFile, componentName);
214
+ replaceTempKeysWithT(mappedTexts.map((m) => ({
215
+ key: m.key,
216
+ node: m.node,
217
+ placeholders: m.placeholders,
218
+ tempKey: m.tempKey,
219
+ })), {
220
+ allowedFunctions: config.behavior.allowedFunctions,
221
+ allowedMemberFunctions: config.behavior.allowedMemberFunctions,
222
+ allowedProps: config.behavior.allowedProps,
223
+ });
224
+ saveSourceFile(sourceFile);
225
+ }
226
+ spinner.succeed(`✅ ${componentName}: ${texts.length} strings (${totalReused} reused, ${totalCached} cached)`);
227
+ }
228
+ catch (error) {
229
+ spinner.fail(`❌ ${componentName}: Failed`);
230
+ if (error instanceof Error) {
231
+ this.error(error.message);
232
+ }
233
+ }
234
+ }
235
+ // Save cache
236
+ if (!flags["dry-run"]) {
237
+ cache.save();
238
+ }
239
+ // Summary
240
+ this.log("");
241
+ this.log(chalk.green("🎉 Translation complete!"));
242
+ this.log(chalk.cyan("📊 Summary:"));
243
+ this.log(` Files processed: ${chalk.bold(filesToProcess.length)}`);
244
+ this.log(` Strings extracted: ${chalk.bold(totalExtracted)}`);
245
+ this.log(` Keys reused: ${chalk.bold(totalReused)}`);
246
+ this.log(` Cached translations: ${chalk.bold(totalCached)}`);
247
+ this.log("");
248
+ }
249
+ }