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 +162 -11
- package/bin/dev.js +5 -5
- package/dist/commands/start.js +154 -0
- package/dist/commands/translate.js +249 -0
- package/dist/core/ast/extract-text.js +18 -67
- package/dist/core/ast/replace-text-with-text.js +20 -7
- package/dist/core/cache/translation-cache.js +98 -0
- package/dist/core/config/config-manager.js +163 -0
- package/dist/core/deduplication/deduplicator.js +43 -0
- package/dist/core/i18n/generate-key.js +72 -0
- package/dist/core/i18n/write-files.js +9 -5
- package/dist/core/scanner/file-scanner.js +53 -0
- package/dist/types/config.js +105 -0
- package/oclif.manifest.json +141 -1
- package/package.json +89 -85
package/README.md
CHANGED
|
@@ -51,7 +51,80 @@ Keys are stored inside:
|
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
54
|
-
## ⚡
|
|
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 (
|
|
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
|
-
## 📂
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
-
|
|
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
|
|
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
|
+
}
|