@ccgp/i18n-ai 0.2.2 → 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,71 +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 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])
131
- );
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
- })
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;
149
158
  });
150
- return new Map(Object.entries(output));
151
159
  }
152
160
 
153
161
  // src/core/sync.ts
@@ -273,7 +281,6 @@ export {
273
281
  withLockFile,
274
282
  analyzeLocale,
275
283
  updateLock,
276
- translate,
277
284
  translateBatch,
278
285
  TranslationService
279
286
  };
package/dist/cli.js CHANGED
@@ -119,38 +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"));
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()
141
+ })
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
+ }
123
167
  async function translateBatch(batch) {
124
168
  if (batch.entries.length === 0) return /* @__PURE__ */ new Map();
125
- const token = batch.apiKey || process.env.OPENROUTER_API_KEY;
126
- if (!token) {
127
- throw new Error("Missing API Key. Please provide apiKey or set OPENROUTER_API_KEY environment variable.");
128
- }
129
- const openrouter = (0, import_ai_sdk_provider.createOpenRouter)({
130
- apiKey: token
131
- });
132
- const inputPayload = Object.fromEntries(
133
- batch.entries.map((e) => [e.key, e.text])
134
- );
135
- const system = `
136
- You are a professional translator.
137
- Task: Translate the values of the JSON object from "${batch.sourceLang}" to "${batch.targetLang}".
138
-
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.
144
- `;
145
- const { output } = await (0, import_ai.generateText)({
146
- model: openrouter.languageModel(batch.model || "google/gemini-2.5-flash"),
147
- system,
148
- prompt: JSON.stringify(inputPayload, null, 2),
149
- output: import_ai.Output.object({
150
- schema: import_zod.default.record(import_zod.default.string(), import_zod.default.string())
151
- })
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;
152
194
  });
153
- return new Map(Object.entries(output));
154
195
  }
155
196
 
156
197
  // src/core/sync.ts
@@ -273,7 +314,7 @@ var TranslationService = class {
273
314
  var import_prompts = __toESM(require("prompts"));
274
315
  var import_chalk = __toESM(require("chalk"));
275
316
  (0, import_dotenv.config)({ path: (0, import_node_path2.resolve)(process.cwd(), ".env") });
276
- var VERSION = "0.2.2";
317
+ var VERSION = "0.2.3";
277
318
  var program = new import_commander.Command();
278
319
  program.name("i18n-ai").description("AI-powered translation CLI").version(VERSION);
279
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-OQMB7A4T.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.2";
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,71 +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 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])
172
- );
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
- })
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;
190
198
  });
191
- return new Map(Object.entries(output));
192
199
  }
193
200
 
194
201
  // src/core/sync.ts
@@ -313,7 +320,6 @@ var TranslationService = class {
313
320
  0 && (module.exports = {
314
321
  TranslationService,
315
322
  analyzeLocale,
316
- translate,
317
323
  translateBatch,
318
324
  updateLock,
319
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-OQMB7A4T.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.2",
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",