@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 +25 -5
- package/dist/cli.js +472 -29
- package/dist/index.js +25 -3
- package/package.json +16 -7
- package/dist/cli.js.map +0 -1
- package/dist/index.js.map +0 -1
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
|
|
24
|
-
|
|
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.
|
|
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
|
-
#
|
|
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
|
|
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 {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
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 =
|
|
17
|
-
const tagline =
|
|
18
|
-
const ver =
|
|
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
|
-
|
|
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 ${
|
|
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 ${
|
|
235
|
+
logger.success(`Created ${chalk.bold(relative(cwd, result.configPath))}`);
|
|
225
236
|
for (const localeFile of result.createdLocaleFiles) {
|
|
226
|
-
logger.success(`Created ${
|
|
237
|
+
logger.success(`Created ${chalk.bold(relative(cwd, localeFile))}`);
|
|
227
238
|
}
|
|
228
239
|
console.log();
|
|
229
|
-
logger.info(`Next: run ${
|
|
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 ${
|
|
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} ${
|
|
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 ${
|
|
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" ?
|
|
328
|
-
logger.info(`${colored} ${
|
|
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 ${
|
|
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(`${
|
|
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 ${
|
|
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} ${
|
|
665
|
+
logger.success(`${verb} ${chalk.bold(relative(cwd, path))}`);
|
|
478
666
|
}
|
|
479
667
|
for (const locale of skipped) {
|
|
480
|
-
logger.warn(`Skipped ${
|
|
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.
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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.
|
|
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://
|
|
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
|
-
"
|
|
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.
|
|
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":[]}
|