solid-translate 1.1.0 → 1.2.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/dist/cli.js CHANGED
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
2
+ import {
3
+ translateBatch
4
+ } from "./chunk-2BKJUY37.js";
3
5
  import "./chunk-FYS2JH42.js";
4
6
 
5
7
  // src/cli.ts
6
8
  import {
7
- readFileSync,
8
- writeFileSync,
9
- existsSync,
9
+ readFileSync as readFileSync2,
10
+ writeFileSync as writeFileSync2,
11
+ existsSync as existsSync2,
10
12
  mkdirSync
11
13
  } from "fs";
12
- import { resolve, join, dirname, relative, basename } from "path";
14
+ import { resolve, join as join2, dirname, relative, basename } from "path";
13
15
 
14
16
  // src/hash.ts
15
17
  import { createHash } from "crypto";
@@ -17,77 +19,6 @@ function hashContent(content) {
17
19
  return createHash("sha256").update(content).digest("hex").slice(0, 16);
18
20
  }
19
21
 
20
- // src/translate.ts
21
- import { generateObject } from "ai";
22
- import { z } from "zod";
23
- async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts) {
24
- const keys = Object.keys(entries);
25
- if (keys.length === 0) return {};
26
- const defaultSystem = [
27
- `You are a professional translator specializing in software localization.`,
28
- `Translate text from "${sourceLocale}" to "${targetLocale}".`,
29
- `Rules:`,
30
- `- Preserve the original tone and meaning`,
31
- `- Keep placeholders like {{variable}}, {variable}, {0}, {1} unchanged`,
32
- `- Keep HTML tags unchanged`,
33
- `- Do not add or remove content`,
34
- `- Return natural, idiomatic translations`
35
- ].join("\n");
36
- let contextSection = "";
37
- if (contexts && Object.keys(contexts).length > 0) {
38
- const contextLines = Object.entries(contexts).filter(([key]) => key in entries).map(([key, ctx]) => ` "${key}": ${ctx}`);
39
- if (contextLines.length > 0) {
40
- contextSection = [
41
- ``,
42
- `Context hints for disambiguation:`,
43
- ...contextLines,
44
- ``
45
- ].join("\n");
46
- }
47
- }
48
- const { object } = await generateObject({
49
- model,
50
- schema: z.object({
51
- translations: z.record(z.string(), z.string())
52
- }),
53
- system: systemPrompt || defaultSystem,
54
- prompt: [
55
- `Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
56
- `Return a JSON object with the exact same keys and the translated values.`,
57
- contextSection,
58
- JSON.stringify(entries, null, 2)
59
- ].join("\n")
60
- });
61
- return object.translations;
62
- }
63
- async function translateMarkdown(model, content, targetLocale, sourceLocale, systemPrompt) {
64
- const defaultSystem = [
65
- `You are a professional translator specializing in documentation.`,
66
- `Translate Markdown/MDX content from "${sourceLocale}" to "${targetLocale}".`,
67
- `Rules:`,
68
- `- Preserve all Markdown formatting (headers, lists, bold, italic, links, etc.)`,
69
- `- Preserve code blocks and inline code unchanged`,
70
- `- Preserve frontmatter YAML keys (only translate values)`,
71
- `- Preserve MDX component syntax and JSX expressions`,
72
- `- Preserve URLs and file paths unchanged`,
73
- `- Return natural, idiomatic translations`
74
- ].join("\n");
75
- const { object } = await generateObject({
76
- model,
77
- schema: z.object({
78
- translated: z.string()
79
- }),
80
- system: systemPrompt || defaultSystem,
81
- prompt: [
82
- `Translate this Markdown/MDX content from "${sourceLocale}" to "${targetLocale}".`,
83
- `Return the complete translated document.`,
84
- ``,
85
- content
86
- ].join("\n")
87
- });
88
- return object.translated;
89
- }
90
-
91
22
  // src/extract.ts
92
23
  function extractStringsFromSource(code, filePath) {
93
24
  const results = [];
@@ -127,6 +58,161 @@ function extractStringsFromSource(code, filePath) {
127
58
  return results;
128
59
  }
129
60
 
61
+ // src/lock.ts
62
+ import { readFileSync, writeFileSync, existsSync } from "fs";
63
+ import { join } from "path";
64
+ function diffLock(sourceDict, lock, contexts) {
65
+ const changedKeys = {};
66
+ const pendingEntries = {};
67
+ for (const [key, value] of Object.entries(sourceDict)) {
68
+ const hash = hashContent(value);
69
+ const existing = lock.keys[key];
70
+ const newContext = contexts ? contexts[key] : existing?.context;
71
+ const contextChanged = contexts !== void 0 && existing?.context !== contexts[key];
72
+ if (!existing || existing.hash !== hash || contextChanged) {
73
+ changedKeys[key] = value;
74
+ pendingEntries[key] = { hash, source: value, context: newContext };
75
+ }
76
+ }
77
+ const deletedKeys = Object.keys(lock.keys).filter(
78
+ (key) => !(key in sourceDict)
79
+ );
80
+ return { changedKeys, pendingEntries, deletedKeys };
81
+ }
82
+ async function syncLocaleFiles(options) {
83
+ const {
84
+ localesDir,
85
+ sourceLocale,
86
+ targetLocales,
87
+ batchSize,
88
+ translate,
89
+ contexts,
90
+ log = () => {
91
+ }
92
+ } = options;
93
+ const sourceFilePath = join(localesDir, `${sourceLocale}.json`);
94
+ if (!existsSync(sourceFilePath)) {
95
+ return {
96
+ status: "no-source",
97
+ translatedKeys: [],
98
+ deletedKeys: [],
99
+ failures: []
100
+ };
101
+ }
102
+ const sourceDict = JSON.parse(
103
+ readFileSync(sourceFilePath, "utf-8")
104
+ );
105
+ const lockFilePath = join(localesDir, ".solid-translate.lock");
106
+ let lock = { version: 1, sourceLocale, keys: {} };
107
+ if (existsSync(lockFilePath)) {
108
+ try {
109
+ lock = JSON.parse(readFileSync(lockFilePath, "utf-8"));
110
+ } catch {
111
+ }
112
+ }
113
+ const { changedKeys, pendingEntries, deletedKeys } = diffLock(
114
+ sourceDict,
115
+ lock,
116
+ contexts
117
+ );
118
+ for (const key of deletedKeys) {
119
+ delete lock.keys[key];
120
+ }
121
+ const changedCount = Object.keys(changedKeys).length;
122
+ if (changedCount === 0 && deletedKeys.length === 0) {
123
+ log("No changes detected in locale files.");
124
+ return {
125
+ status: "no-changes",
126
+ translatedKeys: [],
127
+ deletedKeys: [],
128
+ failures: []
129
+ };
130
+ }
131
+ if (changedCount === 0) {
132
+ for (const targetLocale of targetLocales) {
133
+ const targetFilePath = join(localesDir, `${targetLocale}.json`);
134
+ const existing = readTargetFile(targetFilePath);
135
+ writeTargetFile(targetFilePath, existing, sourceDict);
136
+ log(` ${targetLocale}: pruned deleted keys`);
137
+ }
138
+ writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
139
+ log(
140
+ `Removed ${deletedKeys.length} deleted key${deletedKeys.length > 1 ? "s" : ""} from target locales.`
141
+ );
142
+ return { status: "synced", translatedKeys: [], deletedKeys, failures: [] };
143
+ }
144
+ log(
145
+ `Translating ${changedCount} key${changedCount > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
146
+ );
147
+ const changedContexts = {};
148
+ for (const key of Object.keys(changedKeys)) {
149
+ const ctx = pendingEntries[key]?.context;
150
+ if (ctx) changedContexts[key] = ctx;
151
+ }
152
+ const failures = [];
153
+ const failedKeys = /* @__PURE__ */ new Set();
154
+ for (const targetLocale of targetLocales) {
155
+ const targetFilePath = join(localesDir, `${targetLocale}.json`);
156
+ const existing = readTargetFile(targetFilePath);
157
+ const entries = Object.entries(changedKeys);
158
+ for (let i = 0; i < entries.length; i += batchSize) {
159
+ const batch = Object.fromEntries(entries.slice(i, i + batchSize));
160
+ try {
161
+ const translated = await translate(
162
+ batch,
163
+ targetLocale,
164
+ changedContexts
165
+ );
166
+ Object.assign(existing, translated);
167
+ } catch (err) {
168
+ failures.push({
169
+ locale: targetLocale,
170
+ keys: Object.keys(batch),
171
+ error: err
172
+ });
173
+ for (const key of Object.keys(batch)) {
174
+ failedKeys.add(key);
175
+ }
176
+ }
177
+ }
178
+ writeTargetFile(targetFilePath, existing, sourceDict);
179
+ log(` ${targetLocale}: ${Object.keys(existing).length} keys`);
180
+ }
181
+ const translatedKeys = [];
182
+ for (const [key, entry] of Object.entries(pendingEntries)) {
183
+ if (failedKeys.has(key)) continue;
184
+ lock.keys[key] = entry;
185
+ translatedKeys.push(key);
186
+ }
187
+ writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
188
+ return { status: "synced", translatedKeys, deletedKeys, failures };
189
+ }
190
+ function formatSyncFailures(failures) {
191
+ return failures.map((failure) => {
192
+ const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
193
+ return `${failure.locale}: ${failure.keys.length} key${failure.keys.length > 1 ? "s" : ""} [${failure.keys.join(", ")}] \u2014 ${message}`;
194
+ });
195
+ }
196
+ function readTargetFile(targetFilePath) {
197
+ if (!existsSync(targetFilePath)) return {};
198
+ try {
199
+ return JSON.parse(readFileSync(targetFilePath, "utf-8"));
200
+ } catch {
201
+ return {};
202
+ }
203
+ }
204
+ function writeTargetFile(targetFilePath, translations, sourceDict) {
205
+ for (const key of Object.keys(translations)) {
206
+ if (!(key in sourceDict)) {
207
+ delete translations[key];
208
+ }
209
+ }
210
+ const sorted = Object.fromEntries(
211
+ Object.entries(translations).sort(([a], [b]) => a.localeCompare(b))
212
+ );
213
+ writeFileSync(targetFilePath, JSON.stringify(sorted, null, 2) + "\n");
214
+ }
215
+
130
216
  // src/cli.ts
131
217
  var CONFIG_FILENAMES = [
132
218
  "solid-translate.config.json",
@@ -152,6 +238,10 @@ async function main() {
152
238
  await runTranslate();
153
239
  return;
154
240
  }
241
+ if (command === "check") {
242
+ await runCheck(args.includes("--json"));
243
+ return;
244
+ }
155
245
  console.error(`Unknown command: ${command}`);
156
246
  printUsage();
157
247
  process.exit(1);
@@ -164,6 +254,8 @@ Usage:
164
254
  solid-translate init Create a config file
165
255
  solid-translate extract Extract strings from source files
166
256
  solid-translate translate Translate source strings + files to target locales
257
+ solid-translate check Verify translations are up to date (no AI calls)
258
+ Exit 0 = fresh, 1 = stale. Use --json for machine output
167
259
 
168
260
  Config: solid-translate.config.json (or .js/.ts)
169
261
 
@@ -176,7 +268,7 @@ Environment variables:
176
268
  }
177
269
  async function initConfig() {
178
270
  const configPath = resolve("solid-translate.config.json");
179
- if (existsSync(configPath)) {
271
+ if (existsSync2(configPath)) {
180
272
  console.log("Config already exists: solid-translate.config.json");
181
273
  return;
182
274
  }
@@ -194,16 +286,16 @@ async function initConfig() {
194
286
  mdx: { include: [] }
195
287
  }
196
288
  };
197
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
289
+ writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
198
290
  console.log("Created solid-translate.config.json");
199
291
  console.log("Edit it to set your target locales and AI provider.");
200
292
  }
201
293
  async function loadConfig() {
202
294
  for (const name of CONFIG_FILENAMES) {
203
295
  const path = resolve(name);
204
- if (existsSync(path)) {
296
+ if (existsSync2(path)) {
205
297
  if (name.endsWith(".json")) {
206
- return JSON.parse(readFileSync(path, "utf-8"));
298
+ return JSON.parse(readFileSync2(path, "utf-8"));
207
299
  }
208
300
  const mod = await import(path);
209
301
  return mod.default || mod;
@@ -267,7 +359,7 @@ async function runExtract() {
267
359
  "src/**/*.ts",
268
360
  "src/**/*.jsx"
269
361
  ];
270
- if (!existsSync(localesDir)) {
362
+ if (!existsSync2(localesDir)) {
271
363
  mkdirSync(localesDir, { recursive: true });
272
364
  }
273
365
  const { glob } = await import("glob");
@@ -277,7 +369,7 @@ async function runExtract() {
277
369
  const files = await glob(pattern, { cwd: root, absolute: true });
278
370
  for (const file of files) {
279
371
  try {
280
- const code = readFileSync(file, "utf-8");
372
+ const code = readFileSync2(file, "utf-8");
281
373
  const extracted = extractStringsFromSource(
282
374
  code,
283
375
  relative(root, file)
@@ -290,11 +382,11 @@ async function runExtract() {
290
382
  }
291
383
  }
292
384
  }
293
- const sourceFilePath = join(localesDir, `${sourceLocale}.json`);
385
+ const sourceFilePath = join2(localesDir, `${sourceLocale}.json`);
294
386
  let existing = {};
295
- if (existsSync(sourceFilePath)) {
387
+ if (existsSync2(sourceFilePath)) {
296
388
  try {
297
- existing = JSON.parse(readFileSync(sourceFilePath, "utf-8"));
389
+ existing = JSON.parse(readFileSync2(sourceFilePath, "utf-8"));
298
390
  } catch {
299
391
  }
300
392
  }
@@ -308,12 +400,157 @@ async function runExtract() {
308
400
  const sorted = Object.fromEntries(
309
401
  Object.entries(existing).sort(([a], [b]) => a.localeCompare(b))
310
402
  );
311
- writeFileSync(sourceFilePath, JSON.stringify(sorted, null, 2) + "\n");
403
+ writeFileSync2(sourceFilePath, JSON.stringify(sorted, null, 2) + "\n");
312
404
  console.log(
313
405
  `Extracted ${total} strings (${newKeys} new) \u2192 ${relative(root, sourceFilePath)}`
314
406
  );
315
407
  }
408
+ async function runCheck(jsonOutput) {
409
+ const config = await loadConfig();
410
+ const root = process.cwd();
411
+ const sourceLocale = config.sourceLocale || "en";
412
+ const targetLocales = config.targetLocales || [];
413
+ const localesDir = resolve(config.localesDir || "./src/locales");
414
+ const patterns = config.include || [
415
+ "src/**/*.tsx",
416
+ "src/**/*.ts",
417
+ "src/**/*.jsx"
418
+ ];
419
+ const { glob } = await import("glob");
420
+ const extracted = {};
421
+ const contexts = {};
422
+ for (const pattern of patterns) {
423
+ const files = await glob(pattern, { cwd: root, absolute: true });
424
+ for (const file of files) {
425
+ try {
426
+ const code = readFileSync2(file, "utf-8");
427
+ const entries = extractStringsFromSource(
428
+ code,
429
+ relative(root, file)
430
+ );
431
+ for (const entry of entries) {
432
+ extracted[entry.key] = entry.source;
433
+ if (entry.context) {
434
+ contexts[entry.key] = entry.context;
435
+ }
436
+ }
437
+ } catch {
438
+ }
439
+ }
440
+ }
441
+ const sourceFilePath = join2(localesDir, `${sourceLocale}.json`);
442
+ const sourceDict = {};
443
+ if (existsSync2(sourceFilePath)) {
444
+ try {
445
+ Object.assign(
446
+ sourceDict,
447
+ JSON.parse(readFileSync2(sourceFilePath, "utf-8"))
448
+ );
449
+ } catch {
450
+ }
451
+ }
452
+ for (const [key, value] of Object.entries(extracted)) {
453
+ if (!(key in sourceDict)) {
454
+ sourceDict[key] = value;
455
+ }
456
+ }
457
+ const lockFilePath = join2(localesDir, ".solid-translate.lock");
458
+ let lock = { version: 1, sourceLocale, keys: {} };
459
+ if (existsSync2(lockFilePath)) {
460
+ try {
461
+ lock = JSON.parse(readFileSync2(lockFilePath, "utf-8"));
462
+ } catch {
463
+ }
464
+ }
465
+ const report = {
466
+ fresh: true,
467
+ lock: { missing: [], changed: [], orphaned: [] },
468
+ locales: {}
469
+ };
470
+ for (const [key, value] of Object.entries(sourceDict)) {
471
+ const entry = lock.keys[key];
472
+ if (!entry) {
473
+ report.lock.missing.push(key);
474
+ } else if (entry.hash !== hashContent(value) || (entry.context ?? void 0) !== (contexts[key] ?? void 0)) {
475
+ report.lock.changed.push(key);
476
+ }
477
+ }
478
+ for (const key of Object.keys(lock.keys)) {
479
+ if (!(key in sourceDict)) {
480
+ report.lock.orphaned.push(key);
481
+ }
482
+ }
483
+ const sourceKeys = Object.keys(sourceDict);
484
+ for (const targetLocale of targetLocales) {
485
+ const targetFilePath = join2(localesDir, `${targetLocale}.json`);
486
+ let dict = {};
487
+ let fileExists = existsSync2(targetFilePath);
488
+ if (fileExists) {
489
+ try {
490
+ dict = JSON.parse(readFileSync2(targetFilePath, "utf-8"));
491
+ } catch {
492
+ fileExists = false;
493
+ }
494
+ }
495
+ const localeReport = {
496
+ missing: sourceKeys.filter((key) => !(key in dict)),
497
+ orphaned: Object.keys(dict).filter((key) => !(key in sourceDict)),
498
+ fileExists
499
+ };
500
+ report.locales[targetLocale] = localeReport;
501
+ }
502
+ report.fresh = report.lock.missing.length === 0 && report.lock.changed.length === 0 && report.lock.orphaned.length === 0 && Object.values(report.locales).every(
503
+ (l) => l.missing.length === 0 && l.orphaned.length === 0
504
+ );
505
+ if (jsonOutput) {
506
+ console.log(JSON.stringify(report, null, 2));
507
+ } else {
508
+ printCheckReport(report, sourceLocale);
509
+ }
510
+ process.exit(report.fresh ? 0 : 1);
511
+ }
512
+ function printCheckReport(report, sourceLocale) {
513
+ if (report.fresh) {
514
+ console.log("Translations are up to date.");
515
+ return;
516
+ }
517
+ console.log("Translations are stale:\n");
518
+ const { missing, changed, orphaned } = report.lock;
519
+ if (missing.length || changed.length || orphaned.length) {
520
+ console.log(".solid-translate.lock:");
521
+ for (const key of missing) {
522
+ console.log(` missing: ${JSON.stringify(key)} (never translated)`);
523
+ }
524
+ for (const key of changed) {
525
+ console.log(` changed: ${JSON.stringify(key)} (text or context changed)`);
526
+ }
527
+ for (const key of orphaned) {
528
+ console.log(` orphaned: ${JSON.stringify(key)} (no longer in source)`);
529
+ }
530
+ }
531
+ for (const [locale, localeReport] of Object.entries(report.locales)) {
532
+ if (!localeReport.missing.length && !localeReport.orphaned.length) {
533
+ continue;
534
+ }
535
+ if (!localeReport.fileExists) {
536
+ console.log(`${locale}.json: (file missing)`);
537
+ } else {
538
+ console.log(`${locale}.json:`);
539
+ }
540
+ for (const key of localeReport.missing) {
541
+ console.log(` missing: ${JSON.stringify(key)}`);
542
+ }
543
+ for (const key of localeReport.orphaned) {
544
+ console.log(` orphaned: ${JSON.stringify(key)}`);
545
+ }
546
+ }
547
+ console.log(
548
+ `
549
+ Run \`solid-translate translate\` to refresh ${sourceLocale} \u2192 targets.`
550
+ );
551
+ }
316
552
  async function runTranslate() {
553
+ const { translateBatch: translateBatch2, translateMarkdown: translateMarkdown2 } = await import("./translate-M737VQHG.js");
317
554
  const config = await loadConfig();
318
555
  const root = process.cwd();
319
556
  const sourceLocale = config.sourceLocale || "en";
@@ -350,7 +587,7 @@ async function runTranslate() {
350
587
  absolute: true
351
588
  });
352
589
  for (const file of files) {
353
- const content = readFileSync(file, "utf-8");
590
+ const content = readFileSync2(file, "utf-8");
354
591
  for (const targetLocale of targetLocales) {
355
592
  const targetPath = resolve(
356
593
  root,
@@ -366,7 +603,7 @@ async function runTranslate() {
366
603
  if (format === "json") {
367
604
  try {
368
605
  const sourceDict = JSON.parse(content);
369
- const translated = await translateBatch(
606
+ const translated = await translateBatch2(
370
607
  model,
371
608
  sourceDict,
372
609
  targetLocale,
@@ -374,7 +611,7 @@ async function runTranslate() {
374
611
  config.systemPrompt
375
612
  );
376
613
  mkdirSync(dirname(actualTarget), { recursive: true });
377
- writeFileSync(
614
+ writeFileSync2(
378
615
  actualTarget,
379
616
  JSON.stringify(translated, null, 2) + "\n"
380
617
  );
@@ -389,7 +626,7 @@ async function runTranslate() {
389
626
  }
390
627
  } else {
391
628
  try {
392
- const translated = await translateMarkdown(
629
+ const translated = await translateMarkdown2(
393
630
  model,
394
631
  content,
395
632
  targetLocale,
@@ -397,7 +634,7 @@ async function runTranslate() {
397
634
  config.systemPrompt
398
635
  );
399
636
  mkdirSync(dirname(actualTarget), { recursive: true });
400
- writeFileSync(actualTarget, translated);
637
+ writeFileSync2(actualTarget, translated);
401
638
  console.log(
402
639
  `${format}: ${relative(root, actualTarget)}`
403
640
  );
@@ -416,86 +653,37 @@ async function runTranslate() {
416
653
  console.log("\nTranslation complete.");
417
654
  }
418
655
  async function translateLocaleFiles(model, localesDir, sourceLocale, targetLocales, batchSize, systemPrompt) {
419
- const sourceFilePath = join(localesDir, `${sourceLocale}.json`);
420
- if (!existsSync(sourceFilePath)) {
656
+ const result = await syncLocaleFiles({
657
+ localesDir,
658
+ sourceLocale,
659
+ targetLocales,
660
+ batchSize,
661
+ translate: (batch, targetLocale, contexts) => translateBatch(
662
+ model,
663
+ batch,
664
+ targetLocale,
665
+ sourceLocale,
666
+ systemPrompt,
667
+ contexts
668
+ ),
669
+ log: (message) => console.log(message)
670
+ });
671
+ if (result.status === "no-source") {
421
672
  console.log(
422
673
  "No source locale file found. Run `solid-translate extract` first."
423
674
  );
424
675
  return;
425
676
  }
426
- const sourceDict = JSON.parse(
427
- readFileSync(sourceFilePath, "utf-8")
428
- );
429
- const lockFilePath = join(localesDir, ".solid-translate.lock");
430
- let lock = { version: 1, sourceLocale, keys: {} };
431
- if (existsSync(lockFilePath)) {
432
- try {
433
- lock = JSON.parse(readFileSync(lockFilePath, "utf-8"));
434
- } catch {
435
- }
436
- }
437
- const changedKeys = {};
438
- for (const [key, value] of Object.entries(sourceDict)) {
439
- const hash = hashContent(value);
440
- const existing = lock.keys[key];
441
- if (!existing || existing.hash !== hash) {
442
- changedKeys[key] = value;
443
- lock.keys[key] = { hash, source: value };
444
- }
445
- }
446
- for (const key of Object.keys(lock.keys)) {
447
- if (!(key in sourceDict)) {
448
- delete lock.keys[key];
449
- }
450
- }
451
- if (Object.keys(changedKeys).length === 0) {
452
- console.log("No changes detected in locale files.");
453
- return;
454
- }
455
- const count = Object.keys(changedKeys).length;
456
- console.log(
457
- `Translating ${count} key${count > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
458
- );
459
- for (const targetLocale of targetLocales) {
460
- const targetFilePath = join(localesDir, `${targetLocale}.json`);
461
- let existing = {};
462
- if (existsSync(targetFilePath)) {
463
- try {
464
- existing = JSON.parse(readFileSync(targetFilePath, "utf-8"));
465
- } catch {
466
- }
677
+ if (result.failures.length > 0) {
678
+ console.error("\nTranslation failed for some batches:");
679
+ for (const line of formatSyncFailures(result.failures)) {
680
+ console.error(` ${line}`);
467
681
  }
468
- const entries = Object.entries(changedKeys);
469
- for (let i = 0; i < entries.length; i += batchSize) {
470
- const batch = Object.fromEntries(entries.slice(i, i + batchSize));
471
- try {
472
- const translated = await translateBatch(
473
- model,
474
- batch,
475
- targetLocale,
476
- sourceLocale,
477
- systemPrompt
478
- );
479
- Object.assign(existing, translated);
480
- } catch (err) {
481
- console.error(
482
- `Failed to translate batch for ${targetLocale}:`,
483
- err
484
- );
485
- }
486
- }
487
- for (const key of Object.keys(existing)) {
488
- if (!(key in sourceDict)) {
489
- delete existing[key];
490
- }
491
- }
492
- const sorted = Object.fromEntries(
493
- Object.entries(existing).sort(([a], [b]) => a.localeCompare(b))
682
+ console.error(
683
+ "Failed keys were not recorded in the lock file \u2014 fix the error and rerun `solid-translate translate` to retry them."
494
684
  );
495
- writeFileSync(targetFilePath, JSON.stringify(sorted, null, 2) + "\n");
496
- console.log(` ${targetLocale}: ${Object.keys(sorted).length} keys`);
685
+ process.exit(1);
497
686
  }
498
- writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
499
687
  }
500
688
  main().catch((err) => {
501
689
  console.error(err);