@ccgp/i18n-ai 0.2.0 → 0.2.2

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
@@ -5,10 +5,10 @@ AI-powered internationalization (i18n) translation and synchronization library.
5
5
  ## Features
6
6
 
7
7
  - 🤖 **AI-Powered**: Uses Google Gemini (via OpenRouter) to generate context-aware translations.
8
- - **Parallel Processing**: Translations run concurrently for maximum speed (5x faster).
9
- - �🔄 **Smart Synchronization**: Detects new, modified, and obsolete keys.
10
- - �️ **Variable Protection**: Automatically preserves variables like `{name}` or `{count}`.
11
- - �🔒 **Lock File System**: Prevents unnecessary re-translations of already translated content.
8
+ - **Parallel Processing**: Translations run concurrently for maximum speed (5x faster).
9
+ - 🔄 **Smart Synchronization**: Detects new, modified, and obsolete keys.
10
+ - **Variable Protection**: Automatically preserves variables like `{name}` or `{count}`.
11
+ - 🔒 **Lock File System**: Prevents unnecessary re-translations of already translated content.
12
12
  - 🧩 **Framework Agnostic**: Works with any i18n library that uses JSON files (next-intl, react-i18next, etc.).
13
13
 
14
14
  ## Installation
@@ -47,6 +47,25 @@ This will create an `i18n-ai.config.json` file:
47
47
  bun x i18n-ai sync
48
48
  ```
49
49
 
50
+ ### CLI Options
51
+
52
+ The `sync` command supports these options:
53
+
54
+ | Option | Description |
55
+ |--------|-------------|
56
+ | `-d, --dir <path>` | Messages directory (default: `messages`) |
57
+ | `-l, --locales <items>` | Comma-separated list of locales |
58
+ | `--default <locale>` | Default locale (default: `en`) |
59
+ | `--lock <path>` | Lock file path (default: `translation-lock.json`) |
60
+ | `--api-key <key>` | OpenRouter API key (or set `OPENROUTER_API_KEY`) |
61
+ | `--model <model>` | AI model to use (default: `google/gemini-2.5-flash`) |
62
+
63
+ Example with custom options:
64
+
65
+ ```bash
66
+ bun x i18n-ai sync --locales es,fr,de --model anthropic/claude-3-haiku
67
+ ```
68
+
50
69
  ### Environment Variables
51
70
 
52
71
  You need to provide an API key for the AI provider.
@@ -79,10 +98,8 @@ jobs:
79
98
  steps:
80
99
  - uses: actions/checkout@v4
81
100
  - uses: oven-sh/setup-bun@v1
82
- - run: bun install
83
- - run: bun x i18n-ai sync
84
- env:
85
- OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
101
+ - run: bun install
102
+ - run: bun x i18n-ai sync --api-key ${{ secrets.OPENROUTER_API_KEY }}
86
103
  - run: |
87
104
  git config --global user.name "github-actions[bot]"
88
105
  git config --global user.email "github-actions[bot]@users.noreply.github.com"
@@ -119,19 +119,35 @@ var translate = async ({ text, lang, targetLang, apiKey, model }) => {
119
119
  };
120
120
  async function translateBatch(batch) {
121
121
  if (batch.entries.length === 0) return /* @__PURE__ */ new Map();
122
- const results = await Promise.all(
123
- batch.entries.map(async (entry) => {
124
- const translatedText = await translate({
125
- text: entry.text,
126
- lang: batch.sourceLang,
127
- targetLang: batch.targetLang,
128
- apiKey: batch.apiKey,
129
- model: batch.model
130
- });
131
- return [entry.key, translatedText];
132
- })
122
+ const token = batch.apiKey || process.env.OPENROUTER_API_KEY;
123
+ if (!token) {
124
+ throw new Error("Missing API Key. Please provide apiKey or set OPENROUTER_API_KEY environment variable.");
125
+ }
126
+ const openrouter = createOpenRouter({
127
+ apiKey: token
128
+ });
129
+ const inputPayload = Object.fromEntries(
130
+ batch.entries.map((e) => [e.key, e.text])
133
131
  );
134
- return new Map(results);
132
+ const system = `
133
+ You are a professional translator.
134
+ Task: Translate the values of the JSON object from "${batch.sourceLang}" to "${batch.targetLang}".
135
+
136
+ Rules:
137
+ - Keep the keys exactly the same.
138
+ - Translate only the values.
139
+ - Do not translate variables inside curly braces like "{name}".
140
+ - Return a valid JSON object.
141
+ `;
142
+ const { output } = await generateText({
143
+ model: openrouter.languageModel(batch.model || "google/gemini-2.5-flash"),
144
+ system,
145
+ prompt: JSON.stringify(inputPayload, null, 2),
146
+ output: Output.object({
147
+ schema: z.record(z.string(), z.string())
148
+ })
149
+ });
150
+ return new Map(Object.entries(output));
135
151
  }
136
152
 
137
153
  // src/core/sync.ts
package/dist/cli.js CHANGED
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
+ var import_dotenv = require("dotenv");
27
28
  var import_commander = require("commander");
28
29
  var import_node_path2 = require("path");
29
30
  var import_promises3 = require("fs/promises");
@@ -119,54 +120,37 @@ function updateLock(lock, locale, base, final) {
119
120
  var import_ai_sdk_provider = require("@openrouter/ai-sdk-provider");
120
121
  var import_ai = require("ai");
121
122
  var import_zod = __toESM(require("zod"));
122
- var translate = async ({ text, lang, targetLang, apiKey, model }) => {
123
- const token = apiKey || process.env.OPENROUTER_API_KEY;
123
+ async function translateBatch(batch) {
124
+ if (batch.entries.length === 0) return /* @__PURE__ */ new Map();
125
+ const token = batch.apiKey || process.env.OPENROUTER_API_KEY;
124
126
  if (!token) {
125
127
  throw new Error("Missing API Key. Please provide apiKey or set OPENROUTER_API_KEY environment variable.");
126
128
  }
127
129
  const openrouter = (0, import_ai_sdk_provider.createOpenRouter)({
128
130
  apiKey: token
129
131
  });
132
+ const inputPayload = Object.fromEntries(
133
+ batch.entries.map((e) => [e.key, e.text])
134
+ );
130
135
  const system = `
131
- You are a professional translator. Your main task is to translate text from "${lang}" to "${targetLang}".
136
+ You are a professional translator.
137
+ Task: Translate the values of the JSON object from "${batch.sourceLang}" to "${batch.targetLang}".
132
138
 
133
- IMPORTANT RULES:
134
- 1. Return ONLY the translated text. No explanations, no quotes around the output.
135
- 2. Do NOT translate text inside curly braces, e.g., "{name}", "{count}". These are variables and must remain exactly as they are.
136
- 3. Maintain the original tone and context.
137
- 4. If the text is a single word or short phrase, translate it directly.
138
-
139
- Examples:
140
- - Input: "Hello {name}, welcome back!" -> Output (ES): "Hola {name}, \xA1bienvenido de nuevo!"
141
- - Input: "Contact us calling to {number}" -> Output (FR): "Contactez-nous en appelant le {number}"
139
+ Rules:
140
+ - Keep the keys exactly the same.
141
+ - Translate only the values.
142
+ - Do not translate variables inside curly braces like "{name}".
143
+ - Return a valid JSON object.
142
144
  `;
143
145
  const { output } = await (0, import_ai.generateText)({
144
- model: openrouter.languageModel(model || "google/gemini-2.5-flash"),
146
+ model: openrouter.languageModel(batch.model || "google/gemini-2.5-flash"),
145
147
  system,
146
- prompt: `Text to translate: "${text}"`,
148
+ prompt: JSON.stringify(inputPayload, null, 2),
147
149
  output: import_ai.Output.object({
148
- schema: import_zod.default.object({
149
- translatedText: import_zod.default.string()
150
- })
150
+ schema: import_zod.default.record(import_zod.default.string(), import_zod.default.string())
151
151
  })
152
152
  });
153
- return output.translatedText;
154
- };
155
- async function translateBatch(batch) {
156
- if (batch.entries.length === 0) return /* @__PURE__ */ new Map();
157
- const results = await Promise.all(
158
- batch.entries.map(async (entry) => {
159
- const translatedText = await translate({
160
- text: entry.text,
161
- lang: batch.sourceLang,
162
- targetLang: batch.targetLang,
163
- apiKey: batch.apiKey,
164
- model: batch.model
165
- });
166
- return [entry.key, translatedText];
167
- })
168
- );
169
- return new Map(results);
153
+ return new Map(Object.entries(output));
170
154
  }
171
155
 
172
156
  // src/core/sync.ts
@@ -288,7 +272,8 @@ var TranslationService = class {
288
272
  // src/cli.ts
289
273
  var import_prompts = __toESM(require("prompts"));
290
274
  var import_chalk = __toESM(require("chalk"));
291
- var VERSION = "0.2.0";
275
+ (0, import_dotenv.config)({ path: (0, import_node_path2.resolve)(process.cwd(), ".env") });
276
+ var VERSION = "0.2.2";
292
277
  var program = new import_commander.Command();
293
278
  program.name("i18n-ai").description("AI-powered translation CLI").version(VERSION);
294
279
  var CONFIG_FILE = "i18n.config.json";
@@ -350,6 +335,13 @@ program.command("init").description("Initialize i18n-ai configuration").action(a
350
335
  await (0, import_promises3.writeFile)(baseFilePath, JSON.stringify({ welcome: "Hello World" }, null, 2));
351
336
  console.log(import_chalk.default.green(`\u2705 Created base file ${response.messagesDir}/${response.defaultLocale}.json`));
352
337
  }
338
+ if (process.env.OPENROUTER_API_KEY) {
339
+ console.log(import_chalk.default.green("\u2705 OPENROUTER_API_KEY detected in .env"));
340
+ } else {
341
+ console.log(import_chalk.default.yellow("\n\u26A0\uFE0F OPENROUTER_API_KEY not found in .env"));
342
+ console.log(import_chalk.default.dim(" Add it to your .env file: OPENROUTER_API_KEY=your_key"));
343
+ console.log(import_chalk.default.dim(" Get one at: https://openrouter.ai"));
344
+ }
353
345
  console.log(import_chalk.default.blue('\n\u{1F389} Setup complete! You can now run "i18n-ai sync"'));
354
346
  });
355
347
  program.command("sync").description("Synchronize translations using AI").option("-d, --dir <path>", "Messages directory").option("-l, --locales <items>", "Comma separated list of locales").option("--default <locale>", "Default locale").option("--lock <path>", "Lock file path", "translation-lock.json").option("--api-key <key>", "OpenRouter API key (or set OPENROUTER_API_KEY)").option("--model <model>", "AI model to use", "google/gemini-2.5-flash").action(async (options) => {
package/dist/cli.mjs CHANGED
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TranslationService
4
- } from "./chunk-5QO2MVWV.mjs";
4
+ } from "./chunk-OQMB7A4T.mjs";
5
5
 
6
6
  // src/cli.ts
7
+ import { config as loadEnv } from "dotenv";
7
8
  import { Command } from "commander";
8
9
  import { resolve, join } from "path";
9
10
  import { readFile, writeFile, mkdir } from "fs/promises";
10
11
  import prompts from "prompts";
11
12
  import chalk from "chalk";
12
- var VERSION = "0.2.0";
13
+ loadEnv({ path: resolve(process.cwd(), ".env") });
14
+ var VERSION = "0.2.2";
13
15
  var program = new Command();
14
16
  program.name("i18n-ai").description("AI-powered translation CLI").version(VERSION);
15
17
  var CONFIG_FILE = "i18n.config.json";
@@ -71,6 +73,13 @@ program.command("init").description("Initialize i18n-ai configuration").action(a
71
73
  await writeFile(baseFilePath, JSON.stringify({ welcome: "Hello World" }, null, 2));
72
74
  console.log(chalk.green(`\u2705 Created base file ${response.messagesDir}/${response.defaultLocale}.json`));
73
75
  }
76
+ if (process.env.OPENROUTER_API_KEY) {
77
+ console.log(chalk.green("\u2705 OPENROUTER_API_KEY detected in .env"));
78
+ } else {
79
+ console.log(chalk.yellow("\n\u26A0\uFE0F OPENROUTER_API_KEY not found in .env"));
80
+ console.log(chalk.dim(" Add it to your .env file: OPENROUTER_API_KEY=your_key"));
81
+ console.log(chalk.dim(" Get one at: https://openrouter.ai"));
82
+ }
74
83
  console.log(chalk.blue('\n\u{1F389} Setup complete! You can now run "i18n-ai sync"'));
75
84
  });
76
85
  program.command("sync").description("Synchronize translations using AI").option("-d, --dir <path>", "Messages directory").option("-l, --locales <items>", "Comma separated list of locales").option("--default <locale>", "Default locale").option("--lock <path>", "Lock file path", "translation-lock.json").option("--api-key <key>", "OpenRouter API key (or set OPENROUTER_API_KEY)").option("--model <model>", "AI model to use", "google/gemini-2.5-flash").action(async (options) => {
package/dist/index.js CHANGED
@@ -160,19 +160,35 @@ var translate = async ({ text, lang, targetLang, apiKey, model }) => {
160
160
  };
161
161
  async function translateBatch(batch) {
162
162
  if (batch.entries.length === 0) return /* @__PURE__ */ new Map();
163
- const results = await Promise.all(
164
- batch.entries.map(async (entry) => {
165
- const translatedText = await translate({
166
- text: entry.text,
167
- lang: batch.sourceLang,
168
- targetLang: batch.targetLang,
169
- apiKey: batch.apiKey,
170
- model: batch.model
171
- });
172
- return [entry.key, translatedText];
173
- })
163
+ const token = batch.apiKey || process.env.OPENROUTER_API_KEY;
164
+ if (!token) {
165
+ throw new Error("Missing API Key. Please provide apiKey or set OPENROUTER_API_KEY environment variable.");
166
+ }
167
+ const openrouter = (0, import_ai_sdk_provider.createOpenRouter)({
168
+ apiKey: token
169
+ });
170
+ const inputPayload = Object.fromEntries(
171
+ batch.entries.map((e) => [e.key, e.text])
174
172
  );
175
- return new Map(results);
173
+ const system = `
174
+ You are a professional translator.
175
+ Task: Translate the values of the JSON object from "${batch.sourceLang}" to "${batch.targetLang}".
176
+
177
+ Rules:
178
+ - Keep the keys exactly the same.
179
+ - Translate only the values.
180
+ - Do not translate variables inside curly braces like "{name}".
181
+ - Return a valid JSON object.
182
+ `;
183
+ const { output } = await (0, import_ai.generateText)({
184
+ model: openrouter.languageModel(batch.model || "google/gemini-2.5-flash"),
185
+ system,
186
+ prompt: JSON.stringify(inputPayload, null, 2),
187
+ output: import_ai.Output.object({
188
+ schema: import_zod.default.record(import_zod.default.string(), import_zod.default.string())
189
+ })
190
+ });
191
+ return new Map(Object.entries(output));
176
192
  }
177
193
 
178
194
  // src/core/sync.ts
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  translateBatch,
6
6
  updateLock,
7
7
  withLockFile
8
- } from "./chunk-5QO2MVWV.mjs";
8
+ } from "./chunk-OQMB7A4T.mjs";
9
9
  export {
10
10
  TranslationService,
11
11
  analyzeLocale,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccgp/i18n-ai",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "AI-powered i18n translation and synchronization library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -39,6 +39,7 @@
39
39
  "ai": "^6.0.37",
40
40
  "chalk": "^5.6.2",
41
41
  "commander": "^14.0.2",
42
+ "dotenv": "^17.2.3",
42
43
  "p-queue": "^9.1.0",
43
44
  "prompts": "^2.4.2",
44
45
  "proper-lockfile": "^4.1.2",