@ccgp/i18n-ai 0.1.0 → 0.1.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
@@ -43,10 +43,33 @@ bun x i18n-ai sync
43
43
 
44
44
  The CLI attempts to auto-detect your configuration from `i18n/routing.ts` if you are using `next-intl`. Otherwise, you can specify options:
45
45
 
46
- ```bash
47
46
  i18n-ai sync --locales en,es,fr --default en --dir messages
48
47
  ```
49
48
 
49
+ ### Configuration File (Recommended)
50
+
51
+ You can run `i18n-ai init` to generate a configuration file interactively:
52
+
53
+ ```bash
54
+ bun x i18n-ai init
55
+ ```
56
+
57
+ This will create an `i18n-ai.config.json` file:
58
+
59
+ ```json
60
+ {
61
+ "defaultLocale": "en",
62
+ "locales": ["en", "es", "fr"],
63
+ "messagesDir": "messages"
64
+ }
65
+ ```
66
+
67
+ Then you can simply run:
68
+
69
+ ```bash
70
+ bun x i18n-ai sync
71
+ ```
72
+
50
73
  ### Environment Variables
51
74
 
52
75
  You need to provide an API key for the AI provider.
@@ -10,11 +10,23 @@ var translate = async ({ text, lang, targetLang, apiKey, model }) => {
10
10
  const openrouter = createOpenRouter({
11
11
  apiKey: token
12
12
  });
13
+ const system = `
14
+ You are a professional translator. Your main task is to translate text from "${lang}" to "${targetLang}".
15
+
16
+ IMPORTANT RULES:
17
+ 1. Return ONLY the translated text. No explanations, no quotes around the output.
18
+ 2. Do NOT translate text inside curly braces, e.g., "{name}", "{count}". These are variables and must remain exactly as they are.
19
+ 3. Maintain the original tone and context.
20
+ 4. If the text is a single word or short phrase, translate it directly.
21
+
22
+ Examples:
23
+ - Input: "Hello {name}, welcome back!" -> Output (ES): "Hola {name}, \xA1bienvenido de nuevo!"
24
+ - Input: "Contact us calling to {number}" -> Output (FR): "Contactez-nous en appelant le {number}"
25
+ `;
13
26
  const { output } = await generateText({
14
27
  model: openrouter.languageModel(model || "google/gemini-2.5-flash"),
15
- prompt: `Translate the following text from ${lang} to ${targetLang}. Return ONLY the translated text, no explanations or additional text.
16
-
17
- Text to translate: ${text}`,
28
+ system,
29
+ prompt: `Text to translate: "${text}"`,
18
30
  output: Output.object({
19
31
  schema: z.object({
20
32
  translatedText: z.string()
@@ -236,9 +248,30 @@ var TranslationService = class {
236
248
  }
237
249
  let translatedCount = 0;
238
250
  if (toTranslate.length > 0) {
239
- for (const change of toTranslate) {
251
+ const limit = (concurrency) => {
252
+ let active = 0;
253
+ const queue = [];
254
+ const run = async (fn) => {
255
+ if (active >= concurrency) {
256
+ await new Promise((resolve) => queue.push(resolve));
257
+ }
258
+ active++;
259
+ try {
260
+ return await fn();
261
+ } finally {
262
+ active--;
263
+ if (queue.length > 0) {
264
+ queue.shift()();
265
+ }
266
+ }
267
+ };
268
+ return run;
269
+ };
270
+ const runTask = limit(5);
271
+ await Promise.all(toTranslate.map((change) => runTask(async () => {
240
272
  try {
241
- const translatedValue = await translate({
273
+ const _translate = this.config.translator || translate;
274
+ const translatedValue = await _translate({
242
275
  text: change.sourceText,
243
276
  lang: this.config.defaultLocale,
244
277
  targetLang: targetLocale,
@@ -258,7 +291,7 @@ var TranslationService = class {
258
291
  targetFlat.set(change.key, change.currentTranslation);
259
292
  }
260
293
  }
261
- }
294
+ })));
262
295
  }
263
296
  for (const change of changes.unchanged) {
264
297
  if (change.currentTranslation) {
package/dist/cli.js CHANGED
@@ -44,11 +44,23 @@ var translate = async ({ text, lang, targetLang, apiKey, model }) => {
44
44
  const openrouter = (0, import_ai_sdk_provider.createOpenRouter)({
45
45
  apiKey: token
46
46
  });
47
+ const system = `
48
+ You are a professional translator. Your main task is to translate text from "${lang}" to "${targetLang}".
49
+
50
+ IMPORTANT RULES:
51
+ 1. Return ONLY the translated text. No explanations, no quotes around the output.
52
+ 2. Do NOT translate text inside curly braces, e.g., "{name}", "{count}". These are variables and must remain exactly as they are.
53
+ 3. Maintain the original tone and context.
54
+ 4. If the text is a single word or short phrase, translate it directly.
55
+
56
+ Examples:
57
+ - Input: "Hello {name}, welcome back!" -> Output (ES): "Hola {name}, \xA1bienvenido de nuevo!"
58
+ - Input: "Contact us calling to {number}" -> Output (FR): "Contactez-nous en appelant le {number}"
59
+ `;
47
60
  const { output } = await (0, import_ai.generateText)({
48
61
  model: openrouter.languageModel(model || "google/gemini-2.5-flash"),
49
- prompt: `Translate the following text from ${lang} to ${targetLang}. Return ONLY the translated text, no explanations or additional text.
50
-
51
- Text to translate: ${text}`,
62
+ system,
63
+ prompt: `Text to translate: "${text}"`,
52
64
  output: import_ai.Output.object({
53
65
  schema: import_zod.default.object({
54
66
  translatedText: import_zod.default.string()
@@ -268,9 +280,30 @@ var TranslationService = class {
268
280
  }
269
281
  let translatedCount = 0;
270
282
  if (toTranslate.length > 0) {
271
- for (const change of toTranslate) {
283
+ const limit = (concurrency) => {
284
+ let active = 0;
285
+ const queue = [];
286
+ const run = async (fn) => {
287
+ if (active >= concurrency) {
288
+ await new Promise((resolve2) => queue.push(resolve2));
289
+ }
290
+ active++;
291
+ try {
292
+ return await fn();
293
+ } finally {
294
+ active--;
295
+ if (queue.length > 0) {
296
+ queue.shift()();
297
+ }
298
+ }
299
+ };
300
+ return run;
301
+ };
302
+ const runTask = limit(5);
303
+ await Promise.all(toTranslate.map((change) => runTask(async () => {
272
304
  try {
273
- const translatedValue = await translate({
305
+ const _translate = this.config.translator || translate;
306
+ const translatedValue = await _translate({
274
307
  text: change.sourceText,
275
308
  lang: this.config.defaultLocale,
276
309
  targetLang: targetLocale,
@@ -290,7 +323,7 @@ var TranslationService = class {
290
323
  targetFlat.set(change.key, change.currentTranslation);
291
324
  }
292
325
  }
293
- }
326
+ })));
294
327
  }
295
328
  for (const change of changes.unchanged) {
296
329
  if (change.currentTranslation) {
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TranslationService
4
- } from "./chunk-NY2C35K2.mjs";
4
+ } from "./chunk-KVP2GFHU.mjs";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command } from "commander";
package/dist/index.d.mts CHANGED
@@ -75,6 +75,13 @@ interface SyncConfig {
75
75
  lockFilePath: string;
76
76
  apiKey?: string;
77
77
  model?: string;
78
+ translator?: (params: {
79
+ text: string;
80
+ lang: string;
81
+ targetLang: string;
82
+ apiKey?: string;
83
+ model?: string;
84
+ }) => Promise<string>;
78
85
  }
79
86
  /**
80
87
  * Flattens a nested object to a map of flat paths
package/dist/index.d.ts CHANGED
@@ -75,6 +75,13 @@ interface SyncConfig {
75
75
  lockFilePath: string;
76
76
  apiKey?: string;
77
77
  model?: string;
78
+ translator?: (params: {
79
+ text: string;
80
+ lang: string;
81
+ targetLang: string;
82
+ apiKey?: string;
83
+ model?: string;
84
+ }) => Promise<string>;
78
85
  }
79
86
  /**
80
87
  * Flattens a nested object to a map of flat paths
package/dist/index.js CHANGED
@@ -146,11 +146,23 @@ var translate = async ({ text, lang, targetLang, apiKey, model }) => {
146
146
  const openrouter = (0, import_ai_sdk_provider.createOpenRouter)({
147
147
  apiKey: token
148
148
  });
149
+ const system = `
150
+ You are a professional translator. Your main task is to translate text from "${lang}" to "${targetLang}".
151
+
152
+ IMPORTANT RULES:
153
+ 1. Return ONLY the translated text. No explanations, no quotes around the output.
154
+ 2. Do NOT translate text inside curly braces, e.g., "{name}", "{count}". These are variables and must remain exactly as they are.
155
+ 3. Maintain the original tone and context.
156
+ 4. If the text is a single word or short phrase, translate it directly.
157
+
158
+ Examples:
159
+ - Input: "Hello {name}, welcome back!" -> Output (ES): "Hola {name}, \xA1bienvenido de nuevo!"
160
+ - Input: "Contact us calling to {number}" -> Output (FR): "Contactez-nous en appelant le {number}"
161
+ `;
149
162
  const { output } = await (0, import_ai.generateText)({
150
163
  model: openrouter.languageModel(model || "google/gemini-2.5-flash"),
151
- prompt: `Translate the following text from ${lang} to ${targetLang}. Return ONLY the translated text, no explanations or additional text.
152
-
153
- Text to translate: ${text}`,
164
+ system,
165
+ prompt: `Text to translate: "${text}"`,
154
166
  output: import_ai.Output.object({
155
167
  schema: import_zod.default.object({
156
168
  translatedText: import_zod.default.string()
@@ -282,9 +294,30 @@ var TranslationService = class {
282
294
  }
283
295
  let translatedCount = 0;
284
296
  if (toTranslate.length > 0) {
285
- for (const change of toTranslate) {
297
+ const limit = (concurrency) => {
298
+ let active = 0;
299
+ const queue = [];
300
+ const run = async (fn) => {
301
+ if (active >= concurrency) {
302
+ await new Promise((resolve) => queue.push(resolve));
303
+ }
304
+ active++;
305
+ try {
306
+ return await fn();
307
+ } finally {
308
+ active--;
309
+ if (queue.length > 0) {
310
+ queue.shift()();
311
+ }
312
+ }
313
+ };
314
+ return run;
315
+ };
316
+ const runTask = limit(5);
317
+ await Promise.all(toTranslate.map((change) => runTask(async () => {
286
318
  try {
287
- const translatedValue = await translate({
319
+ const _translate = this.config.translator || translate;
320
+ const translatedValue = await _translate({
288
321
  text: change.sourceText,
289
322
  lang: this.config.defaultLocale,
290
323
  targetLang: targetLocale,
@@ -304,7 +337,7 @@ var TranslationService = class {
304
337
  targetFlat.set(change.key, change.currentTranslation);
305
338
  }
306
339
  }
307
- }
340
+ })));
308
341
  }
309
342
  for (const change of changes.unchanged) {
310
343
  if (change.currentTranslation) {
package/dist/index.mjs CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  translate,
11
11
  unflattenObject,
12
12
  updateLockEntry
13
- } from "./chunk-NY2C35K2.mjs";
13
+ } from "./chunk-KVP2GFHU.mjs";
14
14
  export {
15
15
  TranslationService,
16
16
  analyzeChanges,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccgp/i18n-ai",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "AI-powered i18n translation and synchronization library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -19,8 +19,7 @@
19
19
  "build": "tsup src/index.ts src/cli.ts --format esm,cjs --dts --clean",
20
20
  "dev": "tsup --watch",
21
21
  "start": "node dist/cli.js",
22
- "lint": "tsc --noEmit",
23
- "prepublishOnly": "bun run build"
22
+ "lint": "tsc --noEmit"
24
23
  },
25
24
  "repository": {
26
25
  "type": "git",