@ccgp/i18n-ai 0.2.1 → 0.2.3

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.
@@ -83,55 +83,79 @@ function updateLock(lock, locale, base, final) {
83
83
  // src/core/translator.ts
84
84
  import { createOpenRouter } from "@openrouter/ai-sdk-provider";
85
85
  import { generateText, Output } from "ai";
86
- import z from "zod";
87
- var translate = async ({ text, lang, targetLang, apiKey, model }) => {
88
- const token = apiKey || process.env.OPENROUTER_API_KEY;
89
- if (!token) {
90
- throw new Error("Missing API Key. Please provide apiKey or set OPENROUTER_API_KEY environment variable.");
91
- }
92
- const openrouter = createOpenRouter({
93
- apiKey: token
94
- });
95
- const system = `
96
- You are a professional translator. Your main task is to translate text from "${lang}" to "${targetLang}".
97
-
98
- IMPORTANT RULES:
99
- 1. Return ONLY the translated text. No explanations, no quotes around the output.
100
- 2. Do NOT translate text inside curly braces, e.g., "{name}", "{count}". These are variables and must remain exactly as they are.
101
- 3. Maintain the original tone and context.
102
- 4. If the text is a single word or short phrase, translate it directly.
103
-
104
- Examples:
105
- - Input: "Hello {name}, welcome back!" -> Output (ES): "Hola {name}, \xA1bienvenido de nuevo!"
106
- - Input: "Contact us calling to {number}" -> Output (FR): "Contactez-nous en appelant le {number}"
107
- `;
108
- const { output } = await generateText({
109
- model: openrouter.languageModel(model || "google/gemini-2.5-flash"),
110
- system,
111
- prompt: `Text to translate: "${text}"`,
112
- output: Output.object({
113
- schema: z.object({
114
- translatedText: z.string()
115
- })
86
+ import * as z2 from "zod";
87
+
88
+ // src/env.ts
89
+ import { createEnv } from "@t3-oss/env-core";
90
+ import * as z from "zod";
91
+ var env = createEnv({
92
+ server: {
93
+ OPENROUTER_API_KEY: z.string()
94
+ },
95
+ runtimeEnv: process.env
96
+ });
97
+
98
+ // src/core/translator.ts
99
+ var DEFAULT_MODEL = "google/gemini-2.5-flash";
100
+ var translationSchema = z2.object({
101
+ translations: z2.array(
102
+ z2.object({
103
+ key: z2.string(),
104
+ value: z2.string()
116
105
  })
117
- });
118
- return output.translatedText;
119
- };
106
+ )
107
+ });
108
+ var openrouter = null;
109
+ var getOpenRouter = () => openrouter ?? (openrouter = createOpenRouter({ apiKey: env.OPENROUTER_API_KEY }));
110
+ var SYSTEM_PROMPT = `You are a professional translator.
111
+
112
+ RULES:
113
+ 1. Translate the provided key-value pairs from the source language to the target language
114
+ 2. Return an array of translations with the EXACT same keys and their translated values
115
+ 3. Do NOT translate variables in curly braces: {name}, {count}, etc. Keep them exactly as-is
116
+ 4. Maintain original tone and context
117
+ 5. For single words or short phrases, translate directly`;
118
+ async function withRetry(fn, maxAttempts = 3) {
119
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
120
+ try {
121
+ return await fn();
122
+ } catch (error) {
123
+ const msg = error.message?.toLowerCase() ?? "";
124
+ const retryable = msg.includes("network") || msg.includes("timeout") || msg.includes("econnreset") || msg.includes("429") || msg.includes("503") || msg.includes("502");
125
+ if (attempt === maxAttempts || !retryable) throw error;
126
+ await new Promise((r) => setTimeout(r, 2 ** (attempt - 1) * 1e3));
127
+ }
128
+ }
129
+ throw new Error("Unreachable");
130
+ }
120
131
  async function translateBatch(batch) {
121
132
  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
- })
133
- );
134
- return new Map(results);
133
+ const inputArray = batch.entries.map((e) => ({ key: e.key, text: e.text }));
134
+ return withRetry(async () => {
135
+ const { output } = await generateText({
136
+ model: getOpenRouter().languageModel(DEFAULT_MODEL),
137
+ output: Output.object({
138
+ schema: translationSchema
139
+ }),
140
+ system: SYSTEM_PROMPT,
141
+ prompt: `Translate from "${batch.sourceLang}" to "${batch.targetLang}":
142
+
143
+ ${JSON.stringify(inputArray, null, 2)}
144
+
145
+ Return the translations array with the same keys and translated values.`
146
+ });
147
+ if (!output.translations || output.translations.length === 0) {
148
+ throw new Error("Translation returned empty result");
149
+ }
150
+ const resultMap = new Map(output.translations.map((t) => [t.key, t.value]));
151
+ const missingKeys = batch.entries.filter((e) => !resultMap.has(e.key));
152
+ if (missingKeys.length > 0) {
153
+ throw new Error(
154
+ `Missing translations for keys: ${missingKeys.map((e) => e.key).join(", ")}`
155
+ );
156
+ }
157
+ return resultMap;
158
+ });
135
159
  }
136
160
 
137
161
  // src/core/sync.ts
@@ -257,7 +281,6 @@ export {
257
281
  withLockFile,
258
282
  analyzeLocale,
259
283
  updateLock,
260
- translate,
261
284
  translateBatch,
262
285
  TranslationService
263
286
  };
package/dist/cli.js CHANGED
@@ -119,55 +119,79 @@ function updateLock(lock, locale, base, final) {
119
119
  // src/core/translator.ts
120
120
  var import_ai_sdk_provider = require("@openrouter/ai-sdk-provider");
121
121
  var import_ai = require("ai");
122
- var import_zod = __toESM(require("zod"));
123
- var translate = async ({ text, lang, targetLang, apiKey, model }) => {
124
- const token = apiKey || process.env.OPENROUTER_API_KEY;
125
- if (!token) {
126
- throw new Error("Missing API Key. Please provide apiKey or set OPENROUTER_API_KEY environment variable.");
127
- }
128
- const openrouter = (0, import_ai_sdk_provider.createOpenRouter)({
129
- apiKey: token
130
- });
131
- const system = `
132
- You are a professional translator. Your main task is to translate text from "${lang}" to "${targetLang}".
133
-
134
- IMPORTANT RULES:
135
- 1. Return ONLY the translated text. No explanations, no quotes around the output.
136
- 2. Do NOT translate text inside curly braces, e.g., "{name}", "{count}". These are variables and must remain exactly as they are.
137
- 3. Maintain the original tone and context.
138
- 4. If the text is a single word or short phrase, translate it directly.
139
-
140
- Examples:
141
- - Input: "Hello {name}, welcome back!" -> Output (ES): "Hola {name}, \xA1bienvenido de nuevo!"
142
- - Input: "Contact us calling to {number}" -> Output (FR): "Contactez-nous en appelant le {number}"
143
- `;
144
- const { output } = await (0, import_ai.generateText)({
145
- model: openrouter.languageModel(model || "google/gemini-2.5-flash"),
146
- system,
147
- prompt: `Text to translate: "${text}"`,
148
- output: import_ai.Output.object({
149
- schema: import_zod.default.object({
150
- translatedText: import_zod.default.string()
151
- })
122
+ var z2 = __toESM(require("zod"));
123
+
124
+ // src/env.ts
125
+ var import_env_core = require("@t3-oss/env-core");
126
+ var z = __toESM(require("zod"));
127
+ var env = (0, import_env_core.createEnv)({
128
+ server: {
129
+ OPENROUTER_API_KEY: z.string()
130
+ },
131
+ runtimeEnv: process.env
132
+ });
133
+
134
+ // src/core/translator.ts
135
+ var DEFAULT_MODEL = "google/gemini-2.5-flash";
136
+ var translationSchema = z2.object({
137
+ translations: z2.array(
138
+ z2.object({
139
+ key: z2.string(),
140
+ value: z2.string()
152
141
  })
153
- });
154
- return output.translatedText;
155
- };
142
+ )
143
+ });
144
+ var openrouter = null;
145
+ var getOpenRouter = () => openrouter ?? (openrouter = (0, import_ai_sdk_provider.createOpenRouter)({ apiKey: env.OPENROUTER_API_KEY }));
146
+ var SYSTEM_PROMPT = `You are a professional translator.
147
+
148
+ RULES:
149
+ 1. Translate the provided key-value pairs from the source language to the target language
150
+ 2. Return an array of translations with the EXACT same keys and their translated values
151
+ 3. Do NOT translate variables in curly braces: {name}, {count}, etc. Keep them exactly as-is
152
+ 4. Maintain original tone and context
153
+ 5. For single words or short phrases, translate directly`;
154
+ async function withRetry(fn, maxAttempts = 3) {
155
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
156
+ try {
157
+ return await fn();
158
+ } catch (error) {
159
+ const msg = error.message?.toLowerCase() ?? "";
160
+ const retryable = msg.includes("network") || msg.includes("timeout") || msg.includes("econnreset") || msg.includes("429") || msg.includes("503") || msg.includes("502");
161
+ if (attempt === maxAttempts || !retryable) throw error;
162
+ await new Promise((r) => setTimeout(r, 2 ** (attempt - 1) * 1e3));
163
+ }
164
+ }
165
+ throw new Error("Unreachable");
166
+ }
156
167
  async function translateBatch(batch) {
157
168
  if (batch.entries.length === 0) return /* @__PURE__ */ new Map();
158
- const results = await Promise.all(
159
- batch.entries.map(async (entry) => {
160
- const translatedText = await translate({
161
- text: entry.text,
162
- lang: batch.sourceLang,
163
- targetLang: batch.targetLang,
164
- apiKey: batch.apiKey,
165
- model: batch.model
166
- });
167
- return [entry.key, translatedText];
168
- })
169
- );
170
- return new Map(results);
169
+ const inputArray = batch.entries.map((e) => ({ key: e.key, text: e.text }));
170
+ return withRetry(async () => {
171
+ const { output } = await (0, import_ai.generateText)({
172
+ model: getOpenRouter().languageModel(DEFAULT_MODEL),
173
+ output: import_ai.Output.object({
174
+ schema: translationSchema
175
+ }),
176
+ system: SYSTEM_PROMPT,
177
+ prompt: `Translate from "${batch.sourceLang}" to "${batch.targetLang}":
178
+
179
+ ${JSON.stringify(inputArray, null, 2)}
180
+
181
+ Return the translations array with the same keys and translated values.`
182
+ });
183
+ if (!output.translations || output.translations.length === 0) {
184
+ throw new Error("Translation returned empty result");
185
+ }
186
+ const resultMap = new Map(output.translations.map((t) => [t.key, t.value]));
187
+ const missingKeys = batch.entries.filter((e) => !resultMap.has(e.key));
188
+ if (missingKeys.length > 0) {
189
+ throw new Error(
190
+ `Missing translations for keys: ${missingKeys.map((e) => e.key).join(", ")}`
191
+ );
192
+ }
193
+ return resultMap;
194
+ });
171
195
  }
172
196
 
173
197
  // src/core/sync.ts
@@ -290,7 +314,7 @@ var TranslationService = class {
290
314
  var import_prompts = __toESM(require("prompts"));
291
315
  var import_chalk = __toESM(require("chalk"));
292
316
  (0, import_dotenv.config)({ path: (0, import_node_path2.resolve)(process.cwd(), ".env") });
293
- var VERSION = "0.2.1";
317
+ var VERSION = "0.2.3";
294
318
  var program = new import_commander.Command();
295
319
  program.name("i18n-ai").description("AI-powered translation CLI").version(VERSION);
296
320
  var CONFIG_FILE = "i18n.config.json";
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TranslationService
4
- } from "./chunk-5QO2MVWV.mjs";
4
+ } from "./chunk-IOH4K3A3.mjs";
5
5
 
6
6
  // src/cli.ts
7
7
  import { config as loadEnv } from "dotenv";
@@ -11,7 +11,7 @@ import { readFile, writeFile, mkdir } from "fs/promises";
11
11
  import prompts from "prompts";
12
12
  import chalk from "chalk";
13
13
  loadEnv({ path: resolve(process.cwd(), ".env") });
14
- var VERSION = "0.2.1";
14
+ var VERSION = "0.2.3";
15
15
  var program = new Command();
16
16
  program.name("i18n-ai").description("AI-powered translation CLI").version(VERSION);
17
17
  var CONFIG_FILE = "i18n.config.json";
package/dist/index.d.mts CHANGED
@@ -21,14 +21,6 @@ declare function withLockFile<T>(path: string, fn: (lock: TranslationLock) => Pr
21
21
  declare function analyzeLocale(base: Map<string, string>, target: Map<string, string>, locale: string, lock: TranslationLock): LocaleChanges;
22
22
  declare function updateLock(lock: TranslationLock, locale: string, base: Map<string, string>, final: Map<string, string>): void;
23
23
 
24
- interface TranslateProps {
25
- text: string;
26
- lang: string;
27
- targetLang: string;
28
- apiKey?: string;
29
- model?: string;
30
- }
31
- declare const translate: ({ text, lang, targetLang, apiKey, model }: TranslateProps) => Promise<string>;
32
24
  interface TranslationBatch {
33
25
  entries: Array<{
34
26
  key: string;
@@ -61,4 +53,4 @@ declare class TranslationService {
61
53
  private translateAll;
62
54
  }
63
55
 
64
- export { type LocaleChanges, type LockEntry, type SyncConfig, type TranslationBatch, type TranslationLock, TranslationService, analyzeLocale, translate, translateBatch, updateLock, withLockFile };
56
+ export { type LocaleChanges, type LockEntry, type SyncConfig, type TranslationBatch, type TranslationLock, TranslationService, analyzeLocale, translateBatch, updateLock, withLockFile };
package/dist/index.d.ts CHANGED
@@ -21,14 +21,6 @@ declare function withLockFile<T>(path: string, fn: (lock: TranslationLock) => Pr
21
21
  declare function analyzeLocale(base: Map<string, string>, target: Map<string, string>, locale: string, lock: TranslationLock): LocaleChanges;
22
22
  declare function updateLock(lock: TranslationLock, locale: string, base: Map<string, string>, final: Map<string, string>): void;
23
23
 
24
- interface TranslateProps {
25
- text: string;
26
- lang: string;
27
- targetLang: string;
28
- apiKey?: string;
29
- model?: string;
30
- }
31
- declare const translate: ({ text, lang, targetLang, apiKey, model }: TranslateProps) => Promise<string>;
32
24
  interface TranslationBatch {
33
25
  entries: Array<{
34
26
  key: string;
@@ -61,4 +53,4 @@ declare class TranslationService {
61
53
  private translateAll;
62
54
  }
63
55
 
64
- export { type LocaleChanges, type LockEntry, type SyncConfig, type TranslationBatch, type TranslationLock, TranslationService, analyzeLocale, translate, translateBatch, updateLock, withLockFile };
56
+ export { type LocaleChanges, type LockEntry, type SyncConfig, type TranslationBatch, type TranslationLock, TranslationService, analyzeLocale, translateBatch, updateLock, withLockFile };
package/dist/index.js CHANGED
@@ -32,7 +32,6 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  TranslationService: () => TranslationService,
34
34
  analyzeLocale: () => analyzeLocale,
35
- translate: () => translate,
36
35
  translateBatch: () => translateBatch,
37
36
  updateLock: () => updateLock,
38
37
  withLockFile: () => withLockFile
@@ -124,55 +123,79 @@ function updateLock(lock, locale, base, final) {
124
123
  // src/core/translator.ts
125
124
  var import_ai_sdk_provider = require("@openrouter/ai-sdk-provider");
126
125
  var import_ai = require("ai");
127
- var import_zod = __toESM(require("zod"));
128
- var translate = async ({ text, lang, targetLang, apiKey, model }) => {
129
- const token = apiKey || process.env.OPENROUTER_API_KEY;
130
- if (!token) {
131
- throw new Error("Missing API Key. Please provide apiKey or set OPENROUTER_API_KEY environment variable.");
132
- }
133
- const openrouter = (0, import_ai_sdk_provider.createOpenRouter)({
134
- apiKey: token
135
- });
136
- const system = `
137
- You are a professional translator. Your main task is to translate text from "${lang}" to "${targetLang}".
138
-
139
- IMPORTANT RULES:
140
- 1. Return ONLY the translated text. No explanations, no quotes around the output.
141
- 2. Do NOT translate text inside curly braces, e.g., "{name}", "{count}". These are variables and must remain exactly as they are.
142
- 3. Maintain the original tone and context.
143
- 4. If the text is a single word or short phrase, translate it directly.
144
-
145
- Examples:
146
- - Input: "Hello {name}, welcome back!" -> Output (ES): "Hola {name}, \xA1bienvenido de nuevo!"
147
- - Input: "Contact us calling to {number}" -> Output (FR): "Contactez-nous en appelant le {number}"
148
- `;
149
- const { output } = await (0, import_ai.generateText)({
150
- model: openrouter.languageModel(model || "google/gemini-2.5-flash"),
151
- system,
152
- prompt: `Text to translate: "${text}"`,
153
- output: import_ai.Output.object({
154
- schema: import_zod.default.object({
155
- translatedText: import_zod.default.string()
156
- })
126
+ var z2 = __toESM(require("zod"));
127
+
128
+ // src/env.ts
129
+ var import_env_core = require("@t3-oss/env-core");
130
+ var z = __toESM(require("zod"));
131
+ var env = (0, import_env_core.createEnv)({
132
+ server: {
133
+ OPENROUTER_API_KEY: z.string()
134
+ },
135
+ runtimeEnv: process.env
136
+ });
137
+
138
+ // src/core/translator.ts
139
+ var DEFAULT_MODEL = "google/gemini-2.5-flash";
140
+ var translationSchema = z2.object({
141
+ translations: z2.array(
142
+ z2.object({
143
+ key: z2.string(),
144
+ value: z2.string()
157
145
  })
158
- });
159
- return output.translatedText;
160
- };
146
+ )
147
+ });
148
+ var openrouter = null;
149
+ var getOpenRouter = () => openrouter ?? (openrouter = (0, import_ai_sdk_provider.createOpenRouter)({ apiKey: env.OPENROUTER_API_KEY }));
150
+ var SYSTEM_PROMPT = `You are a professional translator.
151
+
152
+ RULES:
153
+ 1. Translate the provided key-value pairs from the source language to the target language
154
+ 2. Return an array of translations with the EXACT same keys and their translated values
155
+ 3. Do NOT translate variables in curly braces: {name}, {count}, etc. Keep them exactly as-is
156
+ 4. Maintain original tone and context
157
+ 5. For single words or short phrases, translate directly`;
158
+ async function withRetry(fn, maxAttempts = 3) {
159
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
160
+ try {
161
+ return await fn();
162
+ } catch (error) {
163
+ const msg = error.message?.toLowerCase() ?? "";
164
+ const retryable = msg.includes("network") || msg.includes("timeout") || msg.includes("econnreset") || msg.includes("429") || msg.includes("503") || msg.includes("502");
165
+ if (attempt === maxAttempts || !retryable) throw error;
166
+ await new Promise((r) => setTimeout(r, 2 ** (attempt - 1) * 1e3));
167
+ }
168
+ }
169
+ throw new Error("Unreachable");
170
+ }
161
171
  async function translateBatch(batch) {
162
172
  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
- })
174
- );
175
- return new Map(results);
173
+ const inputArray = batch.entries.map((e) => ({ key: e.key, text: e.text }));
174
+ return withRetry(async () => {
175
+ const { output } = await (0, import_ai.generateText)({
176
+ model: getOpenRouter().languageModel(DEFAULT_MODEL),
177
+ output: import_ai.Output.object({
178
+ schema: translationSchema
179
+ }),
180
+ system: SYSTEM_PROMPT,
181
+ prompt: `Translate from "${batch.sourceLang}" to "${batch.targetLang}":
182
+
183
+ ${JSON.stringify(inputArray, null, 2)}
184
+
185
+ Return the translations array with the same keys and translated values.`
186
+ });
187
+ if (!output.translations || output.translations.length === 0) {
188
+ throw new Error("Translation returned empty result");
189
+ }
190
+ const resultMap = new Map(output.translations.map((t) => [t.key, t.value]));
191
+ const missingKeys = batch.entries.filter((e) => !resultMap.has(e.key));
192
+ if (missingKeys.length > 0) {
193
+ throw new Error(
194
+ `Missing translations for keys: ${missingKeys.map((e) => e.key).join(", ")}`
195
+ );
196
+ }
197
+ return resultMap;
198
+ });
176
199
  }
177
200
 
178
201
  // src/core/sync.ts
@@ -297,7 +320,6 @@ var TranslationService = class {
297
320
  0 && (module.exports = {
298
321
  TranslationService,
299
322
  analyzeLocale,
300
- translate,
301
323
  translateBatch,
302
324
  updateLock,
303
325
  withLockFile
package/dist/index.mjs CHANGED
@@ -1,15 +1,13 @@
1
1
  import {
2
2
  TranslationService,
3
3
  analyzeLocale,
4
- translate,
5
4
  translateBatch,
6
5
  updateLock,
7
6
  withLockFile
8
- } from "./chunk-5QO2MVWV.mjs";
7
+ } from "./chunk-IOH4K3A3.mjs";
9
8
  export {
10
9
  TranslationService,
11
10
  analyzeLocale,
12
- translate,
13
11
  translateBatch,
14
12
  updateLock,
15
13
  withLockFile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccgp/i18n-ai",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "AI-powered i18n translation and synchronization library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",