i18nizer 0.4.0-b → 0.5.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/README.md +90 -1
- package/dist/commands/translate.js +14 -9
- package/dist/core/ai/client.js +1 -1
- package/dist/core/ai/generate-english-key.js +46 -0
- package/dist/core/config/config-manager.js +9 -5
- package/dist/core/deduplication/deduplicator.js +17 -3
- package/dist/types/config.js +2 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -226,6 +226,94 @@ Legacy standalone mode (without `i18nizer start`):
|
|
|
226
226
|
|
|
227
227
|
---
|
|
228
228
|
|
|
229
|
+
## ⚙️ Configuration
|
|
230
|
+
|
|
231
|
+
### Translation Function Injection (`autoInjectT`)
|
|
232
|
+
|
|
233
|
+
i18nizer can automatically inject translation hooks into your components:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const t = useTranslations("ComponentName");
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
This behavior is controlled by `behavior.autoInjectT` in `i18nizer.config.yml`:
|
|
240
|
+
|
|
241
|
+
**Next.js Projects** (disabled by default):
|
|
242
|
+
```yaml
|
|
243
|
+
behavior:
|
|
244
|
+
autoInjectT: false # Disabled to avoid breaking Server Components
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
- Server Components cannot use hooks like `useTranslations`
|
|
248
|
+
- i18nizer will replace strings with `t("key")` but won't inject the hook
|
|
249
|
+
- You manually add the translation hook where appropriate
|
|
250
|
+
|
|
251
|
+
**React Projects** (enabled by default):
|
|
252
|
+
```yaml
|
|
253
|
+
behavior:
|
|
254
|
+
autoInjectT: true # Safe for Client Components
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
- Full automation: injects hooks and replaces strings
|
|
258
|
+
- Works seamlessly with React components
|
|
259
|
+
|
|
260
|
+
**Why disabled for Next.js?**
|
|
261
|
+
- Automatically detecting Server vs Client Components is ambiguous
|
|
262
|
+
- Injecting hooks in Server Components causes runtime errors
|
|
263
|
+
- User has full control over translation function placement
|
|
264
|
+
|
|
265
|
+
You can override this setting in your `i18nizer.config.yml` if you know your setup.
|
|
266
|
+
|
|
267
|
+
### AI-Powered English Key Generation (`useAiForKeys`)
|
|
268
|
+
|
|
269
|
+
i18nizer uses AI to generate **English camelCase keys** regardless of your source language:
|
|
270
|
+
|
|
271
|
+
```yaml
|
|
272
|
+
behavior:
|
|
273
|
+
useAiForKeys: true # Default: enabled
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Benefits:**
|
|
277
|
+
- **Consistent keys**: Keys are always in English, even if your source text is in Spanish, French, German, etc.
|
|
278
|
+
- **Readable**: `welcomeBack` instead of `bienvenidoDeNuevo`
|
|
279
|
+
- **Stable**: Keys are cached per source text for deterministic behavior across runs
|
|
280
|
+
- **Minimal diffs**: Same source text always produces the same key
|
|
281
|
+
|
|
282
|
+
**How it works:**
|
|
283
|
+
1. First run: AI generates an English key for each source text
|
|
284
|
+
2. Key is cached with the source text hash
|
|
285
|
+
3. Subsequent runs: Cached key is reused (no AI call needed)
|
|
286
|
+
4. Fallback: If AI is unavailable, uses deterministic camelCase generation
|
|
287
|
+
|
|
288
|
+
**Example:**
|
|
289
|
+
|
|
290
|
+
Source text (Spanish):
|
|
291
|
+
```tsx
|
|
292
|
+
<h1>Bienvenido de nuevo</h1>
|
|
293
|
+
<button>Iniciar sesión</button>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Generated keys (English):
|
|
297
|
+
```json
|
|
298
|
+
{
|
|
299
|
+
"welcomeBack": "Bienvenido de nuevo",
|
|
300
|
+
"signIn": "Iniciar sesión"
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Disabling AI key generation:**
|
|
305
|
+
|
|
306
|
+
If you prefer deterministic key generation based on source text:
|
|
307
|
+
|
|
308
|
+
```yaml
|
|
309
|
+
behavior:
|
|
310
|
+
useAiForKeys: false
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Note: Keys will be in the source language (e.g., `bienvenidoDeNuevo` for Spanish text).
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
229
317
|
## ✨ Features
|
|
230
318
|
|
|
231
319
|
### Phase 1 (Current)
|
|
@@ -235,13 +323,14 @@ Legacy standalone mode (without `i18nizer start`):
|
|
|
235
323
|
- **Framework presets** (Next.js + next-intl, React + react-i18next)
|
|
236
324
|
- **Intelligent caching** to avoid redundant AI translation requests
|
|
237
325
|
- **String deduplication** with deterministic key reuse
|
|
326
|
+
- **AI-powered English key generation** for consistent, readable keys regardless of source language
|
|
238
327
|
- **Configurable behavior** (allowed functions, props, member functions)
|
|
239
328
|
- **Dry-run mode** to preview changes
|
|
240
329
|
- **JSON output preview** with `--show-json`
|
|
241
330
|
- Project-wide or single-file translation
|
|
242
331
|
- Works with **JSX & TSX**
|
|
243
332
|
- Rewrites components automatically (`t("key")`)
|
|
244
|
-
-
|
|
333
|
+
- Generates **English camelCase keys** (AI-assisted with deterministic fallback)
|
|
245
334
|
- Supports **any number of locales**
|
|
246
335
|
- Isolated TypeScript parsing (no project tsconfig required)
|
|
247
336
|
- Friendly logs with colors and spinners
|
|
@@ -48,6 +48,7 @@ export default class Translate extends Command {
|
|
|
48
48
|
description: "Display generated translation JSON output",
|
|
49
49
|
}),
|
|
50
50
|
};
|
|
51
|
+
// eslint-disable-next-line complexity
|
|
51
52
|
async run() {
|
|
52
53
|
const { args, flags } = await this.parse(Translate);
|
|
53
54
|
const cwd = process.cwd();
|
|
@@ -101,11 +102,11 @@ export default class Translate extends Command {
|
|
|
101
102
|
// Initialize cache and deduplicator
|
|
102
103
|
const projectDir = getProjectDir(cwd);
|
|
103
104
|
const cache = new TranslationCache(projectDir);
|
|
104
|
-
const deduplicator = new Deduplicator(cache);
|
|
105
|
+
const deduplicator = new Deduplicator(cache, config.behavior.useAiForKeys, provider);
|
|
105
106
|
let totalExtracted = 0;
|
|
106
107
|
let totalReused = 0;
|
|
107
108
|
let totalCached = 0;
|
|
108
|
-
// Process each file
|
|
109
|
+
// Process each file sequentially
|
|
109
110
|
for (const filePath of filesToProcess) {
|
|
110
111
|
const componentName = path.basename(filePath).replace(/\.(tsx|jsx)$/, "");
|
|
111
112
|
const spinner = ora(`Processing ${componentName}...`).start();
|
|
@@ -122,22 +123,23 @@ export default class Translate extends Command {
|
|
|
122
123
|
continue;
|
|
123
124
|
}
|
|
124
125
|
totalExtracted += texts.length;
|
|
125
|
-
// Deduplicate and assign keys
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
// Deduplicate and assign keys (now async)
|
|
127
|
+
// eslint-disable-next-line no-await-in-loop
|
|
128
|
+
const mappedTexts = await Promise.all(texts.map(async (t) => {
|
|
129
|
+
const result = await deduplicator.deduplicate(t.text, componentName, config.behavior.detectDuplicates);
|
|
128
130
|
if (result.isReused)
|
|
129
131
|
totalReused++;
|
|
130
132
|
if (result.isCached)
|
|
131
133
|
totalCached++;
|
|
132
134
|
return {
|
|
135
|
+
isCached: result.isCached,
|
|
133
136
|
key: result.key,
|
|
134
137
|
node: t.node,
|
|
135
138
|
placeholders: t.placeholders,
|
|
136
139
|
tempKey: t.tempKey,
|
|
137
140
|
text: t.text,
|
|
138
|
-
isCached: result.isCached,
|
|
139
141
|
};
|
|
140
|
-
});
|
|
142
|
+
}));
|
|
141
143
|
// Build translations JSON
|
|
142
144
|
const i18nJson = {};
|
|
143
145
|
for (const mapped of mappedTexts) {
|
|
@@ -169,6 +171,7 @@ export default class Translate extends Command {
|
|
|
169
171
|
text: t.text,
|
|
170
172
|
})),
|
|
171
173
|
});
|
|
174
|
+
// eslint-disable-next-line no-await-in-loop
|
|
172
175
|
const raw = await generateTranslations(prompt, provider);
|
|
173
176
|
if (!raw)
|
|
174
177
|
throw new Error("AI did not return any data");
|
|
@@ -178,7 +181,6 @@ export default class Translate extends Command {
|
|
|
178
181
|
for (const mapped of textsNeedingTranslation) {
|
|
179
182
|
const aiTranslations = namespace[mapped.tempKey];
|
|
180
183
|
if (aiTranslations) {
|
|
181
|
-
const aiKey = aiTranslations.key;
|
|
182
184
|
for (const locale of locales) {
|
|
183
185
|
i18nJson[mapped.key][locale] = aiTranslations[locale] ?? mapped.text;
|
|
184
186
|
}
|
|
@@ -210,7 +212,10 @@ export default class Translate extends Command {
|
|
|
210
212
|
writeLocaleFiles(componentName, { [componentName]: i18nJson }, locales);
|
|
211
213
|
}
|
|
212
214
|
// Rewrite component
|
|
213
|
-
|
|
215
|
+
// Only inject t function if autoInjectT is enabled
|
|
216
|
+
if (config.behavior.autoInjectT) {
|
|
217
|
+
insertUseTranslations(sourceFile, componentName);
|
|
218
|
+
}
|
|
214
219
|
replaceTempKeysWithT(mappedTexts.map((m) => ({
|
|
215
220
|
key: m.key,
|
|
216
221
|
node: m.node,
|
package/dist/core/ai/client.js
CHANGED
|
@@ -3,7 +3,7 @@ import { InferenceClient as HFClient } from "@huggingface/inference";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import OpenAI from "openai";
|
|
6
|
+
import { OpenAI } from "openai";
|
|
7
7
|
const CONFIG_FILE = path.join(os.homedir(), ".i18nizer", "api-keys.json");
|
|
8
8
|
function loadApiKeys() {
|
|
9
9
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { generateTranslations } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generate an English camelCase key from text using AI
|
|
4
|
+
* This ensures keys are always in English regardless of source language
|
|
5
|
+
*/
|
|
6
|
+
export async function generateEnglishKey(text, provider = "huggingface") {
|
|
7
|
+
const prompt = `
|
|
8
|
+
You are an i18n key generator. Generate a concise, meaningful camelCase key in English for the following text.
|
|
9
|
+
|
|
10
|
+
RULES:
|
|
11
|
+
- Output ONLY the camelCase key, nothing else
|
|
12
|
+
- Key should be 2-4 words maximum
|
|
13
|
+
- Key must be in English
|
|
14
|
+
- Key should describe the content/purpose
|
|
15
|
+
- Do NOT include quotes, explanations, or any other text
|
|
16
|
+
|
|
17
|
+
TEXT: "${text.replaceAll('"', '\\"')}"
|
|
18
|
+
|
|
19
|
+
EXAMPLES:
|
|
20
|
+
TEXT: "Bienvenido de nuevo" -> welcomeBack
|
|
21
|
+
TEXT: "Por favor inicia sesión" -> pleaseSignIn
|
|
22
|
+
TEXT: "Enviar formulario" -> submitForm
|
|
23
|
+
TEXT: "Seleccionar ciudad" -> selectCity
|
|
24
|
+
|
|
25
|
+
OUTPUT (key only):`.trim();
|
|
26
|
+
try {
|
|
27
|
+
const response = await generateTranslations(prompt, provider);
|
|
28
|
+
if (!response)
|
|
29
|
+
return undefined;
|
|
30
|
+
// Clean the response - remove quotes, whitespace, and any extra text
|
|
31
|
+
const cleaned = response
|
|
32
|
+
.trim()
|
|
33
|
+
.replaceAll(/["'`]/g, "")
|
|
34
|
+
.replaceAll(/\s+/g, " ")
|
|
35
|
+
.split(/\s+/)[0]; // Take only the first word/token
|
|
36
|
+
// Validate it's a valid camelCase identifier
|
|
37
|
+
if (/^[a-z][a-zA-Z0-9]*$/.test(cleaned)) {
|
|
38
|
+
return cleaned;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error("❌ Error generating English key with AI:", error);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -100,19 +100,23 @@ function mergeConfig(base, override) {
|
|
|
100
100
|
*/
|
|
101
101
|
export function generateConfig(framework, i18nLibrary) {
|
|
102
102
|
const frameworkPreset = FRAMEWORK_PRESETS[framework];
|
|
103
|
+
// Start with defaults merged with framework preset
|
|
104
|
+
const baseConfig = mergeConfig(DEFAULT_CONFIG, frameworkPreset);
|
|
105
|
+
// Set autoInjectT based on framework (disabled for Next.js by default)
|
|
106
|
+
if (framework === "nextjs") {
|
|
107
|
+
baseConfig.behavior.autoInjectT = false;
|
|
108
|
+
}
|
|
103
109
|
// If i18n library is specified, apply it after framework preset
|
|
104
110
|
// Framework settings take precedence for non-i18n specific fields
|
|
105
111
|
if (i18nLibrary) {
|
|
106
112
|
const i18nConfig = I18N_LIBRARY_CONFIGS[i18nLibrary];
|
|
107
|
-
// Merge: defaults -> framework -> i18n library (i18n-specific fields only)
|
|
108
|
-
const merged = mergeConfig(DEFAULT_CONFIG, frameworkPreset);
|
|
109
113
|
return {
|
|
110
|
-
...
|
|
114
|
+
...baseConfig,
|
|
111
115
|
i18nLibrary: i18nConfig.i18nLibrary,
|
|
112
|
-
i18n: i18nConfig.i18n ??
|
|
116
|
+
i18n: i18nConfig.i18n ?? baseConfig.i18n,
|
|
113
117
|
};
|
|
114
118
|
}
|
|
115
|
-
return
|
|
119
|
+
return baseConfig;
|
|
116
120
|
}
|
|
117
121
|
/**
|
|
118
122
|
* Write config to file
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
import { generateEnglishKey } from "../ai/generate-english-key.js";
|
|
1
2
|
import { generateKey, generateUniqueKey } from "../i18n/generate-key.js";
|
|
2
3
|
/**
|
|
3
4
|
* Deduplicate strings and assign deterministic keys
|
|
4
5
|
*/
|
|
5
6
|
export class Deduplicator {
|
|
6
7
|
cache;
|
|
8
|
+
provider;
|
|
9
|
+
useAiForKeys;
|
|
7
10
|
usedKeys = new Set();
|
|
8
|
-
constructor(cache) {
|
|
11
|
+
constructor(cache, useAiForKeys = true, provider = "huggingface") {
|
|
9
12
|
this.cache = cache;
|
|
13
|
+
this.provider = provider;
|
|
14
|
+
this.useAiForKeys = useAiForKeys;
|
|
10
15
|
}
|
|
11
16
|
/**
|
|
12
17
|
* Process a text and return a deterministic key
|
|
13
18
|
* Reuses existing keys if the text was seen before
|
|
14
19
|
*/
|
|
15
|
-
deduplicate(text, componentName, detectDuplicates) {
|
|
20
|
+
async deduplicate(text, componentName, detectDuplicates) {
|
|
16
21
|
// Check cache first
|
|
17
22
|
const cached = this.cache.get(text);
|
|
18
23
|
if (cached && detectDuplicates) {
|
|
@@ -25,7 +30,16 @@ export class Deduplicator {
|
|
|
25
30
|
};
|
|
26
31
|
}
|
|
27
32
|
// Generate a new deterministic key
|
|
28
|
-
|
|
33
|
+
let baseKey;
|
|
34
|
+
if (this.useAiForKeys && !cached) {
|
|
35
|
+
// Try to generate English key using AI
|
|
36
|
+
const aiKey = await generateEnglishKey(text, this.provider);
|
|
37
|
+
baseKey = aiKey ?? generateKey(text);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Use deterministic key generation (fallback or when AI is disabled)
|
|
41
|
+
baseKey = generateKey(text);
|
|
42
|
+
}
|
|
29
43
|
const uniqueKey = generateUniqueKey(baseKey, this.usedKeys);
|
|
30
44
|
this.usedKeys.add(uniqueKey);
|
|
31
45
|
return {
|
package/dist/types/config.js
CHANGED
|
@@ -16,8 +16,10 @@ export const DEFAULT_CONFIG = {
|
|
|
16
16
|
"title",
|
|
17
17
|
"tooltip",
|
|
18
18
|
],
|
|
19
|
+
autoInjectT: true, // Default enabled for React
|
|
19
20
|
detectDuplicates: true,
|
|
20
21
|
opinionatedStructure: true,
|
|
22
|
+
useAiForKeys: true, // Use AI to generate English keys
|
|
21
23
|
},
|
|
22
24
|
framework: "react",
|
|
23
25
|
i18n: {
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18nizer",
|
|
3
3
|
"description": "CLI to extract texts from JSX/TSX and generate i18n JSON with AI translations",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"author": "Yoannis Sanchez Soto",
|
|
6
6
|
"bin": "./bin/run.js",
|
|
7
7
|
"bugs": "https://github.com/yossTheDev/i18nizer/issues",
|