@ccgp/i18n-ai 0.0.4 → 0.1.1

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.
@@ -236,9 +236,30 @@ var TranslationService = class {
236
236
  }
237
237
  let translatedCount = 0;
238
238
  if (toTranslate.length > 0) {
239
- for (const change of toTranslate) {
239
+ const limit = (concurrency) => {
240
+ let active = 0;
241
+ const queue = [];
242
+ const run = async (fn) => {
243
+ if (active >= concurrency) {
244
+ await new Promise((resolve) => queue.push(resolve));
245
+ }
246
+ active++;
247
+ try {
248
+ return await fn();
249
+ } finally {
250
+ active--;
251
+ if (queue.length > 0) {
252
+ queue.shift()();
253
+ }
254
+ }
255
+ };
256
+ return run;
257
+ };
258
+ const runTask = limit(5);
259
+ await Promise.all(toTranslate.map((change) => runTask(async () => {
240
260
  try {
241
- const translatedValue = await translate({
261
+ const _translate = this.config.translator || translate;
262
+ const translatedValue = await _translate({
242
263
  text: change.sourceText,
243
264
  lang: this.config.defaultLocale,
244
265
  targetLang: targetLocale,
@@ -258,7 +279,7 @@ var TranslationService = class {
258
279
  targetFlat.set(change.key, change.currentTranslation);
259
280
  }
260
281
  }
261
- }
282
+ })));
262
283
  }
263
284
  for (const change of changes.unchanged) {
264
285
  if (change.currentTranslation) {
package/dist/cli.js CHANGED
@@ -268,9 +268,30 @@ var TranslationService = class {
268
268
  }
269
269
  let translatedCount = 0;
270
270
  if (toTranslate.length > 0) {
271
- for (const change of toTranslate) {
271
+ const limit = (concurrency) => {
272
+ let active = 0;
273
+ const queue = [];
274
+ const run = async (fn) => {
275
+ if (active >= concurrency) {
276
+ await new Promise((resolve2) => queue.push(resolve2));
277
+ }
278
+ active++;
279
+ try {
280
+ return await fn();
281
+ } finally {
282
+ active--;
283
+ if (queue.length > 0) {
284
+ queue.shift()();
285
+ }
286
+ }
287
+ };
288
+ return run;
289
+ };
290
+ const runTask = limit(5);
291
+ await Promise.all(toTranslate.map((change) => runTask(async () => {
272
292
  try {
273
- const translatedValue = await translate({
293
+ const _translate = this.config.translator || translate;
294
+ const translatedValue = await _translate({
274
295
  text: change.sourceText,
275
296
  lang: this.config.defaultLocale,
276
297
  targetLang: targetLocale,
@@ -290,7 +311,7 @@ var TranslationService = class {
290
311
  targetFlat.set(change.key, change.currentTranslation);
291
312
  }
292
313
  }
293
- }
314
+ })));
294
315
  }
295
316
  for (const change of changes.unchanged) {
296
317
  if (change.currentTranslation) {
@@ -311,36 +332,102 @@ var TranslationService = class {
311
332
 
312
333
  // src/cli.ts
313
334
  var import_dotenv = __toESM(require("dotenv"));
335
+ var import_prompts = __toESM(require("prompts"));
336
+ var import_chalk2 = __toESM(require("chalk"));
314
337
  import_dotenv.default.config();
315
338
  var program = new import_commander.Command();
316
339
  program.name("i18n-ai").description("AI-powered translation CLI").version("0.0.1");
317
- program.command("sync").description("Synchronize translations using AI").option("-d, --dir <path>", "Messages directory", "messages").option("-l, --locales <items>", "Comma separated list of locales").option("--default <locale>", "Default locale", "en").option("--lock <path>", "Lock file path", "translation-lock.json").action(async (options) => {
340
+ var CONFIG_FILE = "i18n-ai.config.json";
341
+ async function loadConfig() {
342
+ try {
343
+ const configPath = (0, import_path2.join)(process.cwd(), CONFIG_FILE);
344
+ const content = await (0, import_promises3.readFile)(configPath, "utf-8");
345
+ return JSON.parse(content);
346
+ } catch {
347
+ return null;
348
+ }
349
+ }
350
+ program.command("init").description("Initialize i18n-ai configuration").action(async () => {
351
+ console.log(import_chalk2.default.bold("\u{1F680} Initializing i18n-ai configuration\n"));
352
+ const response = await (0, import_prompts.default)([
353
+ {
354
+ type: "text",
355
+ name: "messagesDir",
356
+ message: "Where are your messages located?",
357
+ initial: "messages",
358
+ validate: (value) => value.length > 0 ? true : "Directory cannot be empty"
359
+ },
360
+ {
361
+ type: "text",
362
+ name: "defaultLocale",
363
+ message: "What is your default locale?",
364
+ initial: "en"
365
+ },
366
+ {
367
+ type: "list",
368
+ name: "locales",
369
+ message: "Which other locales do you support? (comma separated)",
370
+ initial: "es, fr",
371
+ separator: ","
372
+ }
373
+ ]);
374
+ if (!response.messagesDir || !response.defaultLocale) {
375
+ console.log(import_chalk2.default.red("\n\u274C Initialization canceled"));
376
+ return;
377
+ }
378
+ const allLocales = [response.defaultLocale, ...response.locales.filter((l) => l !== response.defaultLocale)];
379
+ const config = {
380
+ defaultLocale: response.defaultLocale,
381
+ locales: allLocales,
382
+ messagesDir: response.messagesDir
383
+ };
384
+ await (0, import_promises3.writeFile)((0, import_path2.join)(process.cwd(), CONFIG_FILE), JSON.stringify(config, null, 2));
385
+ console.log(import_chalk2.default.green(`
386
+ \u2705 Created ${CONFIG_FILE}`));
387
+ const absMessagesDir = (0, import_path2.resolve)(process.cwd(), response.messagesDir);
388
+ await (0, import_promises3.mkdir)(absMessagesDir, { recursive: true });
389
+ const baseFilePath = (0, import_path2.join)(absMessagesDir, `${response.defaultLocale}.json`);
390
+ try {
391
+ await (0, import_promises3.readFile)(baseFilePath);
392
+ } catch {
393
+ await (0, import_promises3.writeFile)(baseFilePath, JSON.stringify({ welcome: "Hello World" }, null, 2));
394
+ console.log(import_chalk2.default.green(`\u2705 Created base file ${response.messagesDir}/${response.defaultLocale}.json`));
395
+ }
396
+ console.log(import_chalk2.default.blue('\n\u{1F389} Setup complete! You can now run "i18n-ai sync"'));
397
+ });
398
+ 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").action(async (options) => {
318
399
  try {
319
400
  const cwd = process.cwd();
320
- const messagesDir = (0, import_path2.resolve)(cwd, options.dir);
321
- let locales = options.locales ? options.locales.split(",") : [];
322
- let defaultLocale = options.default;
323
- if (locales.length === 0) {
324
- try {
325
- const routingPath = (0, import_path2.join)(cwd, "i18n", "routing.ts");
326
- const routingContent = await (0, import_promises3.readFile)(routingPath, "utf-8").catch(() => "");
327
- if (routingContent) {
328
- const localesMatch = routingContent.match(/locales:\s*\[([\s\S]*?)\]/);
329
- const defaultMatch = routingContent.match(/defaultLocale:\s*["'](\w+)["']/);
330
- if (localesMatch) {
331
- locales = localesMatch[1].split(",").map((l) => l.trim().replace(/['"]/g, "")).filter(Boolean);
332
- }
333
- if (defaultMatch && !defaultLocale) {
334
- defaultLocale = defaultMatch[1];
401
+ const config = await loadConfig();
402
+ let messagesDir = options.dir ? (0, import_path2.resolve)(cwd, options.dir) : config ? (0, import_path2.resolve)(cwd, config.messagesDir) : null;
403
+ let locales = options.locales ? options.locales.split(",") : config ? config.locales : [];
404
+ let defaultLocale = options.default || (config ? config.defaultLocale : null);
405
+ if (!messagesDir || locales.length === 0 || !defaultLocale) {
406
+ if (!config) {
407
+ try {
408
+ const routingPath = (0, import_path2.join)(cwd, "i18n", "routing.ts");
409
+ const routingContent = await (0, import_promises3.readFile)(routingPath, "utf-8").catch(() => "");
410
+ if (routingContent) {
411
+ const localesMatch = routingContent.match(/locales:\s*\[([\s\S]*?)\]/);
412
+ const defaultMatch = routingContent.match(/defaultLocale:\s*["'](\w+)["']/);
413
+ if (localesMatch && locales.length === 0) {
414
+ locales = localesMatch[1].split(",").map((l) => l.trim().replace(/['"]/g, "")).filter(Boolean);
415
+ }
416
+ if (defaultMatch && !defaultLocale) {
417
+ defaultLocale = defaultMatch[1];
418
+ }
335
419
  }
420
+ } catch {
336
421
  }
337
- } catch (e) {
338
422
  }
423
+ if (!messagesDir) messagesDir = (0, import_path2.resolve)(cwd, "messages");
424
+ if (!defaultLocale) defaultLocale = "en";
339
425
  }
340
426
  if (locales.length === 0) {
341
- console.error("\u274C No locales found. Please specify --locales or ensure i18n/routing.ts exists.");
427
+ console.error(import_chalk2.default.red('\u274C No locales found. Run "i18n-ai init" or specify --locales.'));
342
428
  process.exit(1);
343
429
  }
430
+ console.log(import_chalk2.default.dim(`Using Config: Default=${defaultLocale}, Locales=${locales.join(",")}, Dir=${messagesDir}`));
344
431
  const service = new TranslationService({
345
432
  locales,
346
433
  defaultLocale,
@@ -349,7 +436,7 @@ program.command("sync").description("Synchronize translations using AI").option(
349
436
  });
350
437
  await service.sync();
351
438
  } catch (error) {
352
- console.error("Fatal error:", error);
439
+ console.error(import_chalk2.default.red("Fatal error:"), error);
353
440
  process.exit(1);
354
441
  }
355
442
  });
package/dist/cli.mjs CHANGED
@@ -1,43 +1,109 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TranslationService
4
- } from "./chunk-NY2C35K2.mjs";
4
+ } from "./chunk-X4OG3I7N.mjs";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command } from "commander";
8
8
  import { resolve, join } from "path";
9
- import { readFile } from "fs/promises";
9
+ import { readFile, writeFile, mkdir } from "fs/promises";
10
10
  import dotenv from "dotenv";
11
+ import prompts from "prompts";
12
+ import chalk from "chalk";
11
13
  dotenv.config();
12
14
  var program = new Command();
13
15
  program.name("i18n-ai").description("AI-powered translation CLI").version("0.0.1");
14
- program.command("sync").description("Synchronize translations using AI").option("-d, --dir <path>", "Messages directory", "messages").option("-l, --locales <items>", "Comma separated list of locales").option("--default <locale>", "Default locale", "en").option("--lock <path>", "Lock file path", "translation-lock.json").action(async (options) => {
16
+ var CONFIG_FILE = "i18n-ai.config.json";
17
+ async function loadConfig() {
18
+ try {
19
+ const configPath = join(process.cwd(), CONFIG_FILE);
20
+ const content = await readFile(configPath, "utf-8");
21
+ return JSON.parse(content);
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ program.command("init").description("Initialize i18n-ai configuration").action(async () => {
27
+ console.log(chalk.bold("\u{1F680} Initializing i18n-ai configuration\n"));
28
+ const response = await prompts([
29
+ {
30
+ type: "text",
31
+ name: "messagesDir",
32
+ message: "Where are your messages located?",
33
+ initial: "messages",
34
+ validate: (value) => value.length > 0 ? true : "Directory cannot be empty"
35
+ },
36
+ {
37
+ type: "text",
38
+ name: "defaultLocale",
39
+ message: "What is your default locale?",
40
+ initial: "en"
41
+ },
42
+ {
43
+ type: "list",
44
+ name: "locales",
45
+ message: "Which other locales do you support? (comma separated)",
46
+ initial: "es, fr",
47
+ separator: ","
48
+ }
49
+ ]);
50
+ if (!response.messagesDir || !response.defaultLocale) {
51
+ console.log(chalk.red("\n\u274C Initialization canceled"));
52
+ return;
53
+ }
54
+ const allLocales = [response.defaultLocale, ...response.locales.filter((l) => l !== response.defaultLocale)];
55
+ const config = {
56
+ defaultLocale: response.defaultLocale,
57
+ locales: allLocales,
58
+ messagesDir: response.messagesDir
59
+ };
60
+ await writeFile(join(process.cwd(), CONFIG_FILE), JSON.stringify(config, null, 2));
61
+ console.log(chalk.green(`
62
+ \u2705 Created ${CONFIG_FILE}`));
63
+ const absMessagesDir = resolve(process.cwd(), response.messagesDir);
64
+ await mkdir(absMessagesDir, { recursive: true });
65
+ const baseFilePath = join(absMessagesDir, `${response.defaultLocale}.json`);
66
+ try {
67
+ await readFile(baseFilePath);
68
+ } catch {
69
+ await writeFile(baseFilePath, JSON.stringify({ welcome: "Hello World" }, null, 2));
70
+ console.log(chalk.green(`\u2705 Created base file ${response.messagesDir}/${response.defaultLocale}.json`));
71
+ }
72
+ console.log(chalk.blue('\n\u{1F389} Setup complete! You can now run "i18n-ai sync"'));
73
+ });
74
+ 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").action(async (options) => {
15
75
  try {
16
76
  const cwd = process.cwd();
17
- const messagesDir = resolve(cwd, options.dir);
18
- let locales = options.locales ? options.locales.split(",") : [];
19
- let defaultLocale = options.default;
20
- if (locales.length === 0) {
21
- try {
22
- const routingPath = join(cwd, "i18n", "routing.ts");
23
- const routingContent = await readFile(routingPath, "utf-8").catch(() => "");
24
- if (routingContent) {
25
- const localesMatch = routingContent.match(/locales:\s*\[([\s\S]*?)\]/);
26
- const defaultMatch = routingContent.match(/defaultLocale:\s*["'](\w+)["']/);
27
- if (localesMatch) {
28
- locales = localesMatch[1].split(",").map((l) => l.trim().replace(/['"]/g, "")).filter(Boolean);
29
- }
30
- if (defaultMatch && !defaultLocale) {
31
- defaultLocale = defaultMatch[1];
77
+ const config = await loadConfig();
78
+ let messagesDir = options.dir ? resolve(cwd, options.dir) : config ? resolve(cwd, config.messagesDir) : null;
79
+ let locales = options.locales ? options.locales.split(",") : config ? config.locales : [];
80
+ let defaultLocale = options.default || (config ? config.defaultLocale : null);
81
+ if (!messagesDir || locales.length === 0 || !defaultLocale) {
82
+ if (!config) {
83
+ try {
84
+ const routingPath = join(cwd, "i18n", "routing.ts");
85
+ const routingContent = await readFile(routingPath, "utf-8").catch(() => "");
86
+ if (routingContent) {
87
+ const localesMatch = routingContent.match(/locales:\s*\[([\s\S]*?)\]/);
88
+ const defaultMatch = routingContent.match(/defaultLocale:\s*["'](\w+)["']/);
89
+ if (localesMatch && locales.length === 0) {
90
+ locales = localesMatch[1].split(",").map((l) => l.trim().replace(/['"]/g, "")).filter(Boolean);
91
+ }
92
+ if (defaultMatch && !defaultLocale) {
93
+ defaultLocale = defaultMatch[1];
94
+ }
32
95
  }
96
+ } catch {
33
97
  }
34
- } catch (e) {
35
98
  }
99
+ if (!messagesDir) messagesDir = resolve(cwd, "messages");
100
+ if (!defaultLocale) defaultLocale = "en";
36
101
  }
37
102
  if (locales.length === 0) {
38
- console.error("\u274C No locales found. Please specify --locales or ensure i18n/routing.ts exists.");
103
+ console.error(chalk.red('\u274C No locales found. Run "i18n-ai init" or specify --locales.'));
39
104
  process.exit(1);
40
105
  }
106
+ console.log(chalk.dim(`Using Config: Default=${defaultLocale}, Locales=${locales.join(",")}, Dir=${messagesDir}`));
41
107
  const service = new TranslationService({
42
108
  locales,
43
109
  defaultLocale,
@@ -46,7 +112,7 @@ program.command("sync").description("Synchronize translations using AI").option(
46
112
  });
47
113
  await service.sync();
48
114
  } catch (error) {
49
- console.error("Fatal error:", error);
115
+ console.error(chalk.red("Fatal error:"), error);
50
116
  process.exit(1);
51
117
  }
52
118
  });
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
@@ -282,9 +282,30 @@ var TranslationService = class {
282
282
  }
283
283
  let translatedCount = 0;
284
284
  if (toTranslate.length > 0) {
285
- for (const change of toTranslate) {
285
+ const limit = (concurrency) => {
286
+ let active = 0;
287
+ const queue = [];
288
+ const run = async (fn) => {
289
+ if (active >= concurrency) {
290
+ await new Promise((resolve) => queue.push(resolve));
291
+ }
292
+ active++;
293
+ try {
294
+ return await fn();
295
+ } finally {
296
+ active--;
297
+ if (queue.length > 0) {
298
+ queue.shift()();
299
+ }
300
+ }
301
+ };
302
+ return run;
303
+ };
304
+ const runTask = limit(5);
305
+ await Promise.all(toTranslate.map((change) => runTask(async () => {
286
306
  try {
287
- const translatedValue = await translate({
307
+ const _translate = this.config.translator || translate;
308
+ const translatedValue = await _translate({
288
309
  text: change.sourceText,
289
310
  lang: this.config.defaultLocale,
290
311
  targetLang: targetLocale,
@@ -304,7 +325,7 @@ var TranslationService = class {
304
325
  targetFlat.set(change.key, change.currentTranslation);
305
326
  }
306
327
  }
307
- }
328
+ })));
308
329
  }
309
330
  for (const change of changes.unchanged) {
310
331
  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-X4OG3I7N.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.0.4",
3
+ "version": "0.1.1",
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",
@@ -40,10 +39,12 @@
40
39
  "chalk": "^5.4.1",
41
40
  "commander": "^12.0.0",
42
41
  "dotenv": "^16.4.5",
42
+ "prompts": "^2.4.2",
43
43
  "zod": "^4.3.5"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^20.11.24",
47
+ "@types/prompts": "^2.4.9",
47
48
  "tsup": "^8.3.5",
48
49
  "tsx": "^4.7.1",
49
50
  "typescript": "^5.3.3"