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 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
- - Always generates **English camelCase keys**
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
- const mappedTexts = texts.map((t) => {
127
- const result = deduplicator.deduplicate(t.text, componentName, config.behavior.detectDuplicates);
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
- insertUseTranslations(sourceFile, componentName);
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,
@@ -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
- ...merged,
114
+ ...baseConfig,
111
115
  i18nLibrary: i18nConfig.i18nLibrary,
112
- i18n: i18nConfig.i18n ?? merged.i18n,
116
+ i18n: i18nConfig.i18n ?? baseConfig.i18n,
113
117
  };
114
118
  }
115
- return mergeConfig(DEFAULT_CONFIG, frameworkPreset);
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
- const baseKey = generateKey(text);
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 {
@@ -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: {
@@ -301,5 +301,5 @@
301
301
  ]
302
302
  }
303
303
  },
304
- "version": "0.4.0-b"
304
+ "version": "0.5.0"
305
305
  }
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.0-b",
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",