@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 +25 -8
- package/dist/{chunk-5QO2MVWV.mjs → chunk-OQMB7A4T.mjs} +28 -12
- package/dist/cli.js +27 -35
- package/dist/cli.mjs +11 -2
- package/dist/index.js +28 -12
- package/dist/index.mjs +1 -1
- package/package.json +2 -1
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
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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.
|
|
136
|
+
You are a professional translator.
|
|
137
|
+
Task: Translate the values of the JSON object from "${batch.sourceLang}" to "${batch.targetLang}".
|
|
132
138
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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:
|
|
148
|
+
prompt: JSON.stringify(inputPayload, null, 2),
|
|
147
149
|
output: import_ai.Output.object({
|
|
148
|
-
schema: import_zod.default.
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ccgp/i18n-ai",
|
|
3
|
-
"version": "0.2.
|
|
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",
|