@cgaravitoq/i18n-ai 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.
package/dist/cli.js ADDED
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_dotenv = require("dotenv");
28
+ var import_commander = require("commander");
29
+ var import_node_path2 = require("path");
30
+ var import_promises3 = require("fs/promises");
31
+
32
+ // src/core/sync.ts
33
+ var import_promises2 = require("fs/promises");
34
+ var import_node_path = require("path");
35
+ var import_p_queue = __toESM(require("p-queue"));
36
+
37
+ // src/core/lock.ts
38
+ var import_node_crypto = require("crypto");
39
+ var import_promises = require("fs/promises");
40
+ var import_proper_lockfile = require("proper-lockfile");
41
+ var hash = (text) => (0, import_node_crypto.createHash)("sha256").update(text).digest("hex").slice(0, 32);
42
+ var hashesMatch = (current, stored) => current === stored || stored.length === 16 && current.startsWith(stored);
43
+ async function loadLock(path) {
44
+ try {
45
+ return JSON.parse(await (0, import_promises.readFile)(path, "utf-8"));
46
+ } catch (e) {
47
+ if (e.code === "ENOENT") return { locales: {} };
48
+ if (e instanceof SyntaxError)
49
+ throw new Error(`Lock file corrupted: ${path}`);
50
+ throw e;
51
+ }
52
+ }
53
+ async function saveLock(path, lock) {
54
+ await (0, import_promises.writeFile)(path, `${JSON.stringify(lock, null, 2)}
55
+ `);
56
+ }
57
+ async function withLockFile(path, fn) {
58
+ let lock = await loadLock(path);
59
+ if (Object.keys(lock.locales).length === 0) await saveLock(path, lock);
60
+ const release = await (0, import_proper_lockfile.lock)(path, {
61
+ retries: { retries: 5, factor: 2, minTimeout: 1e3, maxTimeout: 5e3 },
62
+ stale: 3e4
63
+ });
64
+ try {
65
+ lock = await loadLock(path);
66
+ const result = await fn(lock);
67
+ await saveLock(path, lock);
68
+ return result;
69
+ } finally {
70
+ await release();
71
+ }
72
+ }
73
+ function analyzeLocale(base, target, locale, lock) {
74
+ const result = {
75
+ locale,
76
+ toTranslate: [],
77
+ toPreserve: /* @__PURE__ */ new Map(),
78
+ toRemove: /* @__PURE__ */ new Set(),
79
+ manualEdits: /* @__PURE__ */ new Map()
80
+ };
81
+ const localeData = lock.locales[locale] ?? {};
82
+ for (const [key, text] of base) {
83
+ const entry = localeData[key];
84
+ const current = target.get(key);
85
+ const currentHash = hash(text);
86
+ if (!entry) {
87
+ current ? result.toPreserve.set(key, current) : result.toTranslate.push({ key, text, status: "new" });
88
+ } else if (!hashesMatch(currentHash, entry.sourceHash)) {
89
+ result.toTranslate.push({ key, text, status: "modified" });
90
+ } else if (current && current !== entry.translation) {
91
+ result.manualEdits.set(key, current);
92
+ } else {
93
+ result.toPreserve.set(key, current ?? entry.translation);
94
+ }
95
+ }
96
+ for (const key of target.keys()) {
97
+ if (!base.has(key)) result.toRemove.add(key);
98
+ }
99
+ return result;
100
+ }
101
+ function updateLock(lock, locale, base, final) {
102
+ lock.locales[locale] ??= {};
103
+ const localeData = lock.locales[locale];
104
+ for (const [key, translation] of final) {
105
+ const sourceText = base.get(key);
106
+ if (sourceText) {
107
+ localeData[key] = {
108
+ sourceHash: hash(sourceText),
109
+ sourceText,
110
+ translation
111
+ };
112
+ }
113
+ }
114
+ for (const key of Object.keys(localeData)) {
115
+ if (!base.has(key)) delete localeData[key];
116
+ }
117
+ }
118
+
119
+ // src/core/translator.ts
120
+ var import_ai_sdk_provider = require("@openrouter/ai-sdk-provider");
121
+ var import_ai = require("ai");
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
+ }
167
+ async function translateBatch(batch) {
168
+ if (batch.entries.length === 0) return /* @__PURE__ */ new Map();
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
+ });
195
+ }
196
+
197
+ // src/core/sync.ts
198
+ var chunk = (arr, n) => Array.from(
199
+ { length: Math.ceil(arr.length / n) },
200
+ (_, i) => arr.slice(i * n, (i + 1) * n)
201
+ );
202
+ var flatten = (obj, prefix = "") => {
203
+ const result = /* @__PURE__ */ new Map();
204
+ for (const [k, v] of Object.entries(obj)) {
205
+ const path = prefix ? `${prefix}.${k}` : k;
206
+ typeof v === "string" ? result.set(path, v) : flatten(v, path).forEach((val, p) => result.set(p, val));
207
+ }
208
+ return result;
209
+ };
210
+ var unflatten = (flat) => {
211
+ const result = {};
212
+ for (const [path, value] of flat) {
213
+ const keys = path.split(".");
214
+ let cur = result;
215
+ for (let i = 0; i < keys.length - 1; i++) {
216
+ cur = cur[keys[i]] ??= {};
217
+ }
218
+ cur[keys.at(-1)] = value;
219
+ }
220
+ return result;
221
+ };
222
+ var readJson = async (path) => {
223
+ try {
224
+ return JSON.parse(await (0, import_promises2.readFile)(path, "utf-8"));
225
+ } catch (e) {
226
+ if (e.code === "ENOENT") return null;
227
+ throw e;
228
+ }
229
+ };
230
+ var writeJsonAtomic = async (path, data) => {
231
+ const tmp = `${path}.tmp`;
232
+ await (0, import_promises2.writeFile)(tmp, `${JSON.stringify(data, null, 2)}
233
+ `);
234
+ await (0, import_promises2.rename)(tmp, path);
235
+ };
236
+ var TranslationService = class {
237
+ constructor(config) {
238
+ this.config = config;
239
+ this.queue = new import_p_queue.default({ concurrency: config.concurrency ?? 10 });
240
+ this.batchSize = config.batchSize ?? 20;
241
+ }
242
+ queue;
243
+ batchSize;
244
+ async sync() {
245
+ const basePath = (0, import_node_path.join)(
246
+ this.config.messagesDir,
247
+ `${this.config.defaultLocale}.json`
248
+ );
249
+ const baseObj = await readJson(basePath);
250
+ if (!baseObj) throw new Error(`Base locale file not found: ${basePath}`);
251
+ const base = flatten(baseObj);
252
+ console.log(
253
+ `[ok] Loaded ${this.config.defaultLocale}.json (${base.size} keys)`
254
+ );
255
+ await withLockFile(this.config.lockFilePath, async (lock) => {
256
+ const locales = this.config.locales.filter(
257
+ (l) => l !== this.config.defaultLocale
258
+ );
259
+ await Promise.all(
260
+ locales.map((locale) => this.syncLocale(locale, base, lock))
261
+ );
262
+ });
263
+ console.log("\n[done] Synchronization complete!");
264
+ }
265
+ async syncLocale(locale, base, lock) {
266
+ const targetPath = (0, import_node_path.join)(this.config.messagesDir, `${locale}.json`);
267
+ const targetObj = await readJson(targetPath) ?? {};
268
+ const target = flatten(targetObj);
269
+ const changes = analyzeLocale(base, target, locale, lock);
270
+ const hasWork = changes.toTranslate.length > 0 || changes.toRemove.size > 0 || changes.manualEdits.size > 0;
271
+ console.log(
272
+ `
273
+ [${locale}] ${changes.toTranslate.length} to translate, ${changes.toRemove.size} obsolete`
274
+ );
275
+ if (!hasWork) {
276
+ console.log(" [ok] Already synchronized");
277
+ return;
278
+ }
279
+ const translations = await this.translateAll(changes);
280
+ const final = new Map([
281
+ ...changes.toPreserve,
282
+ ...changes.manualEdits,
283
+ ...translations
284
+ ]);
285
+ changes.toRemove.forEach((k) => final.delete(k));
286
+ updateLock(lock, locale, base, final);
287
+ await writeJsonAtomic(targetPath, unflatten(final));
288
+ console.log(` [ok] Updated ${targetPath}`);
289
+ }
290
+ async translateAll(changes) {
291
+ if (changes.toTranslate.length === 0) return /* @__PURE__ */ new Map();
292
+ const batches = chunk(changes.toTranslate, this.batchSize);
293
+ const results = await Promise.all(
294
+ batches.map(
295
+ (batch) => this.queue.add(
296
+ () => translateBatch({
297
+ entries: batch,
298
+ sourceLang: this.config.defaultLocale,
299
+ targetLang: changes.locale,
300
+ apiKey: this.config.apiKey,
301
+ model: this.config.model
302
+ })
303
+ )
304
+ )
305
+ );
306
+ return results.reduce(
307
+ (acc, r) => r ? new Map([...acc, ...r]) : acc,
308
+ /* @__PURE__ */ new Map()
309
+ );
310
+ }
311
+ };
312
+
313
+ // src/cli.ts
314
+ var import_prompts = __toESM(require("prompts"));
315
+ var import_chalk = __toESM(require("chalk"));
316
+ (0, import_dotenv.config)({ path: (0, import_node_path2.resolve)(process.cwd(), ".env") });
317
+ var VERSION = "0.2.3";
318
+ var program = new import_commander.Command();
319
+ program.name("i18n-ai").description("AI-powered translation CLI").version(VERSION);
320
+ var CONFIG_FILE = "i18n.config.json";
321
+ async function loadConfig() {
322
+ try {
323
+ const configPath = (0, import_node_path2.join)(process.cwd(), CONFIG_FILE);
324
+ const content = await (0, import_promises3.readFile)(configPath, "utf-8");
325
+ return JSON.parse(content);
326
+ } catch {
327
+ return null;
328
+ }
329
+ }
330
+ program.command("init").description("Initialize i18n-ai configuration").action(async () => {
331
+ console.log(import_chalk.default.bold("\u{1F680} Initializing i18n-ai configuration\n"));
332
+ const response = await (0, import_prompts.default)([
333
+ {
334
+ type: "text",
335
+ name: "messagesDir",
336
+ message: "Where are your messages located?",
337
+ initial: "messages",
338
+ validate: (value) => value.length > 0 ? true : "Directory cannot be empty"
339
+ },
340
+ {
341
+ type: "text",
342
+ name: "defaultLocale",
343
+ message: "What is your default locale?",
344
+ initial: "en"
345
+ },
346
+ {
347
+ type: "list",
348
+ name: "locales",
349
+ message: "Which other locales do you support? (comma separated)",
350
+ initial: "es, fr",
351
+ separator: ","
352
+ }
353
+ ]);
354
+ if (!response.messagesDir || !response.defaultLocale) {
355
+ console.log(import_chalk.default.red("\n\u274C Initialization canceled"));
356
+ return;
357
+ }
358
+ const allLocales = [
359
+ response.defaultLocale,
360
+ ...response.locales.map((l) => l.trim()).filter((l) => l && l !== response.defaultLocale)
361
+ ];
362
+ const config = {
363
+ defaultLocale: response.defaultLocale,
364
+ locales: allLocales,
365
+ messagesDir: response.messagesDir
366
+ };
367
+ await (0, import_promises3.writeFile)((0, import_node_path2.join)(process.cwd(), CONFIG_FILE), JSON.stringify(config, null, 2));
368
+ console.log(import_chalk.default.green(`
369
+ \u2705 Created ${CONFIG_FILE}`));
370
+ const absMessagesDir = (0, import_node_path2.resolve)(process.cwd(), response.messagesDir);
371
+ await (0, import_promises3.mkdir)(absMessagesDir, { recursive: true });
372
+ const baseFilePath = (0, import_node_path2.join)(absMessagesDir, `${response.defaultLocale}.json`);
373
+ try {
374
+ await (0, import_promises3.readFile)(baseFilePath);
375
+ } catch {
376
+ await (0, import_promises3.writeFile)(baseFilePath, JSON.stringify({ welcome: "Hello World" }, null, 2));
377
+ console.log(import_chalk.default.green(`\u2705 Created base file ${response.messagesDir}/${response.defaultLocale}.json`));
378
+ }
379
+ if (process.env.OPENROUTER_API_KEY) {
380
+ console.log(import_chalk.default.green("\u2705 OPENROUTER_API_KEY detected in .env"));
381
+ } else {
382
+ console.log(import_chalk.default.yellow("\n\u26A0\uFE0F OPENROUTER_API_KEY not found in .env"));
383
+ console.log(import_chalk.default.dim(" Add it to your .env file: OPENROUTER_API_KEY=your_key"));
384
+ console.log(import_chalk.default.dim(" Get one at: https://openrouter.ai"));
385
+ }
386
+ console.log(import_chalk.default.blue('\n\u{1F389} Setup complete! You can now run "i18n-ai sync"'));
387
+ });
388
+ 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) => {
389
+ try {
390
+ const cwd = process.cwd();
391
+ const config = await loadConfig();
392
+ const messagesDir = options.dir ? (0, import_node_path2.resolve)(cwd, options.dir) : config?.messagesDir ? (0, import_node_path2.resolve)(cwd, config.messagesDir) : (0, import_node_path2.resolve)(cwd, "messages");
393
+ const locales = options.locales ? options.locales.split(",").map((l) => l.trim()).filter(Boolean) : config?.locales ?? [];
394
+ const defaultLocale = options.default ?? config?.defaultLocale ?? "en";
395
+ if (locales.length === 0) {
396
+ console.error(import_chalk.default.red('No locales found. Run "i18n-ai init" or specify --locales.'));
397
+ process.exit(1);
398
+ }
399
+ console.log(import_chalk.default.dim(`Using Config: Default=${defaultLocale}, Locales=${locales.join(",")}, Dir=${messagesDir}`));
400
+ const service = new TranslationService({
401
+ locales,
402
+ defaultLocale,
403
+ messagesDir,
404
+ lockFilePath: (0, import_node_path2.resolve)(cwd, options.lock),
405
+ apiKey: options.apiKey,
406
+ model: options.model
407
+ });
408
+ await service.sync();
409
+ } catch (error) {
410
+ console.error(import_chalk.default.red("Fatal error:"), error);
411
+ process.exit(1);
412
+ }
413
+ });
414
+ program.parse();
package/dist/cli.mjs ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ TranslationService
4
+ } from "./chunk-IOH4K3A3.mjs";
5
+
6
+ // src/cli.ts
7
+ import { config as loadEnv } from "dotenv";
8
+ import { Command } from "commander";
9
+ import { resolve, join } from "path";
10
+ import { readFile, writeFile, mkdir } from "fs/promises";
11
+ import prompts from "prompts";
12
+ import chalk from "chalk";
13
+ loadEnv({ path: resolve(process.cwd(), ".env") });
14
+ var VERSION = "0.2.3";
15
+ var program = new Command();
16
+ program.name("i18n-ai").description("AI-powered translation CLI").version(VERSION);
17
+ var CONFIG_FILE = "i18n.config.json";
18
+ async function loadConfig() {
19
+ try {
20
+ const configPath = join(process.cwd(), CONFIG_FILE);
21
+ const content = await readFile(configPath, "utf-8");
22
+ return JSON.parse(content);
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ program.command("init").description("Initialize i18n-ai configuration").action(async () => {
28
+ console.log(chalk.bold("\u{1F680} Initializing i18n-ai configuration\n"));
29
+ const response = await prompts([
30
+ {
31
+ type: "text",
32
+ name: "messagesDir",
33
+ message: "Where are your messages located?",
34
+ initial: "messages",
35
+ validate: (value) => value.length > 0 ? true : "Directory cannot be empty"
36
+ },
37
+ {
38
+ type: "text",
39
+ name: "defaultLocale",
40
+ message: "What is your default locale?",
41
+ initial: "en"
42
+ },
43
+ {
44
+ type: "list",
45
+ name: "locales",
46
+ message: "Which other locales do you support? (comma separated)",
47
+ initial: "es, fr",
48
+ separator: ","
49
+ }
50
+ ]);
51
+ if (!response.messagesDir || !response.defaultLocale) {
52
+ console.log(chalk.red("\n\u274C Initialization canceled"));
53
+ return;
54
+ }
55
+ const allLocales = [
56
+ response.defaultLocale,
57
+ ...response.locales.map((l) => l.trim()).filter((l) => l && l !== response.defaultLocale)
58
+ ];
59
+ const config = {
60
+ defaultLocale: response.defaultLocale,
61
+ locales: allLocales,
62
+ messagesDir: response.messagesDir
63
+ };
64
+ await writeFile(join(process.cwd(), CONFIG_FILE), JSON.stringify(config, null, 2));
65
+ console.log(chalk.green(`
66
+ \u2705 Created ${CONFIG_FILE}`));
67
+ const absMessagesDir = resolve(process.cwd(), response.messagesDir);
68
+ await mkdir(absMessagesDir, { recursive: true });
69
+ const baseFilePath = join(absMessagesDir, `${response.defaultLocale}.json`);
70
+ try {
71
+ await readFile(baseFilePath);
72
+ } catch {
73
+ await writeFile(baseFilePath, JSON.stringify({ welcome: "Hello World" }, null, 2));
74
+ console.log(chalk.green(`\u2705 Created base file ${response.messagesDir}/${response.defaultLocale}.json`));
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
+ }
83
+ console.log(chalk.blue('\n\u{1F389} Setup complete! You can now run "i18n-ai sync"'));
84
+ });
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) => {
86
+ try {
87
+ const cwd = process.cwd();
88
+ const config = await loadConfig();
89
+ const messagesDir = options.dir ? resolve(cwd, options.dir) : config?.messagesDir ? resolve(cwd, config.messagesDir) : resolve(cwd, "messages");
90
+ const locales = options.locales ? options.locales.split(",").map((l) => l.trim()).filter(Boolean) : config?.locales ?? [];
91
+ const defaultLocale = options.default ?? config?.defaultLocale ?? "en";
92
+ if (locales.length === 0) {
93
+ console.error(chalk.red('No locales found. Run "i18n-ai init" or specify --locales.'));
94
+ process.exit(1);
95
+ }
96
+ console.log(chalk.dim(`Using Config: Default=${defaultLocale}, Locales=${locales.join(",")}, Dir=${messagesDir}`));
97
+ const service = new TranslationService({
98
+ locales,
99
+ defaultLocale,
100
+ messagesDir,
101
+ lockFilePath: resolve(cwd, options.lock),
102
+ apiKey: options.apiKey,
103
+ model: options.model
104
+ });
105
+ await service.sync();
106
+ } catch (error) {
107
+ console.error(chalk.red("Fatal error:"), error);
108
+ process.exit(1);
109
+ }
110
+ });
111
+ program.parse();
@@ -0,0 +1,56 @@
1
+ interface TranslationLock {
2
+ locales: Record<string, Record<string, LockEntry>>;
3
+ }
4
+ interface LockEntry {
5
+ sourceHash: string;
6
+ sourceText: string;
7
+ translation: string;
8
+ }
9
+ interface LocaleChanges {
10
+ locale: string;
11
+ toTranslate: Array<{
12
+ key: string;
13
+ text: string;
14
+ status: 'new' | 'modified';
15
+ }>;
16
+ toPreserve: Map<string, string>;
17
+ toRemove: Set<string>;
18
+ manualEdits: Map<string, string>;
19
+ }
20
+ declare function withLockFile<T>(path: string, fn: (lock: TranslationLock) => Promise<T>): Promise<T>;
21
+ declare function analyzeLocale(base: Map<string, string>, target: Map<string, string>, locale: string, lock: TranslationLock): LocaleChanges;
22
+ declare function updateLock(lock: TranslationLock, locale: string, base: Map<string, string>, final: Map<string, string>): void;
23
+
24
+ interface TranslationBatch {
25
+ entries: Array<{
26
+ key: string;
27
+ text: string;
28
+ }>;
29
+ sourceLang: string;
30
+ targetLang: string;
31
+ apiKey?: string;
32
+ model?: string;
33
+ }
34
+ declare function translateBatch(batch: TranslationBatch): Promise<Map<string, string>>;
35
+
36
+ interface SyncConfig {
37
+ locales: string[];
38
+ defaultLocale: string;
39
+ messagesDir: string;
40
+ lockFilePath: string;
41
+ concurrency?: number;
42
+ batchSize?: number;
43
+ apiKey?: string;
44
+ model?: string;
45
+ }
46
+ declare class TranslationService {
47
+ private config;
48
+ private queue;
49
+ private batchSize;
50
+ constructor(config: SyncConfig);
51
+ sync(): Promise<void>;
52
+ private syncLocale;
53
+ private translateAll;
54
+ }
55
+
56
+ export { type LocaleChanges, type LockEntry, type SyncConfig, type TranslationBatch, type TranslationLock, TranslationService, analyzeLocale, translateBatch, updateLock, withLockFile };
@@ -0,0 +1,56 @@
1
+ interface TranslationLock {
2
+ locales: Record<string, Record<string, LockEntry>>;
3
+ }
4
+ interface LockEntry {
5
+ sourceHash: string;
6
+ sourceText: string;
7
+ translation: string;
8
+ }
9
+ interface LocaleChanges {
10
+ locale: string;
11
+ toTranslate: Array<{
12
+ key: string;
13
+ text: string;
14
+ status: 'new' | 'modified';
15
+ }>;
16
+ toPreserve: Map<string, string>;
17
+ toRemove: Set<string>;
18
+ manualEdits: Map<string, string>;
19
+ }
20
+ declare function withLockFile<T>(path: string, fn: (lock: TranslationLock) => Promise<T>): Promise<T>;
21
+ declare function analyzeLocale(base: Map<string, string>, target: Map<string, string>, locale: string, lock: TranslationLock): LocaleChanges;
22
+ declare function updateLock(lock: TranslationLock, locale: string, base: Map<string, string>, final: Map<string, string>): void;
23
+
24
+ interface TranslationBatch {
25
+ entries: Array<{
26
+ key: string;
27
+ text: string;
28
+ }>;
29
+ sourceLang: string;
30
+ targetLang: string;
31
+ apiKey?: string;
32
+ model?: string;
33
+ }
34
+ declare function translateBatch(batch: TranslationBatch): Promise<Map<string, string>>;
35
+
36
+ interface SyncConfig {
37
+ locales: string[];
38
+ defaultLocale: string;
39
+ messagesDir: string;
40
+ lockFilePath: string;
41
+ concurrency?: number;
42
+ batchSize?: number;
43
+ apiKey?: string;
44
+ model?: string;
45
+ }
46
+ declare class TranslationService {
47
+ private config;
48
+ private queue;
49
+ private batchSize;
50
+ constructor(config: SyncConfig);
51
+ sync(): Promise<void>;
52
+ private syncLocale;
53
+ private translateAll;
54
+ }
55
+
56
+ export { type LocaleChanges, type LockEntry, type SyncConfig, type TranslationBatch, type TranslationLock, TranslationService, analyzeLocale, translateBatch, updateLock, withLockFile };