solid-translate 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/translate.ts
4
+ import { z } from "zod";
5
+ async function loadGenerateObject() {
6
+ const { generateObject } = await import("ai");
7
+ return generateObject;
8
+ }
9
+ async function loadGenerateText() {
10
+ const { generateText } = await import("ai");
11
+ return generateText;
12
+ }
13
+ function extractJsonObject(text) {
14
+ const start = text.indexOf("{");
15
+ const end = text.lastIndexOf("}");
16
+ if (start === -1 || end <= start) {
17
+ throw new Error("model response contained no JSON object");
18
+ }
19
+ const parsed = JSON.parse(text.slice(start, end + 1));
20
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
21
+ throw new Error("model response was not a JSON object");
22
+ }
23
+ return parsed;
24
+ }
25
+ function collectBatchTranslations(parsed, requestedKeys) {
26
+ let dict = parsed;
27
+ const inner = parsed["translations"];
28
+ if (typeof inner === "object" && inner !== null && !Array.isArray(inner) && // Only unwrap when the envelope key is not itself a requested key
29
+ !requestedKeys.includes("translations")) {
30
+ dict = inner;
31
+ }
32
+ const translations = {};
33
+ const missing = [];
34
+ for (const key of requestedKeys) {
35
+ const value = dict[key];
36
+ if (typeof value === "string" && value.length > 0) {
37
+ translations[key] = value;
38
+ } else {
39
+ missing.push(key);
40
+ }
41
+ }
42
+ return { translations, missing };
43
+ }
44
+ async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts) {
45
+ const keys = Object.keys(entries);
46
+ if (keys.length === 0) return {};
47
+ const defaultSystem = [
48
+ `You are a professional translator specializing in software localization.`,
49
+ `Translate text from "${sourceLocale}" to "${targetLocale}".`,
50
+ `Rules:`,
51
+ `- Preserve the original tone and meaning`,
52
+ `- Keep placeholders like {{variable}}, {variable}, {0}, {1} unchanged`,
53
+ `- Keep HTML tags unchanged`,
54
+ `- Do not add or remove content`,
55
+ `- Return natural, idiomatic translations`
56
+ ].join("\n");
57
+ let contextSection = "";
58
+ if (contexts && Object.keys(contexts).length > 0) {
59
+ const contextLines = Object.entries(contexts).filter(([key]) => key in entries).map(([key, ctx]) => ` "${key}": ${ctx}`);
60
+ if (contextLines.length > 0) {
61
+ contextSection = [
62
+ ``,
63
+ `Context hints for disambiguation:`,
64
+ ...contextLines,
65
+ ``
66
+ ].join("\n");
67
+ }
68
+ }
69
+ const generateText = await loadGenerateText();
70
+ const basePrompt = [
71
+ `Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
72
+ `Respond with ONLY a JSON object \u2014 no prose, no code fences \u2014 containing the exact same keys and the translated values.`,
73
+ contextSection,
74
+ JSON.stringify(entries, null, 2)
75
+ ].join("\n");
76
+ const attempt = async (prompt) => {
77
+ const { text } = await generateText({
78
+ model,
79
+ system: systemPrompt || defaultSystem,
80
+ prompt
81
+ });
82
+ return collectBatchTranslations(extractJsonObject(text), keys);
83
+ };
84
+ let { translations, missing } = await attempt(basePrompt);
85
+ if (missing.length > 0) {
86
+ const retryEntries = {};
87
+ for (const key of missing) retryEntries[key] = entries[key];
88
+ const retry = await attempt(
89
+ [
90
+ `Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
91
+ `Respond with ONLY a JSON object \u2014 no prose, no code fences \u2014 containing the exact same keys and the translated values.`,
92
+ contextSection,
93
+ JSON.stringify(retryEntries, null, 2)
94
+ ].join("\n")
95
+ );
96
+ translations = { ...translations, ...retry.translations };
97
+ missing = retry.missing;
98
+ }
99
+ if (missing.length > 0) {
100
+ const sample = missing.slice(0, 3).join('", "');
101
+ throw new Error(
102
+ `model returned no translation for ${missing.length} of ${keys.length} keys (e.g. "${sample}")`
103
+ );
104
+ }
105
+ return translations;
106
+ }
107
+ async function translateMarkdown(model, content, targetLocale, sourceLocale, systemPrompt) {
108
+ const defaultSystem = [
109
+ `You are a professional translator specializing in documentation.`,
110
+ `Translate Markdown/MDX content from "${sourceLocale}" to "${targetLocale}".`,
111
+ `Rules:`,
112
+ `- Preserve all Markdown formatting (headers, lists, bold, italic, links, etc.)`,
113
+ `- Preserve code blocks and inline code unchanged`,
114
+ `- Preserve frontmatter YAML keys (only translate values)`,
115
+ `- Preserve MDX component syntax and JSX expressions`,
116
+ `- Preserve URLs and file paths unchanged`,
117
+ `- Return natural, idiomatic translations`
118
+ ].join("\n");
119
+ const generateObject = await loadGenerateObject();
120
+ const { object } = await generateObject({
121
+ model,
122
+ schema: z.object({
123
+ translated: z.string()
124
+ }),
125
+ system: systemPrompt || defaultSystem,
126
+ prompt: [
127
+ `Translate this Markdown/MDX content from "${sourceLocale}" to "${targetLocale}".`,
128
+ `Return the complete translated document.`,
129
+ ``,
130
+ content
131
+ ].join("\n")
132
+ });
133
+ return object.translated;
134
+ }
135
+
136
+ export {
137
+ extractJsonObject,
138
+ collectBatchTranslations,
139
+ translateBatch,
140
+ translateMarkdown
141
+ };
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  translateBatch
4
- } from "./chunk-2BKJUY37.js";
4
+ } from "./chunk-P6RTZPHZ.js";
5
5
  import {
6
6
  __commonJS,
7
7
  __toESM
@@ -15106,7 +15106,16 @@ async function syncLocaleFiles(options) {
15106
15106
  delete lock.keys[key];
15107
15107
  }
15108
15108
  const changedCount = Object.keys(changedKeys).length;
15109
- if (changedCount === 0 && deletedKeys.length === 0) {
15109
+ const missingByLocale = {};
15110
+ for (const targetLocale of targetLocales) {
15111
+ const existing = readTargetFile(join(localesDir, `${targetLocale}.json`));
15112
+ const missing = Object.keys(sourceDict).filter(
15113
+ (key) => !(key in changedKeys) && !(key in existing)
15114
+ );
15115
+ if (missing.length > 0) missingByLocale[targetLocale] = missing;
15116
+ }
15117
+ const missingLocaleCount = Object.keys(missingByLocale).length;
15118
+ if (changedCount === 0 && deletedKeys.length === 0 && missingLocaleCount === 0) {
15110
15119
  log("No changes detected in locale files.");
15111
15120
  return {
15112
15121
  status: "no-changes",
@@ -15115,7 +15124,7 @@ async function syncLocaleFiles(options) {
15115
15124
  failures: []
15116
15125
  };
15117
15126
  }
15118
- if (changedCount === 0) {
15127
+ if (changedCount === 0 && missingLocaleCount === 0) {
15119
15128
  for (const targetLocale of targetLocales) {
15120
15129
  const targetFilePath = join(localesDir, `${targetLocale}.json`);
15121
15130
  const existing = readTargetFile(targetFilePath);
@@ -15128,27 +15137,43 @@ async function syncLocaleFiles(options) {
15128
15137
  );
15129
15138
  return { status: "synced", translatedKeys: [], deletedKeys, failures: [] };
15130
15139
  }
15131
- log(
15132
- `Translating ${changedCount} key${changedCount > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
15133
- );
15134
- const changedContexts = {};
15135
- for (const key of Object.keys(changedKeys)) {
15136
- const ctx = pendingEntries[key]?.context;
15137
- if (ctx) changedContexts[key] = ctx;
15140
+ if (changedCount > 0) {
15141
+ log(
15142
+ `Translating ${changedCount} key${changedCount > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
15143
+ );
15138
15144
  }
15145
+ if (missingLocaleCount > 0) {
15146
+ const healTotal = Object.values(missingByLocale).reduce(
15147
+ (sum, keys) => sum + keys.length,
15148
+ 0
15149
+ );
15150
+ log(
15151
+ `Healing ${healTotal} key${healTotal > 1 ? "s" : ""} missing from ${missingLocaleCount} locale file${missingLocaleCount > 1 ? "s" : ""}...`
15152
+ );
15153
+ }
15154
+ const contextFor = (key) => pendingEntries[key]?.context ?? lock.keys[key]?.context;
15139
15155
  const failures = [];
15140
15156
  const failedKeys = /* @__PURE__ */ new Set();
15141
15157
  for (const targetLocale of targetLocales) {
15142
15158
  const targetFilePath = join(localesDir, `${targetLocale}.json`);
15143
15159
  const existing = readTargetFile(targetFilePath);
15144
- const entries = Object.entries(changedKeys);
15160
+ const localeEntries = { ...changedKeys };
15161
+ for (const key of missingByLocale[targetLocale] ?? []) {
15162
+ localeEntries[key] = sourceDict[key];
15163
+ }
15164
+ const localeContexts = {};
15165
+ for (const key of Object.keys(localeEntries)) {
15166
+ const ctx = contextFor(key);
15167
+ if (ctx) localeContexts[key] = ctx;
15168
+ }
15169
+ const entries = Object.entries(localeEntries);
15145
15170
  for (let i = 0; i < entries.length; i += batchSize) {
15146
15171
  const batch = Object.fromEntries(entries.slice(i, i + batchSize));
15147
15172
  try {
15148
15173
  const translated = await translate(
15149
15174
  batch,
15150
15175
  targetLocale,
15151
- changedContexts
15176
+ localeContexts
15152
15177
  );
15153
15178
  Object.assign(existing, translated);
15154
15179
  } catch (err) {
@@ -15545,7 +15570,7 @@ Run \`solid-translate translate\` to refresh ${sourceLocale} \u2192 targets.`
15545
15570
  );
15546
15571
  }
15547
15572
  async function runTranslate() {
15548
- const { translateBatch: translateBatch2, translateMarkdown: translateMarkdown2 } = await import("./translate-M737VQHG.js");
15573
+ const { translateBatch: translateBatch2, translateMarkdown: translateMarkdown2 } = await import("./translate-SWLCBQ5Y.js");
15549
15574
  const config = await loadConfig();
15550
15575
  const root = process.cwd();
15551
15576
  const sourceLocale = config.sourceLocale || "en";
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ collectBatchTranslations,
4
+ extractJsonObject,
3
5
  translateBatch,
4
6
  translateMarkdown
5
- } from "./chunk-2BKJUY37.js";
7
+ } from "./chunk-P6RTZPHZ.js";
6
8
  import "./chunk-FYS2JH42.js";
7
9
  export {
10
+ collectBatchTranslations,
11
+ extractJsonObject,
8
12
  translateBatch,
9
13
  translateMarkdown
10
14
  };
package/dist/vite.js CHANGED
@@ -14617,9 +14617,40 @@ function hashContent(content) {
14617
14617
 
14618
14618
  // src/translate.ts
14619
14619
  import { z } from "zod";
14620
- async function loadGenerateObject() {
14621
- const { generateObject } = await import("ai");
14622
- return generateObject;
14620
+ async function loadGenerateText() {
14621
+ const { generateText } = await import("ai");
14622
+ return generateText;
14623
+ }
14624
+ function extractJsonObject(text) {
14625
+ const start = text.indexOf("{");
14626
+ const end = text.lastIndexOf("}");
14627
+ if (start === -1 || end <= start) {
14628
+ throw new Error("model response contained no JSON object");
14629
+ }
14630
+ const parsed = JSON.parse(text.slice(start, end + 1));
14631
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
14632
+ throw new Error("model response was not a JSON object");
14633
+ }
14634
+ return parsed;
14635
+ }
14636
+ function collectBatchTranslations(parsed, requestedKeys) {
14637
+ let dict = parsed;
14638
+ const inner = parsed["translations"];
14639
+ if (typeof inner === "object" && inner !== null && !Array.isArray(inner) && // Only unwrap when the envelope key is not itself a requested key
14640
+ !requestedKeys.includes("translations")) {
14641
+ dict = inner;
14642
+ }
14643
+ const translations = {};
14644
+ const missing = [];
14645
+ for (const key of requestedKeys) {
14646
+ const value = dict[key];
14647
+ if (typeof value === "string" && value.length > 0) {
14648
+ translations[key] = value;
14649
+ } else {
14650
+ missing.push(key);
14651
+ }
14652
+ }
14653
+ return { translations, missing };
14623
14654
  }
14624
14655
  async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts) {
14625
14656
  const keys = Object.keys(entries);
@@ -14646,21 +14677,43 @@ async function translateBatch(model, entries, targetLocale, sourceLocale, system
14646
14677
  ].join("\n");
14647
14678
  }
14648
14679
  }
14649
- const generateObject = await loadGenerateObject();
14650
- const { object } = await generateObject({
14651
- model,
14652
- schema: z.object({
14653
- translations: z.record(z.string(), z.string())
14654
- }),
14655
- system: systemPrompt || defaultSystem,
14656
- prompt: [
14657
- `Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
14658
- `Return a JSON object with the exact same keys and the translated values.`,
14659
- contextSection,
14660
- JSON.stringify(entries, null, 2)
14661
- ].join("\n")
14662
- });
14663
- return object.translations;
14680
+ const generateText = await loadGenerateText();
14681
+ const basePrompt = [
14682
+ `Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
14683
+ `Respond with ONLY a JSON object \u2014 no prose, no code fences \u2014 containing the exact same keys and the translated values.`,
14684
+ contextSection,
14685
+ JSON.stringify(entries, null, 2)
14686
+ ].join("\n");
14687
+ const attempt = async (prompt) => {
14688
+ const { text } = await generateText({
14689
+ model,
14690
+ system: systemPrompt || defaultSystem,
14691
+ prompt
14692
+ });
14693
+ return collectBatchTranslations(extractJsonObject(text), keys);
14694
+ };
14695
+ let { translations, missing } = await attempt(basePrompt);
14696
+ if (missing.length > 0) {
14697
+ const retryEntries = {};
14698
+ for (const key of missing) retryEntries[key] = entries[key];
14699
+ const retry = await attempt(
14700
+ [
14701
+ `Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
14702
+ `Respond with ONLY a JSON object \u2014 no prose, no code fences \u2014 containing the exact same keys and the translated values.`,
14703
+ contextSection,
14704
+ JSON.stringify(retryEntries, null, 2)
14705
+ ].join("\n")
14706
+ );
14707
+ translations = { ...translations, ...retry.translations };
14708
+ missing = retry.missing;
14709
+ }
14710
+ if (missing.length > 0) {
14711
+ const sample = missing.slice(0, 3).join('", "');
14712
+ throw new Error(
14713
+ `model returned no translation for ${missing.length} of ${keys.length} keys (e.g. "${sample}")`
14714
+ );
14715
+ }
14716
+ return translations;
14664
14717
  }
14665
14718
 
14666
14719
  // src/extract.ts
@@ -15172,7 +15225,16 @@ async function syncLocaleFiles(options) {
15172
15225
  delete lock.keys[key];
15173
15226
  }
15174
15227
  const changedCount = Object.keys(changedKeys).length;
15175
- if (changedCount === 0 && deletedKeys.length === 0) {
15228
+ const missingByLocale = {};
15229
+ for (const targetLocale of targetLocales) {
15230
+ const existing = readTargetFile(join(localesDir, `${targetLocale}.json`));
15231
+ const missing = Object.keys(sourceDict).filter(
15232
+ (key) => !(key in changedKeys) && !(key in existing)
15233
+ );
15234
+ if (missing.length > 0) missingByLocale[targetLocale] = missing;
15235
+ }
15236
+ const missingLocaleCount = Object.keys(missingByLocale).length;
15237
+ if (changedCount === 0 && deletedKeys.length === 0 && missingLocaleCount === 0) {
15176
15238
  log("No changes detected in locale files.");
15177
15239
  return {
15178
15240
  status: "no-changes",
@@ -15181,7 +15243,7 @@ async function syncLocaleFiles(options) {
15181
15243
  failures: []
15182
15244
  };
15183
15245
  }
15184
- if (changedCount === 0) {
15246
+ if (changedCount === 0 && missingLocaleCount === 0) {
15185
15247
  for (const targetLocale of targetLocales) {
15186
15248
  const targetFilePath = join(localesDir, `${targetLocale}.json`);
15187
15249
  const existing = readTargetFile(targetFilePath);
@@ -15194,27 +15256,43 @@ async function syncLocaleFiles(options) {
15194
15256
  );
15195
15257
  return { status: "synced", translatedKeys: [], deletedKeys, failures: [] };
15196
15258
  }
15197
- log(
15198
- `Translating ${changedCount} key${changedCount > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
15199
- );
15200
- const changedContexts = {};
15201
- for (const key of Object.keys(changedKeys)) {
15202
- const ctx = pendingEntries[key]?.context;
15203
- if (ctx) changedContexts[key] = ctx;
15259
+ if (changedCount > 0) {
15260
+ log(
15261
+ `Translating ${changedCount} key${changedCount > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
15262
+ );
15204
15263
  }
15264
+ if (missingLocaleCount > 0) {
15265
+ const healTotal = Object.values(missingByLocale).reduce(
15266
+ (sum, keys) => sum + keys.length,
15267
+ 0
15268
+ );
15269
+ log(
15270
+ `Healing ${healTotal} key${healTotal > 1 ? "s" : ""} missing from ${missingLocaleCount} locale file${missingLocaleCount > 1 ? "s" : ""}...`
15271
+ );
15272
+ }
15273
+ const contextFor = (key) => pendingEntries[key]?.context ?? lock.keys[key]?.context;
15205
15274
  const failures = [];
15206
15275
  const failedKeys = /* @__PURE__ */ new Set();
15207
15276
  for (const targetLocale of targetLocales) {
15208
15277
  const targetFilePath = join(localesDir, `${targetLocale}.json`);
15209
15278
  const existing = readTargetFile(targetFilePath);
15210
- const entries = Object.entries(changedKeys);
15279
+ const localeEntries = { ...changedKeys };
15280
+ for (const key of missingByLocale[targetLocale] ?? []) {
15281
+ localeEntries[key] = sourceDict[key];
15282
+ }
15283
+ const localeContexts = {};
15284
+ for (const key of Object.keys(localeEntries)) {
15285
+ const ctx = contextFor(key);
15286
+ if (ctx) localeContexts[key] = ctx;
15287
+ }
15288
+ const entries = Object.entries(localeEntries);
15211
15289
  for (let i = 0; i < entries.length; i += batchSize) {
15212
15290
  const batch = Object.fromEntries(entries.slice(i, i + batchSize));
15213
15291
  try {
15214
15292
  const translated = await translate(
15215
15293
  batch,
15216
15294
  targetLocale,
15217
- changedContexts
15295
+ localeContexts
15218
15296
  );
15219
15297
  Object.assign(existing, translated);
15220
15298
  } catch (err) {