@mariokreitz/langsync 0.1.0 → 0.3.0

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
@@ -18,10 +18,16 @@ chaos of hand-edited JSON or fragile Excel hand-offs.
18
18
 
19
19
  ## Install
20
20
 
21
+ <!-- embedme ../../docs/shared/install.sh -->
22
+
21
23
  ```bash
22
24
  pnpm add -D @mariokreitz/langsync
23
- # or: npm install -D @mariokreitz/langsync
24
- # or: yarn add -D @mariokreitz/langsync
25
+ # or
26
+ npm install -D @mariokreitz/langsync
27
+ # or
28
+ yarn add -D @mariokreitz/langsync
29
+
30
+
25
31
  ```
26
32
 
27
33
  > Requires **Node.js 22+** and an ESM-compatible project.
@@ -38,10 +44,13 @@ npx langsync validate
38
44
  # 3. Add missing keys (empty placeholders) to non-reference locales
39
45
  npx langsync sync
40
46
 
41
- # 4. Hand off to translators via Excel
47
+ # 4. Optionally fill the gaps with AI
48
+ npx langsync translate
49
+
50
+ # 5. Hand off to translators via Excel
42
51
  npx langsync export excel
43
52
 
44
- # 5. Import their work back
53
+ # 6. Import their work back
45
54
  npx langsync import excel
46
55
  ```
47
56
 
@@ -53,6 +62,8 @@ npx langsync import excel
53
62
  | `langsync validate` | Report missing, extra, and empty keys; exits non-zero on errors. |
54
63
  | `langsync find-missing` | Report missing keys per locale; exits non-zero on errors. |
55
64
  | `langsync sync` | Synchronize keys from the reference locale into every other locale. |
65
+ | `langsync translate` | Fill empty values in non-reference locales using an AI provider. |
66
+ | `langsync watch` | Watch locale files and run incremental sync + validation on change. |
56
67
  | `langsync export excel` | Export all locales into a single `.xlsx` workbook. |
57
68
  | `langsync import excel` | Import translations from a workbook back into JSON files. |
58
69
 
@@ -61,8 +72,9 @@ All read commands support `--reporter json`. All write commands support
61
72
 
62
73
  ## Configuration
63
74
 
75
+ <!-- embedme ../../docs/shared/config.ts -->
76
+
64
77
  ```ts
65
- // langsync.config.ts
66
78
  import { defineConfig } from '@mariokreitz/langsync';
67
79
 
68
80
  export default defineConfig({
@@ -71,6 +83,14 @@ export default defineConfig({
71
83
  locales: ['en', 'de', 'fr'],
72
84
  defaultLocale: 'en',
73
85
  framework: 'i18next',
86
+ excel: {
87
+ file: 'translations.xlsx',
88
+ sheetName: 'Translations',
89
+ },
90
+ ai: {
91
+ provider: 'openai',
92
+ model: 'gpt-4o-mini',
93
+ },
74
94
  });
75
95
  ```
76
96
 
package/dist/cli.js CHANGED
@@ -1,21 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { logger } from '@langsync/shared/logger';
4
- import chalk3 from 'chalk';
3
+ import chalk from 'chalk';
5
4
  import ora from 'ora';
6
5
  import { relative, join, resolve, dirname } from 'path';
7
6
  import { readFile, mkdir, writeFile, access } from 'fs/promises';
8
- import '@langsync/shared/types';
9
7
  import prompts from 'prompts';
10
- import { loadConfig } from '@langsync/shared/config';
11
- import { loadLocaleFiles, writeJson } from '@langsync/shared/fs';
12
- import { syncTrees, validateLocales } from '@langsync/core';
13
- import { exportToExcel, importFromExcel } from '@langsync/excel-engine';
8
+ import { cosmiconfig } from 'cosmiconfig';
9
+ import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
10
+ import { z } from 'zod';
11
+ import ExcelJS from 'exceljs';
12
+ import chokidar from 'chokidar';
14
13
 
14
+ var PREFIX = chalk.bold.cyan("langsync");
15
+ var logger = {
16
+ info: (message) => console.log(`${PREFIX} ${chalk.blue("\u2139")} ${message}`),
17
+ success: (message) => console.log(`${PREFIX} ${chalk.green("\u2714")} ${message}`),
18
+ warn: (message) => console.warn(`${PREFIX} ${chalk.yellow("\u26A0")} ${message}`),
19
+ error: (message) => console.error(`${PREFIX} ${chalk.red("\u2716")} ${message}`),
20
+ debug: (message) => {
21
+ if (process.env.LANGSYNC_DEBUG) {
22
+ console.log(`${PREFIX} ${chalk.gray("\u2022")} ${chalk.gray(message)}`);
23
+ }
24
+ }
25
+ };
15
26
  function printBanner(version) {
16
- const title = chalk3.bold.cyan("LangSync");
17
- const tagline = chalk3.gray("Modern localization workflow tooling.");
18
- const ver = chalk3.dim(`v${version}`);
27
+ const title = chalk.bold.cyan("LangSync");
28
+ const tagline = chalk.gray("Modern localization workflow tooling.");
29
+ const ver = chalk.dim(`v${version}`);
19
30
  console.log(`
20
31
  ${title} ${ver}
21
32
  ${tagline}
@@ -26,7 +37,7 @@ function assertNodeVersion(minMajor) {
26
37
  const major = Number.parseInt(current.split(".")[0] ?? "0", 10);
27
38
  if (major < minMajor) {
28
39
  console.error(
29
- chalk3.red(
40
+ chalk.red(
30
41
  `langsync requires Node.js >= ${String(minMajor)}. You are running ${current}. Please upgrade.`
31
42
  )
32
43
  );
@@ -34,7 +45,7 @@ function assertNodeVersion(minMajor) {
34
45
  }
35
46
  }
36
47
  var FRAMEWORK_SIGNATURES = [
37
- { framework: "i18next", packages: ["i18next", "react-i18next", "vue-i18next"] },
48
+ { framework: "i18next", packages: ["i18next", "react-i18next", "i18next-vue", "vue-i18next"] },
38
49
  { framework: "ngx-translate", packages: ["@ngx-translate/core", "@ngx-translate/http-loader"] },
39
50
  { framework: "react-intl", packages: ["react-intl", "@formatjs/intl"] }
40
51
  ];
@@ -206,7 +217,7 @@ function registerInitCommand(program) {
206
217
  const detectSpinner = ora("Detecting i18n framework\u2026").start();
207
218
  const detectedFramework = await detectFramework(cwd);
208
219
  detectSpinner.succeed(
209
- detectedFramework ? `Detected ${chalk3.cyan(detectedFramework)}` : "No known framework detected"
220
+ detectedFramework ? `Detected ${chalk.cyan(detectedFramework)}` : "No known framework detected"
210
221
  );
211
222
  const answers = await runInitPrompts({
212
223
  detectedFramework,
@@ -221,12 +232,12 @@ function registerInitCommand(program) {
221
232
  });
222
233
  writeSpinner.succeed("Files written");
223
234
  console.log();
224
- logger.success(`Created ${chalk3.bold(relative(cwd, result.configPath))}`);
235
+ logger.success(`Created ${chalk.bold(relative(cwd, result.configPath))}`);
225
236
  for (const localeFile of result.createdLocaleFiles) {
226
- logger.success(`Created ${chalk3.bold(relative(cwd, localeFile))}`);
237
+ logger.success(`Created ${chalk.bold(relative(cwd, localeFile))}`);
227
238
  }
228
239
  console.log();
229
- logger.info(`Next: run ${chalk3.bold("langsync validate")} to check your locales.`);
240
+ logger.info(`Next: run ${chalk.bold("langsync validate")} to check your locales.`);
230
241
  } catch (error) {
231
242
  const message = error instanceof Error ? error.message : String(error);
232
243
  logger.error(message);
@@ -234,6 +245,136 @@ function registerInitCommand(program) {
234
245
  }
235
246
  });
236
247
  }
248
+ var LangSyncConfigSchema = z.object({
249
+ input: z.string().describe("Path to the source i18n directory."),
250
+ output: z.string().describe("Path to the output/translations directory."),
251
+ locales: z.array(z.string()).min(1).describe("List of supported locales."),
252
+ defaultLocale: z.string().optional(),
253
+ framework: z.enum(["i18next", "ngx-translate", "react-intl"]).optional().describe("i18n framework integration."),
254
+ excel: z.object({
255
+ file: z.string().default("translations.xlsx"),
256
+ sheetName: z.string().default("Translations")
257
+ }).optional(),
258
+ ai: z.object({
259
+ provider: z.enum(["openai", "deepl", "anthropic", "gemini"]).default("openai").describe("AI translation provider."),
260
+ apiKey: z.string().optional().describe("API key. Falls back to the provider-specific env var."),
261
+ model: z.string().optional().describe("Provider model id (e.g. gpt-4o-mini).")
262
+ }).optional().describe("AI translation settings.")
263
+ });
264
+ async function loadConfig(cwd = process.cwd()) {
265
+ const explorer = cosmiconfig("langsync", {
266
+ searchPlaces: [
267
+ "langsync.config.ts",
268
+ "langsync.config.js",
269
+ "langsync.config.mjs",
270
+ "langsync.config.json",
271
+ ".langsyncrc",
272
+ ".langsyncrc.json",
273
+ "package.json"
274
+ ],
275
+ loaders: {
276
+ ".ts": TypeScriptLoader()
277
+ }
278
+ });
279
+ const result = await explorer.search(cwd);
280
+ if (!result) return null;
281
+ const parsed = LangSyncConfigSchema.parse(result.config);
282
+ return { config: parsed, filepath: result.filepath };
283
+ }
284
+ async function writeJson(filePath, data, { indent = 2 } = {}) {
285
+ const absolute = resolve(filePath);
286
+ await mkdir(dirname(absolute), { recursive: true });
287
+ await writeFile(absolute, JSON.stringify(data, null, indent) + "\n", "utf-8");
288
+ }
289
+ async function loadLocaleFiles(options) {
290
+ const inputAbs = resolve(options.cwd, options.inputDir);
291
+ const out = [];
292
+ for (const locale of options.locales) {
293
+ const path = join(inputAbs, `${locale}.json`);
294
+ let translations = {};
295
+ try {
296
+ const content = await readFile(path, "utf-8");
297
+ try {
298
+ translations = JSON.parse(content);
299
+ } catch (error) {
300
+ const message = error instanceof Error ? error.message : String(error);
301
+ throw new Error(`Failed to parse ${locale}.json: ${message}`, { cause: error });
302
+ }
303
+ } catch (error) {
304
+ const errno = error.code;
305
+ if (errno !== "ENOENT") throw error;
306
+ }
307
+ out.push({ locale, path, translations });
308
+ }
309
+ return out;
310
+ }
311
+
312
+ // ../core/dist/index.js
313
+ function flatten(tree, prefix = "") {
314
+ const out = {};
315
+ for (const [key, value] of Object.entries(tree)) {
316
+ const fullKey = prefix ? `${prefix}.${key}` : key;
317
+ if (typeof value === "string") {
318
+ out[fullKey] = value;
319
+ } else {
320
+ Object.assign(out, flatten(value, fullKey));
321
+ }
322
+ }
323
+ return out;
324
+ }
325
+ function unflatten(flat) {
326
+ const result = {};
327
+ for (const [key, value] of Object.entries(flat)) {
328
+ const parts = key.split(".");
329
+ let cursor = result;
330
+ for (let i = 0; i < parts.length - 1; i++) {
331
+ const part = parts[i];
332
+ const next = cursor[part];
333
+ if (typeof next !== "object" || next === null) {
334
+ const fresh = {};
335
+ cursor[part] = fresh;
336
+ cursor = fresh;
337
+ } else {
338
+ cursor = next;
339
+ }
340
+ }
341
+ cursor[parts[parts.length - 1]] = value;
342
+ }
343
+ return result;
344
+ }
345
+ function validateLocales(files, referenceLocale) {
346
+ const issues = [];
347
+ const reference = files.find((f) => f.locale === referenceLocale);
348
+ if (!reference) return issues;
349
+ const referenceKeys = new Set(Object.keys(flatten(reference.translations)));
350
+ for (const file of files) {
351
+ const flat = flatten(file.translations);
352
+ const fileKeys = new Set(Object.keys(flat));
353
+ if (file.locale !== referenceLocale) {
354
+ for (const key of referenceKeys) {
355
+ if (!fileKeys.has(key)) issues.push({ type: "missing", locale: file.locale, key });
356
+ }
357
+ for (const key of fileKeys) {
358
+ if (!referenceKeys.has(key)) issues.push({ type: "extra", locale: file.locale, key });
359
+ }
360
+ }
361
+ for (const [key, value] of Object.entries(flat)) {
362
+ if (!value || value.trim() === "") issues.push({ type: "empty", locale: file.locale, key });
363
+ }
364
+ }
365
+ return issues;
366
+ }
367
+ function syncTrees(source, target) {
368
+ const sourceFlat = flatten(source);
369
+ const targetFlat = flatten(target);
370
+ const merged = {};
371
+ for (const key of Object.keys(sourceFlat)) {
372
+ merged[key] = targetFlat[key] ?? "";
373
+ }
374
+ return unflatten(merged);
375
+ }
376
+
377
+ // src/commands/sync/run.ts
237
378
  async function runSync(options) {
238
379
  const loaded = await loadConfig(options.cwd);
239
380
  if (!loaded) {
@@ -275,11 +416,11 @@ function registerSyncCommand(program) {
275
416
  });
276
417
  const targets = flags.dryRun ? planned : written;
277
418
  if (targets.length === 0) {
278
- logger.info(`Nothing to sync against ${chalk3.cyan(referenceLocale)}.`);
419
+ logger.info(`Nothing to sync against ${chalk.cyan(referenceLocale)}.`);
279
420
  } else {
280
421
  const verb = flags.dryRun ? "Would update" : "Updated";
281
422
  for (const path of targets) {
282
- logger.success(`${verb} ${chalk3.bold(relative(cwd, path))}`);
423
+ logger.success(`${verb} ${chalk.bold(relative(cwd, path))}`);
283
424
  }
284
425
  }
285
426
  } catch (error) {
@@ -289,6 +430,8 @@ function registerSyncCommand(program) {
289
430
  }
290
431
  });
291
432
  }
433
+
434
+ // src/commands/validate/run.ts
292
435
  async function runValidate(options) {
293
436
  const loaded = await loadConfig(options.cwd);
294
437
  if (!loaded) {
@@ -319,13 +462,13 @@ function registerValidateCommand(program) {
319
462
  console.log(JSON.stringify({ referenceLocale, issues }, null, 2));
320
463
  } else {
321
464
  if (issues.length === 0) {
322
- logger.success(`All locales are consistent with ${chalk3.cyan(referenceLocale)}.`);
465
+ logger.success(`All locales are consistent with ${chalk.cyan(referenceLocale)}.`);
323
466
  } else {
324
467
  const byType = { missing: 0, extra: 0, empty: 0 };
325
468
  for (const issue of issues) {
326
469
  byType[issue.type]++;
327
- const colored = issue.type === "empty" ? chalk3.yellow(issue.type) : chalk3.red(issue.type);
328
- logger.info(`${colored} ${chalk3.cyan(issue.locale)} ${issue.key}`);
470
+ const colored = issue.type === "empty" ? chalk.yellow(issue.type) : chalk.red(issue.type);
471
+ logger.info(`${colored} ${chalk.cyan(issue.locale)} ${issue.key}`);
329
472
  }
330
473
  console.log();
331
474
  logger.info(
@@ -367,10 +510,10 @@ function registerFindMissingCommand(program) {
367
510
  if (flags.reporter === "json") {
368
511
  console.log(JSON.stringify({ referenceLocale, missingByLocale }, null, 2));
369
512
  } else if (Object.keys(missingByLocale).length === 0) {
370
- logger.success(`No missing keys relative to ${chalk3.cyan(referenceLocale)}.`);
513
+ logger.success(`No missing keys relative to ${chalk.cyan(referenceLocale)}.`);
371
514
  } else {
372
515
  for (const [locale, keys] of Object.entries(missingByLocale)) {
373
- logger.warn(`${chalk3.cyan(locale)} is missing ${keys.length} key(s):`);
516
+ logger.warn(`${chalk.cyan(locale)} is missing ${keys.length} key(s):`);
374
517
  for (const key of keys) console.log(` - ${key}`);
375
518
  }
376
519
  }
@@ -382,6 +525,51 @@ function registerFindMissingCommand(program) {
382
525
  }
383
526
  });
384
527
  }
528
+ async function exportToExcel(options) {
529
+ const workbook = new ExcelJS.Workbook();
530
+ const sheet = workbook.addWorksheet(options.sheetName ?? "Translations");
531
+ const localeKeys = options.locales.map((l) => l.locale);
532
+ sheet.addRow(["key", ...localeKeys]);
533
+ const allKeys = /* @__PURE__ */ new Set();
534
+ const flatPerLocale = options.locales.map((l) => flatten(l.translations));
535
+ for (const flat of flatPerLocale) {
536
+ for (const k of Object.keys(flat)) allKeys.add(k);
537
+ }
538
+ for (const key of [...allKeys].sort()) {
539
+ sheet.addRow([key, ...flatPerLocale.map((flat) => flat[key] ?? "")]);
540
+ }
541
+ sheet.getRow(1).font = { bold: true };
542
+ await workbook.xlsx.writeFile(options.file);
543
+ }
544
+ async function importFromExcel(file, sheetName) {
545
+ const workbook = new ExcelJS.Workbook();
546
+ await workbook.xlsx.readFile(file);
547
+ const sheet = sheetName ? workbook.getWorksheet(sheetName) : workbook.worksheets[0];
548
+ if (!sheet) throw new Error(`Worksheet not found: ${sheetName ?? "<first>"}`);
549
+ const header = sheet.getRow(1).values;
550
+ const locales = header.slice(2).filter((v) => typeof v === "string");
551
+ const flatPerLocale = Object.fromEntries(
552
+ locales.map((l) => [l, {}])
553
+ );
554
+ sheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
555
+ if (rowNumber === 1) return;
556
+ const values = row.values;
557
+ const key = values[1];
558
+ if (!key) return;
559
+ locales.forEach((locale, idx) => {
560
+ const value = values[idx + 2];
561
+ flatPerLocale[locale][key] = typeof value === "string" ? value : "";
562
+ });
563
+ });
564
+ return {
565
+ locales: locales.map((locale) => ({
566
+ locale,
567
+ translations: unflatten(flatPerLocale[locale])
568
+ }))
569
+ };
570
+ }
571
+
572
+ // src/commands/export/run.ts
385
573
  var DEFAULT_FILE = "translations.xlsx";
386
574
  var DEFAULT_SHEET = "Translations";
387
575
  async function runExportExcel(options) {
@@ -417,7 +605,7 @@ function registerExportCommand(program) {
417
605
  sheetName: flags.sheet
418
606
  });
419
607
  logger.success(
420
- `Exported ${chalk3.cyan(String(locales.length))} locale(s) to ${chalk3.bold(
608
+ `Exported ${chalk.cyan(String(locales.length))} locale(s) to ${chalk.bold(
421
609
  relative(cwd, file)
422
610
  )}`
423
611
  );
@@ -474,10 +662,10 @@ function registerImportCommand(program) {
474
662
  const targets = flags.dryRun ? planned : written;
475
663
  const verb = flags.dryRun ? "Would write" : "Wrote";
476
664
  for (const path of targets) {
477
- logger.success(`${verb} ${chalk3.bold(relative(cwd, path))}`);
665
+ logger.success(`${verb} ${chalk.bold(relative(cwd, path))}`);
478
666
  }
479
667
  for (const locale of skipped) {
480
- logger.warn(`Skipped ${chalk3.cyan(locale)} (not in configured locales).`);
668
+ logger.warn(`Skipped ${chalk.cyan(locale)} (not in configured locales).`);
481
669
  }
482
670
  } catch (error) {
483
671
  const message = error instanceof Error ? error.message : String(error);
@@ -487,8 +675,263 @@ function registerImportCommand(program) {
487
675
  });
488
676
  }
489
677
 
678
+ // ../ai-engine/dist/index.js
679
+ var DEFAULT_MODEL = "gpt-4o-mini";
680
+ var ENDPOINT = "https://api.openai.com/v1/chat/completions";
681
+ var OpenAIAdapter = class {
682
+ provider = "openai";
683
+ apiKey;
684
+ model;
685
+ fetchImpl;
686
+ constructor(options = {}) {
687
+ const apiKey = options.apiKey ?? process.env.OPENAI_API_KEY;
688
+ if (!apiKey) {
689
+ throw new Error(
690
+ "OpenAI API key missing. Set `ai.apiKey` in your config or the OPENAI_API_KEY env var."
691
+ );
692
+ }
693
+ this.apiKey = apiKey;
694
+ this.model = options.model ?? DEFAULT_MODEL;
695
+ this.fetchImpl = options.fetchImpl ?? fetch;
696
+ }
697
+ async translate(request) {
698
+ const response = await this.fetchImpl(ENDPOINT, {
699
+ method: "POST",
700
+ headers: {
701
+ "content-type": "application/json",
702
+ authorization: `Bearer ${this.apiKey}`
703
+ },
704
+ body: JSON.stringify({
705
+ model: this.model,
706
+ temperature: 0,
707
+ messages: [
708
+ {
709
+ role: "system",
710
+ content: `You are a professional software localization engine. Translate the user message from ${request.sourceLocale} to ${request.targetLocale}. Preserve placeholders, ICU syntax, and surrounding punctuation. Respond with the translation only, no quotes or commentary.`
711
+ },
712
+ { role: "user", content: request.text }
713
+ ]
714
+ })
715
+ });
716
+ if (!response.ok) {
717
+ throw new Error(`OpenAI request failed: ${response.status} ${response.statusText}`);
718
+ }
719
+ const data = await response.json();
720
+ const content = data.choices?.[0]?.message?.content?.trim();
721
+ if (!content) {
722
+ throw new Error("OpenAI returned an empty translation.");
723
+ }
724
+ return content;
725
+ }
726
+ };
727
+ var RELEASED_PROVIDERS = ["openai"];
728
+ var ALL_PROVIDERS = ["openai", "deepl", "anthropic", "gemini"];
729
+ function experimentalEnabled() {
730
+ return process.env.LANGSYNC_AI_EXPERIMENTAL === "1";
731
+ }
732
+ function availableProviders() {
733
+ return experimentalEnabled() ? [...ALL_PROVIDERS] : [...RELEASED_PROVIDERS];
734
+ }
735
+ function createAdapter(options) {
736
+ if (!availableProviders().includes(options.provider)) {
737
+ if (ALL_PROVIDERS.includes(options.provider)) {
738
+ throw new Error(
739
+ `AI provider "${options.provider}" is not yet available. Currently supported: ${availableProviders().join(", ")}.`
740
+ );
741
+ }
742
+ throw new Error(`Unknown AI provider "${options.provider}".`);
743
+ }
744
+ const { provider, ...rest } = options;
745
+ switch (provider) {
746
+ case "openai":
747
+ return new OpenAIAdapter(rest);
748
+ default:
749
+ throw new Error(`AI provider "${provider}" has no adapter implementation yet.`);
750
+ }
751
+ }
752
+ function isEmpty(value) {
753
+ return value === void 0 || value.trim() === "";
754
+ }
755
+ async function fillEmptyTranslations(options) {
756
+ const referenceFlat = flatten(options.reference);
757
+ const targetFlat = flatten(options.target);
758
+ const translatedKeys = [];
759
+ for (const [key, referenceValue] of Object.entries(referenceFlat)) {
760
+ if (isEmpty(referenceValue)) continue;
761
+ if (!isEmpty(targetFlat[key])) continue;
762
+ targetFlat[key] = await options.adapter.translate({
763
+ text: referenceValue,
764
+ sourceLocale: options.sourceLocale,
765
+ targetLocale: options.targetLocale
766
+ });
767
+ translatedKeys.push(key);
768
+ }
769
+ return { tree: unflatten(targetFlat), translatedKeys };
770
+ }
771
+
772
+ // src/commands/translate/run.ts
773
+ async function runTranslate(options) {
774
+ const loaded = await loadConfig(options.cwd);
775
+ if (!loaded) {
776
+ throw new Error("No LangSync config found. Run `langsync init` first.");
777
+ }
778
+ const { config } = loaded;
779
+ const referenceLocale = config.defaultLocale ?? config.locales[0];
780
+ const provider = options.provider ?? config.ai?.provider ?? "openai";
781
+ const adapter = createAdapter({
782
+ provider,
783
+ apiKey: config.ai?.apiKey,
784
+ model: config.ai?.model
785
+ });
786
+ const files = await loadLocaleFiles({
787
+ cwd: options.cwd,
788
+ inputDir: config.input,
789
+ locales: config.locales
790
+ });
791
+ const reference = files.find((f) => f.locale === referenceLocale);
792
+ if (!reference) {
793
+ throw new Error(`Could not find reference locale file for "${referenceLocale}".`);
794
+ }
795
+ const targets = files.filter((f) => f.locale !== referenceLocale);
796
+ const written = [];
797
+ const planned = [];
798
+ const translatedByLocale = {};
799
+ for (const target of targets) {
800
+ const { tree, translatedKeys } = await fillEmptyTranslations({
801
+ reference: reference.translations,
802
+ target: target.translations,
803
+ sourceLocale: referenceLocale,
804
+ targetLocale: target.locale,
805
+ adapter
806
+ });
807
+ translatedByLocale[target.locale] = translatedKeys;
808
+ if (translatedKeys.length === 0) continue;
809
+ planned.push(target.path);
810
+ if (!options.dryRun) {
811
+ await writeJson(target.path, tree);
812
+ written.push(target.path);
813
+ }
814
+ }
815
+ return { provider, referenceLocale, written, planned, translatedByLocale };
816
+ }
817
+
818
+ // src/commands/translate.ts
819
+ function registerTranslateCommand(program) {
820
+ program.command("translate").description("Fill empty values in non-reference locales using an AI provider.").option("--provider <provider>", "Override the configured AI provider.").option("--dry-run", "Report what would be translated without writing files.", false).action(async (flags) => {
821
+ try {
822
+ const cwd = process.cwd();
823
+ const result = await runTranslate({
824
+ cwd,
825
+ dryRun: flags.dryRun,
826
+ provider: flags.provider
827
+ });
828
+ const totals = Object.values(result.translatedByLocale).reduce(
829
+ (sum, keys) => sum + keys.length,
830
+ 0
831
+ );
832
+ if (totals === 0) {
833
+ logger.info(`Nothing to translate with ${chalk.cyan(result.provider)}.`);
834
+ return;
835
+ }
836
+ const verb = flags.dryRun ? "Would translate" : "Translated";
837
+ for (const [locale, keys] of Object.entries(result.translatedByLocale)) {
838
+ if (keys.length === 0) continue;
839
+ logger.success(`${verb} ${chalk.bold(String(keys.length))} key(s) for ${locale}`);
840
+ }
841
+ if (!flags.dryRun) {
842
+ for (const path of result.written) {
843
+ logger.info(`Wrote ${chalk.bold(relative(cwd, path))}`);
844
+ }
845
+ }
846
+ } catch (error) {
847
+ const message = error instanceof Error ? error.message : String(error);
848
+ logger.error(message);
849
+ process.exitCode = 1;
850
+ }
851
+ });
852
+ }
853
+ async function resolveWatchDir(cwd) {
854
+ const loaded = await loadConfig(cwd);
855
+ if (!loaded) {
856
+ throw new Error("No LangSync config found. Run `langsync init` first.");
857
+ }
858
+ return resolve(cwd, loaded.config.input);
859
+ }
860
+ async function runWatchPass(options) {
861
+ const { referenceLocale, written } = await runSync({
862
+ cwd: options.cwd,
863
+ dryRun: options.dryRun
864
+ });
865
+ const loaded = await loadConfig(options.cwd);
866
+ if (!loaded) {
867
+ throw new Error("No LangSync config found. Run `langsync init` first.");
868
+ }
869
+ const files = await loadLocaleFiles({
870
+ cwd: options.cwd,
871
+ inputDir: loaded.config.input,
872
+ locales: loaded.config.locales
873
+ });
874
+ const issues = validateLocales(files, referenceLocale);
875
+ return { referenceLocale, written, issues };
876
+ }
877
+
878
+ // src/commands/watch.ts
879
+ function reportPass(cwd, written, issueCount) {
880
+ if (written.length === 0) {
881
+ logger.info("No locale changes to sync.");
882
+ } else {
883
+ for (const path of written) {
884
+ logger.success(`Synced ${chalk.bold(relative(cwd, path))}`);
885
+ }
886
+ }
887
+ if (issueCount > 0) {
888
+ logger.warn(`${chalk.yellow(String(issueCount))} validation issue(s) remaining.`);
889
+ } else {
890
+ logger.success("All locales are consistent.");
891
+ }
892
+ }
893
+ function registerWatchCommand(program) {
894
+ program.command("watch").description("Watch locale files and run incremental sync + validation on change.").option("--dry-run", "Report planned changes without writing files.", false).option("--debounce <ms>", "Debounce window for rapid file changes.", "200").action(async (flags) => {
895
+ try {
896
+ const cwd = process.cwd();
897
+ const watchDir = await resolveWatchDir(cwd);
898
+ const debounceMs = Number.parseInt(flags.debounce, 10) || 200;
899
+ const initial = await runWatchPass({ cwd, dryRun: flags.dryRun });
900
+ reportPass(cwd, initial.written, initial.issues.length);
901
+ logger.info(`Watching ${chalk.cyan(relative(cwd, watchDir) || ".")} for changes...`);
902
+ const watcher = chokidar.watch(join(watchDir, "*.json"), {
903
+ ignoreInitial: true
904
+ });
905
+ let timer;
906
+ let running = false;
907
+ const trigger = () => {
908
+ if (timer) clearTimeout(timer);
909
+ timer = setTimeout(() => {
910
+ void (async () => {
911
+ if (running) return;
912
+ running = true;
913
+ try {
914
+ const pass = await runWatchPass({ cwd, dryRun: flags.dryRun });
915
+ reportPass(cwd, pass.written, pass.issues.length);
916
+ } catch (error) {
917
+ logger.error(error instanceof Error ? error.message : String(error));
918
+ } finally {
919
+ running = false;
920
+ }
921
+ })();
922
+ }, debounceMs);
923
+ };
924
+ watcher.on("change", trigger).on("add", trigger).on("unlink", trigger);
925
+ } catch (error) {
926
+ const message = error instanceof Error ? error.message : String(error);
927
+ logger.error(message);
928
+ process.exitCode = 1;
929
+ }
930
+ });
931
+ }
932
+
490
933
  // src/cli.ts
491
- var VERSION = "0.0.0";
934
+ var VERSION = "0.3.0" ;
492
935
  async function main() {
493
936
  assertNodeVersion(22);
494
937
  const program = new Command();
@@ -499,6 +942,8 @@ async function main() {
499
942
  registerSyncCommand(program);
500
943
  registerValidateCommand(program);
501
944
  registerFindMissingCommand(program);
945
+ registerTranslateCommand(program);
946
+ registerWatchCommand(program);
502
947
  registerExportCommand(program);
503
948
  registerImportCommand(program);
504
949
  await program.parseAsync(process.argv);
@@ -508,5 +953,3 @@ main().catch((error) => {
508
953
  logger.error(message);
509
954
  process.exit(1);
510
955
  });
511
- //# sourceMappingURL=cli.js.map
512
- //# sourceMappingURL=cli.js.map
package/dist/index.js CHANGED
@@ -1,4 +1,26 @@
1
1
  #!/usr/bin/env node
2
- export { defineConfig } from '@langsync/shared/config';
3
- //# sourceMappingURL=index.js.map
4
- //# sourceMappingURL=index.js.map
2
+ import 'cosmiconfig';
3
+ import 'cosmiconfig-typescript-loader';
4
+ import { z } from 'zod';
5
+
6
+ z.object({
7
+ input: z.string().describe("Path to the source i18n directory."),
8
+ output: z.string().describe("Path to the output/translations directory."),
9
+ locales: z.array(z.string()).min(1).describe("List of supported locales."),
10
+ defaultLocale: z.string().optional(),
11
+ framework: z.enum(["i18next", "ngx-translate", "react-intl"]).optional().describe("i18n framework integration."),
12
+ excel: z.object({
13
+ file: z.string().default("translations.xlsx"),
14
+ sheetName: z.string().default("Translations")
15
+ }).optional(),
16
+ ai: z.object({
17
+ provider: z.enum(["openai", "deepl", "anthropic", "gemini"]).default("openai").describe("AI translation provider."),
18
+ apiKey: z.string().optional().describe("API key. Falls back to the provider-specific env var."),
19
+ model: z.string().optional().describe("Provider model id (e.g. gpt-4o-mini).")
20
+ }).optional().describe("AI translation settings.")
21
+ });
22
+ function defineConfig(config) {
23
+ return config;
24
+ }
25
+
26
+ export { defineConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariokreitz/langsync",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Modern localization workflow tooling for TypeScript applications.",
5
5
  "keywords": [
6
6
  "i18n",
@@ -10,7 +10,10 @@
10
10
  "cli",
11
11
  "i18next",
12
12
  "ngx-translate",
13
- "react-intl"
13
+ "react-intl",
14
+ "ai-translation",
15
+ "openai",
16
+ "watch-mode"
14
17
  ],
15
18
  "license": "MIT",
16
19
  "author": "Mario Kreitz",
@@ -19,7 +22,7 @@
19
22
  "url": "git+https://github.com/mariokreitz/langsync.git",
20
23
  "directory": "packages/cli"
21
24
  },
22
- "homepage": "https://github.com/mariokreitz/langsync#readme",
25
+ "homepage": "https://docs.langsync.kreitz-webdev.de",
23
26
  "bugs": {
24
27
  "url": "https://github.com/mariokreitz/langsync/issues"
25
28
  },
@@ -49,17 +52,23 @@
49
52
  },
50
53
  "dependencies": {
51
54
  "chalk": "^5.3.0",
55
+ "chokidar": "^4.0.3",
52
56
  "commander": "^12.1.0",
57
+ "cosmiconfig": "^9.0.0",
58
+ "cosmiconfig-typescript-loader": "^6.1.0",
59
+ "exceljs": "^4.4.0",
53
60
  "listr2": "^8.2.5",
54
61
  "ora": "^8.1.1",
55
62
  "prompts": "^2.4.2",
56
- "@langsync/shared": "0.1.0",
57
- "@langsync/excel-engine": "0.1.0",
58
- "@langsync/core": "0.1.0"
63
+ "zod": "^3.24.1"
59
64
  },
60
65
  "devDependencies": {
61
66
  "@types/prompts": "^2.4.9",
62
- "memfs": "^4.15.1"
67
+ "memfs": "^4.57.3",
68
+ "@langsync/shared": "0.1.0",
69
+ "@langsync/core": "0.1.0",
70
+ "@langsync/excel-engine": "0.1.0",
71
+ "@langsync/ai-engine": "0.1.0"
63
72
  },
64
73
  "scripts": {
65
74
  "build": "tsup",
package/dist/cli.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/ui/banner.ts","../src/ui/node-version.ts","../src/commands/init/detect-framework.ts","../src/commands/init/prompt.ts","../src/commands/init/write-config.ts","../src/commands/init.ts","../src/commands/sync/run.ts","../src/commands/sync.ts","../src/commands/validate/run.ts","../src/commands/validate.ts","../src/commands/find-missing/run.ts","../src/commands/find-missing.ts","../src/commands/export/run.ts","../src/commands/export.ts","../src/commands/import/run.ts","../src/commands/import.ts","../src/cli.ts"],"names":["chalk","join","logger","relative","loadConfig","loadLocaleFiles","resolve","DEFAULT_FILE","DEFAULT_SHEET","writeJson"],"mappings":"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","file":"cli.js","sourcesContent":["import chalk from 'chalk';\n\nexport function printBanner(version: string): void {\n const title = chalk.bold.cyan('LangSync');\n const tagline = chalk.gray('Modern localization workflow tooling.');\n const ver = chalk.dim(`v${version}`);\n console.log(`\\n ${title} ${ver}\\n ${tagline}\\n`);\n}\n","import chalk from 'chalk';\n\nexport function assertNodeVersion(minMajor: number): void {\n const current = process.versions.node;\n const major = Number.parseInt(current.split('.')[0] ?? '0', 10);\n if (major < minMajor) {\n console.error(\n chalk.red(\n `langsync requires Node.js >= ${String(minMajor)}. ` +\n `You are running ${current}. Please upgrade.`,\n ),\n );\n process.exit(1);\n }\n}\n","import { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { type I18nFramework } from '@langsync/shared/types';\n\ninterface PackageJsonLike {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n}\n\nconst FRAMEWORK_SIGNATURES: readonly {\n framework: I18nFramework;\n packages: readonly string[];\n}[] = [\n { framework: 'i18next', packages: ['i18next', 'react-i18next', 'vue-i18next'] },\n { framework: 'ngx-translate', packages: ['@ngx-translate/core', '@ngx-translate/http-loader'] },\n { framework: 'react-intl', packages: ['react-intl', '@formatjs/intl'] },\n];\n\n/**\n * Detect the i18n framework used by the project at `cwd` by inspecting its `package.json`.\n * Returns `null` when no known framework dependency is present.\n */\nexport async function detectFramework(cwd: string): Promise<I18nFramework | null> {\n const pkgPath = join(cwd, 'package.json');\n let pkg: PackageJsonLike;\n try {\n pkg = JSON.parse(await readFile(pkgPath, 'utf-8')) as PackageJsonLike;\n } catch {\n return null;\n }\n\n const allDeps = {\n ...pkg.dependencies,\n ...pkg.devDependencies,\n ...pkg.peerDependencies,\n };\n\n for (const { framework, packages } of FRAMEWORK_SIGNATURES) {\n if (packages.some((p) => p in allDeps)) return framework;\n }\n return null;\n}\n","import prompts from 'prompts';\nimport { type I18nFramework } from '@langsync/shared/types';\n\nexport interface InitAnswers {\n input: string;\n output: string;\n locales: string[];\n defaultLocale: string;\n framework: I18nFramework | 'none';\n}\n\nexport interface InitPromptOptions {\n detectedFramework: I18nFramework | null;\n /** When true, skip prompts and use defaults (with detected framework). */\n yes?: boolean;\n}\n\nconst FRAMEWORK_CHOICES = [\n { title: 'i18next', value: 'i18next' as const },\n { title: 'ngx-translate (Angular)', value: 'ngx-translate' as const },\n { title: 'react-intl', value: 'react-intl' as const },\n { title: 'None / Custom', value: 'none' as const },\n];\n\nconst DEFAULTS = {\n input: './src/i18n',\n output: './translations',\n locales: 'en,de',\n defaultLocale: 'en',\n};\n\nexport async function runInitPrompts(options: InitPromptOptions): Promise<InitAnswers> {\n const framework: InitAnswers['framework'] = options.detectedFramework ?? 'none';\n\n if (options.yes) {\n return {\n input: DEFAULTS.input,\n output: DEFAULTS.output,\n locales: DEFAULTS.locales.split(',').map((l) => l.trim()),\n defaultLocale: DEFAULTS.defaultLocale,\n framework,\n };\n }\n\n const response = await prompts(\n [\n {\n type: 'text',\n name: 'input',\n message: 'Where are your source i18n files?',\n initial: DEFAULTS.input,\n },\n {\n type: 'text',\n name: 'output',\n message: 'Where should translation output go?',\n initial: DEFAULTS.output,\n },\n {\n type: 'text',\n name: 'locales',\n message: 'Which locales? (comma-separated)',\n initial: DEFAULTS.locales,\n format: (value: string) =>\n value\n .split(',')\n .map((l) => l.trim())\n .filter(Boolean),\n validate: (value: string) => {\n const list = value\n .split(',')\n .map((l) => l.trim())\n .filter(Boolean);\n return list.length > 0 ? true : 'Please provide at least one locale.';\n },\n },\n {\n type: 'text',\n name: 'defaultLocale',\n message: 'Default (reference) locale?',\n initial: DEFAULTS.defaultLocale,\n },\n {\n type: 'select',\n name: 'framework',\n message: 'Which i18n framework do you use?',\n choices: FRAMEWORK_CHOICES,\n initial: FRAMEWORK_CHOICES.findIndex((c) => c.value === framework),\n },\n ],\n {\n onCancel: () => {\n throw new Error('Aborted by user.');\n },\n },\n );\n\n // prompts() returns a typed-erased Answers record; trust our shape.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return response as InitAnswers;\n}\n","import { mkdir, writeFile, access } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { type InitAnswers } from './prompt.js';\n\nexport type ConfigFormat = 'ts' | 'json';\n\nexport interface WriteConfigOptions {\n cwd: string;\n answers: InitAnswers;\n format: ConfigFormat;\n force: boolean;\n}\n\nexport interface WriteConfigResult {\n configPath: string;\n createdLocaleFiles: string[];\n}\n\nconst CANDIDATE_CONFIG_FILES = [\n 'langsync.config.ts',\n 'langsync.config.js',\n 'langsync.config.mjs',\n 'langsync.config.json',\n];\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await access(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function findExistingConfig(cwd: string): Promise<string | null> {\n for (const file of CANDIDATE_CONFIG_FILES) {\n const full = join(cwd, file);\n if (await pathExists(full)) return full;\n }\n return null;\n}\n\nfunction renderTsConfig(answers: InitAnswers): string {\n const frameworkLine =\n answers.framework === 'none' ? '' : ` framework: '${answers.framework}',\\n`;\n const localesArr = answers.locales.map((l) => `'${l}'`).join(', ');\n return `import { defineConfig } from '@mariokreitz/langsync';\n\nexport default defineConfig({\n input: '${answers.input}',\n output: '${answers.output}',\n locales: [${localesArr}],\n defaultLocale: '${answers.defaultLocale}',\n${frameworkLine}});\n`;\n}\n\nfunction renderJsonConfig(answers: InitAnswers): string {\n const obj: Record<string, unknown> = {\n input: answers.input,\n output: answers.output,\n locales: answers.locales,\n defaultLocale: answers.defaultLocale,\n };\n if (answers.framework !== 'none') obj.framework = answers.framework;\n return JSON.stringify(obj, null, 2) + '\\n';\n}\n\n/**\n * Write the LangSync config file and scaffold empty locale JSON stubs.\n * Refuses to overwrite an existing config unless `force` is set.\n */\nexport async function writeConfig(options: WriteConfigOptions): Promise<WriteConfigResult> {\n const { cwd, answers, format, force } = options;\n\n const existing = await findExistingConfig(cwd);\n if (existing && !force) {\n throw new Error(\n `A LangSync config already exists at ${existing}. Re-run with --force to overwrite.`,\n );\n }\n\n const configFilename = format === 'ts' ? 'langsync.config.ts' : 'langsync.config.json';\n const configPath = resolve(cwd, configFilename);\n const content = format === 'ts' ? renderTsConfig(answers) : renderJsonConfig(answers);\n\n await mkdir(dirname(configPath), { recursive: true });\n await writeFile(configPath, content, 'utf-8');\n\n // Scaffold empty locale stubs in the input directory.\n const inputDir = resolve(cwd, answers.input);\n await mkdir(inputDir, { recursive: true });\n const createdLocaleFiles: string[] = [];\n for (const locale of answers.locales) {\n const localePath = join(inputDir, `${locale}.json`);\n if (!(await pathExists(localePath))) {\n await writeFile(localePath, '{}\\n', 'utf-8');\n createdLocaleFiles.push(localePath);\n }\n }\n\n return { configPath, createdLocaleFiles };\n}\n","import { type Command } from 'commander';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { relative } from 'node:path';\nimport { logger } from '@langsync/shared/logger';\nimport { detectFramework } from './init/detect-framework.js';\nimport { runInitPrompts } from './init/prompt.js';\nimport { writeConfig, type ConfigFormat } from './init/write-config.js';\n\ninterface InitFlags {\n force: boolean;\n json: boolean;\n yes: boolean;\n}\n\nexport function registerInitCommand(program: Command): void {\n program\n .command('init')\n .description('Initialize a new LangSync configuration in the current project.')\n .option('--force', 'Overwrite an existing config file without asking.', false)\n .option('--json', 'Emit langsync.config.json instead of langsync.config.ts.', false)\n .option('-y, --yes', 'Accept all defaults; skip prompts.', false)\n .action(async (flags: InitFlags) => {\n const cwd = process.cwd();\n const format: ConfigFormat = flags.json ? 'json' : 'ts';\n\n try {\n const detectSpinner = ora('Detecting i18n framework…').start();\n const detectedFramework = await detectFramework(cwd);\n detectSpinner.succeed(\n detectedFramework\n ? `Detected ${chalk.cyan(detectedFramework)}`\n : 'No known framework detected',\n );\n\n // prompts owns stdio — must NOT be wrapped in a spinner.\n const answers = await runInitPrompts({\n detectedFramework,\n yes: flags.yes,\n });\n\n const writeSpinner = ora('Writing config and scaffolding locale files…').start();\n const result = await writeConfig({\n cwd,\n answers,\n format,\n force: flags.force,\n });\n writeSpinner.succeed('Files written');\n\n console.log();\n logger.success(`Created ${chalk.bold(relative(cwd, result.configPath))}`);\n for (const localeFile of result.createdLocaleFiles) {\n logger.success(`Created ${chalk.bold(relative(cwd, localeFile))}`);\n }\n console.log();\n logger.info(`Next: run ${chalk.bold('langsync validate')} to check your locales.`);\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n logger.error(message);\n process.exitCode = 1;\n }\n });\n}\n","import { loadConfig } from '@langsync/shared/config';\nimport { loadLocaleFiles, writeJson } from '@langsync/shared/fs';\nimport { syncTrees } from '@langsync/core';\n\nexport interface RunSyncOptions {\n cwd: string;\n dryRun?: boolean;\n}\n\nexport interface RunSyncResult {\n referenceLocale: string;\n written: string[];\n planned: string[];\n}\n\n/**\n * Synchronize keys from the reference locale into every other locale,\n * preserving existing translations and adding empty placeholders for new keys.\n */\nexport async function runSync(options: RunSyncOptions): Promise<RunSyncResult> {\n const loaded = await loadConfig(options.cwd);\n if (!loaded) {\n throw new Error('No LangSync config found. Run `langsync init` first.');\n }\n const { config } = loaded;\n const referenceLocale = config.defaultLocale ?? config.locales[0]!;\n\n const files = await loadLocaleFiles({\n cwd: options.cwd,\n inputDir: config.input,\n locales: config.locales,\n });\n\n const reference = files.find((f) => f.locale === referenceLocale);\n if (!reference) {\n throw new Error(`Could not find reference locale file for \"${referenceLocale}\".`);\n }\n\n const targets = files.filter((f) => f.locale !== referenceLocale);\n const planned: string[] = [];\n const written: string[] = [];\n\n for (const target of targets) {\n const merged = syncTrees(reference.translations, target.translations);\n planned.push(target.path);\n if (!options.dryRun) {\n await writeJson(target.path, merged);\n written.push(target.path);\n }\n }\n\n return { referenceLocale, written, planned };\n}\n","import { type Command } from 'commander';\nimport chalk from 'chalk';\nimport { relative } from 'node:path';\nimport { logger } from '@langsync/shared/logger';\nimport { runSync } from './sync/run.js';\n\ninterface SyncFlags {\n dryRun: boolean;\n}\n\nexport function registerSyncCommand(program: Command): void {\n program\n .command('sync')\n .description('Synchronize translation keys across all configured locales.')\n .option('--dry-run', 'Report planned changes without writing files.', false)\n .action(async (flags: SyncFlags) => {\n try {\n const cwd = process.cwd();\n const { written, planned, referenceLocale } = await runSync({\n cwd,\n dryRun: flags.dryRun,\n });\n\n const targets = flags.dryRun ? planned : written;\n if (targets.length === 0) {\n logger.info(`Nothing to sync against ${chalk.cyan(referenceLocale)}.`);\n } else {\n const verb = flags.dryRun ? 'Would update' : 'Updated';\n for (const path of targets) {\n logger.success(`${verb} ${chalk.bold(relative(cwd, path))}`);\n }\n }\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n logger.error(message);\n process.exitCode = 1;\n }\n });\n}\n","import { loadConfig } from '@langsync/shared/config';\nimport { loadLocaleFiles } from '@langsync/shared/fs';\nimport { validateLocales, type ValidationIssue } from '@langsync/core';\n\nexport interface RunValidateOptions {\n cwd: string;\n}\n\nexport interface RunValidateResult {\n referenceLocale: string;\n issues: ValidationIssue[];\n exitCode: 0 | 1;\n}\n\n/**\n * Validate the locale files configured in the LangSync config at `cwd`.\n *\n * - `missing` and `extra` issues are treated as errors (exit code 1).\n * - `empty` issues are treated as warnings only (exit code 0).\n */\nexport async function runValidate(options: RunValidateOptions): Promise<RunValidateResult> {\n const loaded = await loadConfig(options.cwd);\n if (!loaded) {\n throw new Error('No LangSync config found. Run `langsync init` first.');\n }\n\n const { config } = loaded;\n const referenceLocale = config.defaultLocale ?? config.locales[0]!;\n\n const files = await loadLocaleFiles({\n cwd: options.cwd,\n inputDir: config.input,\n locales: config.locales,\n });\n\n const issues = validateLocales(files, referenceLocale);\n const hasErrors = issues.some((i) => i.type === 'missing' || i.type === 'extra');\n\n return {\n referenceLocale,\n issues,\n exitCode: hasErrors ? 1 : 0,\n };\n}\n","import { type Command } from 'commander';\nimport chalk from 'chalk';\nimport { logger } from '@langsync/shared/logger';\nimport { runValidate } from './validate/run.js';\n\ninterface ValidateFlags {\n reporter: 'pretty' | 'json';\n}\n\nexport function registerValidateCommand(program: Command): void {\n program\n .command('validate')\n .description('Validate locale consistency, structure and missing keys.')\n .option('--reporter <kind>', 'Output format: pretty | json.', 'pretty')\n .action(async (flags: ValidateFlags) => {\n try {\n const { issues, exitCode, referenceLocale } = await runValidate({ cwd: process.cwd() });\n\n if (flags.reporter === 'json') {\n console.log(JSON.stringify({ referenceLocale, issues }, null, 2));\n } else {\n if (issues.length === 0) {\n logger.success(`All locales are consistent with ${chalk.cyan(referenceLocale)}.`);\n } else {\n const byType = { missing: 0, extra: 0, empty: 0 };\n for (const issue of issues) {\n byType[issue.type]++;\n const colored =\n issue.type === 'empty' ? chalk.yellow(issue.type) : chalk.red(issue.type);\n logger.info(`${colored} ${chalk.cyan(issue.locale)} ${issue.key}`);\n }\n console.log();\n logger.info(\n `Summary: ${byType.missing} missing, ${byType.extra} extra, ${byType.empty} empty`,\n );\n }\n }\n\n process.exitCode = exitCode;\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n logger.error(message);\n process.exitCode = 1;\n }\n });\n}\n","import { runValidate } from '../validate/run.js';\n\nexport interface RunFindMissingOptions {\n cwd: string;\n}\n\nexport interface RunFindMissingResult {\n referenceLocale: string;\n missingByLocale: Record<string, string[]>;\n exitCode: 0 | 1;\n}\n\n/**\n * Report missing translation keys per locale, relative to the reference locale.\n */\nexport async function runFindMissing(\n options: RunFindMissingOptions,\n): Promise<RunFindMissingResult> {\n const { issues, referenceLocale } = await runValidate({ cwd: options.cwd });\n\n const missingByLocale: Record<string, string[]> = {};\n for (const issue of issues) {\n if (issue.type !== 'missing') continue;\n (missingByLocale[issue.locale] ??= []).push(issue.key);\n }\n for (const locale of Object.keys(missingByLocale)) {\n missingByLocale[locale]!.sort();\n }\n\n const exitCode: 0 | 1 = Object.keys(missingByLocale).length === 0 ? 0 : 1;\n return { referenceLocale, missingByLocale, exitCode };\n}\n","import { type Command } from 'commander';\nimport chalk from 'chalk';\nimport { logger } from '@langsync/shared/logger';\nimport { runFindMissing } from './find-missing/run.js';\n\ninterface FindMissingFlags {\n reporter: 'pretty' | 'json';\n}\n\nexport function registerFindMissingCommand(program: Command): void {\n program\n .command('find-missing')\n .description('Find missing translation keys across locales.')\n .option('--reporter <kind>', 'Output format: pretty | json.', 'pretty')\n .action(async (flags: FindMissingFlags) => {\n try {\n const { referenceLocale, missingByLocale, exitCode } = await runFindMissing({\n cwd: process.cwd(),\n });\n\n if (flags.reporter === 'json') {\n console.log(JSON.stringify({ referenceLocale, missingByLocale }, null, 2));\n } else if (Object.keys(missingByLocale).length === 0) {\n logger.success(`No missing keys relative to ${chalk.cyan(referenceLocale)}.`);\n } else {\n for (const [locale, keys] of Object.entries(missingByLocale)) {\n logger.warn(`${chalk.cyan(locale)} is missing ${keys.length} key(s):`);\n for (const key of keys) console.log(` - ${key}`);\n }\n }\n\n process.exitCode = exitCode;\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n logger.error(message);\n process.exitCode = 1;\n }\n });\n}\n","import { resolve } from 'node:path';\nimport { loadConfig } from '@langsync/shared/config';\nimport { loadLocaleFiles } from '@langsync/shared/fs';\nimport { exportToExcel } from '@langsync/excel-engine';\n\nexport interface RunExportExcelOptions {\n cwd: string;\n /** Override the excel file path (relative to cwd). */\n file?: string;\n /** Override the worksheet name. */\n sheetName?: string;\n}\n\nexport interface RunExportExcelResult {\n file: string;\n sheetName: string;\n locales: string[];\n}\n\nconst DEFAULT_FILE = 'translations.xlsx';\nconst DEFAULT_SHEET = 'Translations';\n\n/**\n * Export all configured locales into a single Excel workbook.\n */\nexport async function runExportExcel(\n options: RunExportExcelOptions,\n): Promise<RunExportExcelResult> {\n const loaded = await loadConfig(options.cwd);\n if (!loaded) {\n throw new Error('No LangSync config found. Run `langsync init` first.');\n }\n const { config } = loaded;\n\n const file = resolve(options.cwd, options.file ?? config.excel?.file ?? DEFAULT_FILE);\n const sheetName = options.sheetName ?? config.excel?.sheetName ?? DEFAULT_SHEET;\n\n const files = await loadLocaleFiles({\n cwd: options.cwd,\n inputDir: config.input,\n locales: config.locales,\n });\n\n await exportToExcel({\n file,\n sheetName,\n locales: files.map((f) => ({ locale: f.locale, translations: f.translations })),\n });\n\n return { file, sheetName, locales: files.map((f) => f.locale) };\n}\n","import { type Command } from 'commander';\nimport chalk from 'chalk';\nimport { relative } from 'node:path';\nimport { logger } from '@langsync/shared/logger';\nimport { runExportExcel } from './export/run.js';\n\ninterface ExportExcelFlags {\n file?: string;\n sheet?: string;\n}\n\nexport function registerExportCommand(program: Command): void {\n const cmd = program.command('export').description('Export translations to external formats.');\n\n cmd\n .command('excel')\n .description('Export translations to an Excel workbook.')\n .option('--file <path>', 'Output Excel file (overrides config).')\n .option('--sheet <name>', 'Worksheet name (overrides config).')\n .action(async (flags: ExportExcelFlags) => {\n try {\n const cwd = process.cwd();\n const { file, locales } = await runExportExcel({\n cwd,\n file: flags.file,\n sheetName: flags.sheet,\n });\n logger.success(\n `Exported ${chalk.cyan(String(locales.length))} locale(s) to ${chalk.bold(\n relative(cwd, file),\n )}`,\n );\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n logger.error(message);\n process.exitCode = 1;\n }\n });\n}\n","import { join, resolve } from 'node:path';\nimport { loadConfig } from '@langsync/shared/config';\nimport { writeJson } from '@langsync/shared/fs';\nimport { importFromExcel } from '@langsync/excel-engine';\n\nexport interface RunImportExcelOptions {\n cwd: string;\n file?: string;\n sheetName?: string;\n dryRun?: boolean;\n}\n\nexport interface RunImportExcelResult {\n file: string;\n sheetName: string;\n written: string[];\n planned: string[];\n skipped: string[];\n}\n\nconst DEFAULT_FILE = 'translations.xlsx';\nconst DEFAULT_SHEET = 'Translations';\n\n/**\n * Import locales from an Excel workbook and write them back to the configured\n * input directory as JSON files. Locales not listed in the config are skipped.\n */\nexport async function runImportExcel(\n options: RunImportExcelOptions,\n): Promise<RunImportExcelResult> {\n const loaded = await loadConfig(options.cwd);\n if (!loaded) {\n throw new Error('No LangSync config found. Run `langsync init` first.');\n }\n const { config } = loaded;\n\n const file = resolve(options.cwd, options.file ?? config.excel?.file ?? DEFAULT_FILE);\n const sheetName = options.sheetName ?? config.excel?.sheetName ?? DEFAULT_SHEET;\n const inputAbs = resolve(options.cwd, config.input);\n const configuredLocales = new Set(config.locales);\n\n const result = await importFromExcel(file, sheetName);\n\n const planned: string[] = [];\n const written: string[] = [];\n const skipped: string[] = [];\n\n for (const entry of result.locales) {\n if (!configuredLocales.has(entry.locale)) {\n skipped.push(entry.locale);\n continue;\n }\n const path = join(inputAbs, `${entry.locale}.json`);\n planned.push(path);\n if (!options.dryRun) {\n await writeJson(path, entry.translations);\n written.push(path);\n }\n }\n\n return { file, sheetName, written, planned, skipped };\n}\n","import { type Command } from 'commander';\nimport chalk from 'chalk';\nimport { relative } from 'node:path';\nimport { logger } from '@langsync/shared/logger';\nimport { runImportExcel } from './import/run.js';\n\ninterface ImportExcelFlags {\n file?: string;\n sheet?: string;\n dryRun: boolean;\n}\n\nexport function registerImportCommand(program: Command): void {\n const cmd = program.command('import').description('Import translations from external formats.');\n\n cmd\n .command('excel')\n .description('Import translations from an Excel workbook.')\n .option('--file <path>', 'Input Excel file (overrides config).')\n .option('--sheet <name>', 'Worksheet name (overrides config).')\n .option('--dry-run', 'Report planned writes without touching disk.', false)\n .action(async (flags: ImportExcelFlags) => {\n try {\n const cwd = process.cwd();\n const { written, planned, skipped } = await runImportExcel({\n cwd,\n file: flags.file,\n sheetName: flags.sheet,\n dryRun: flags.dryRun,\n });\n\n const targets = flags.dryRun ? planned : written;\n const verb = flags.dryRun ? 'Would write' : 'Wrote';\n for (const path of targets) {\n logger.success(`${verb} ${chalk.bold(relative(cwd, path))}`);\n }\n for (const locale of skipped) {\n logger.warn(`Skipped ${chalk.cyan(locale)} (not in configured locales).`);\n }\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n logger.error(message);\n process.exitCode = 1;\n }\n });\n}\n","import { Command } from 'commander';\nimport { logger } from '@langsync/shared/logger';\nimport { printBanner } from './ui/banner.js';\nimport { assertNodeVersion } from './ui/node-version.js';\nimport { registerInitCommand } from './commands/init.js';\nimport { registerSyncCommand } from './commands/sync.js';\nimport { registerValidateCommand } from './commands/validate.js';\nimport { registerFindMissingCommand } from './commands/find-missing.js';\nimport { registerExportCommand } from './commands/export.js';\nimport { registerImportCommand } from './commands/import.js';\n\nconst VERSION = '0.0.0';\n\nasync function main(): Promise<void> {\n assertNodeVersion(22);\n\n const program = new Command();\n\n program\n .name('langsync')\n .description('Modern localization workflow tooling for TypeScript applications.')\n .version(VERSION, '-v, --version', 'Output the current version.')\n .hook('preAction', () => {\n printBanner(VERSION);\n });\n\n registerInitCommand(program);\n registerSyncCommand(program);\n registerValidateCommand(program);\n registerFindMissingCommand(program);\n registerExportCommand(program);\n registerImportCommand(program);\n\n await program.parseAsync(process.argv);\n}\n\nmain().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n logger.error(message);\n process.exit(1);\n});\n"]}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js","sourcesContent":[]}