solid-translate 0.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/LICENSE +21 -0
- package/README.md +451 -0
- package/dist/cli.js +502 -0
- package/dist/index.d.ts +239 -0
- package/dist/index.js +238 -0
- package/dist/index.js.map +1 -0
- package/dist/vite.d.ts +42 -0
- package/dist/vite.js +339 -0
- package/dist/vite.js.map +1 -0
- package/package.json +92 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import {
|
|
6
|
+
readFileSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync
|
|
10
|
+
} from "fs";
|
|
11
|
+
import { resolve, join, dirname, relative, basename } from "path";
|
|
12
|
+
|
|
13
|
+
// src/hash.ts
|
|
14
|
+
import { createHash } from "crypto";
|
|
15
|
+
function hashContent(content) {
|
|
16
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/translate.ts
|
|
20
|
+
import { generateObject } from "ai";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts) {
|
|
23
|
+
const keys = Object.keys(entries);
|
|
24
|
+
if (keys.length === 0) return {};
|
|
25
|
+
const defaultSystem = [
|
|
26
|
+
`You are a professional translator specializing in software localization.`,
|
|
27
|
+
`Translate text from "${sourceLocale}" to "${targetLocale}".`,
|
|
28
|
+
`Rules:`,
|
|
29
|
+
`- Preserve the original tone and meaning`,
|
|
30
|
+
`- Keep placeholders like {{variable}}, {variable}, {0}, {1} unchanged`,
|
|
31
|
+
`- Keep HTML tags unchanged`,
|
|
32
|
+
`- Do not add or remove content`,
|
|
33
|
+
`- Return natural, idiomatic translations`
|
|
34
|
+
].join("\n");
|
|
35
|
+
let contextSection = "";
|
|
36
|
+
if (contexts && Object.keys(contexts).length > 0) {
|
|
37
|
+
const contextLines = Object.entries(contexts).filter(([key]) => key in entries).map(([key, ctx]) => ` "${key}": ${ctx}`);
|
|
38
|
+
if (contextLines.length > 0) {
|
|
39
|
+
contextSection = [
|
|
40
|
+
``,
|
|
41
|
+
`Context hints for disambiguation:`,
|
|
42
|
+
...contextLines,
|
|
43
|
+
``
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const { object } = await generateObject({
|
|
48
|
+
model,
|
|
49
|
+
schema: z.object({
|
|
50
|
+
translations: z.record(z.string(), z.string())
|
|
51
|
+
}),
|
|
52
|
+
system: systemPrompt || defaultSystem,
|
|
53
|
+
prompt: [
|
|
54
|
+
`Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
|
|
55
|
+
`Return a JSON object with the exact same keys and the translated values.`,
|
|
56
|
+
contextSection,
|
|
57
|
+
JSON.stringify(entries, null, 2)
|
|
58
|
+
].join("\n")
|
|
59
|
+
});
|
|
60
|
+
return object.translations;
|
|
61
|
+
}
|
|
62
|
+
async function translateMarkdown(model, content, targetLocale, sourceLocale, systemPrompt) {
|
|
63
|
+
const defaultSystem = [
|
|
64
|
+
`You are a professional translator specializing in documentation.`,
|
|
65
|
+
`Translate Markdown/MDX content from "${sourceLocale}" to "${targetLocale}".`,
|
|
66
|
+
`Rules:`,
|
|
67
|
+
`- Preserve all Markdown formatting (headers, lists, bold, italic, links, etc.)`,
|
|
68
|
+
`- Preserve code blocks and inline code unchanged`,
|
|
69
|
+
`- Preserve frontmatter YAML keys (only translate values)`,
|
|
70
|
+
`- Preserve MDX component syntax and JSX expressions`,
|
|
71
|
+
`- Preserve URLs and file paths unchanged`,
|
|
72
|
+
`- Return natural, idiomatic translations`
|
|
73
|
+
].join("\n");
|
|
74
|
+
const { object } = await generateObject({
|
|
75
|
+
model,
|
|
76
|
+
schema: z.object({
|
|
77
|
+
translated: z.string()
|
|
78
|
+
}),
|
|
79
|
+
system: systemPrompt || defaultSystem,
|
|
80
|
+
prompt: [
|
|
81
|
+
`Translate this Markdown/MDX content from "${sourceLocale}" to "${targetLocale}".`,
|
|
82
|
+
`Return the complete translated document.`,
|
|
83
|
+
``,
|
|
84
|
+
content
|
|
85
|
+
].join("\n")
|
|
86
|
+
});
|
|
87
|
+
return object.translated;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/extract.ts
|
|
91
|
+
function extractStringsFromSource(code, filePath) {
|
|
92
|
+
const results = [];
|
|
93
|
+
const seen = /* @__PURE__ */ new Set();
|
|
94
|
+
const tComponentRegex = /<T(\s[^>]*)?>([^]*?)<\/T>/g;
|
|
95
|
+
let match;
|
|
96
|
+
while ((match = tComponentRegex.exec(code)) !== null) {
|
|
97
|
+
const attrs = match[1] || "";
|
|
98
|
+
const rawChildren = match[2];
|
|
99
|
+
const line = code.substring(0, match.index).split("\n").length;
|
|
100
|
+
const idMatch = attrs.match(/id=["']([^"']+)["']/);
|
|
101
|
+
const contextMatch = attrs.match(/context=["']([^"']+)["']/);
|
|
102
|
+
let slotIndex = 0;
|
|
103
|
+
const source = rawChildren.replace(
|
|
104
|
+
/<(?:Var|Num|Currency|DateTime)(?:\s[^>]*)?>([^]*?)<\/(?:Var|Num|Currency|DateTime)>/g,
|
|
105
|
+
() => `{${slotIndex++}}`
|
|
106
|
+
).trim();
|
|
107
|
+
const key = idMatch ? idMatch[1] : source;
|
|
108
|
+
if (!key || seen.has(key)) continue;
|
|
109
|
+
seen.add(key);
|
|
110
|
+
results.push({
|
|
111
|
+
key,
|
|
112
|
+
source,
|
|
113
|
+
file: filePath,
|
|
114
|
+
line,
|
|
115
|
+
context: contextMatch ? contextMatch[1] : void 0
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const msgRegex = /\bmsg\(\s*["']([^"']+)["']\s*(?:,\s*\{[^}]*\})?\s*\)/g;
|
|
119
|
+
while ((match = msgRegex.exec(code)) !== null) {
|
|
120
|
+
const source = match[1];
|
|
121
|
+
if (seen.has(source)) continue;
|
|
122
|
+
seen.add(source);
|
|
123
|
+
const line = code.substring(0, match.index).split("\n").length;
|
|
124
|
+
results.push({ key: source, source, file: filePath, line });
|
|
125
|
+
}
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/cli.ts
|
|
130
|
+
var CONFIG_FILENAMES = [
|
|
131
|
+
"solid-translate.config.json",
|
|
132
|
+
"solid-translate.config.js",
|
|
133
|
+
"solid-translate.config.ts"
|
|
134
|
+
];
|
|
135
|
+
async function main() {
|
|
136
|
+
const args = process.argv.slice(2);
|
|
137
|
+
const command = args[0];
|
|
138
|
+
if (!command || command === "--help" || command === "-h") {
|
|
139
|
+
printUsage();
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
if (command === "init") {
|
|
143
|
+
await initConfig();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (command === "extract") {
|
|
147
|
+
await runExtract();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (command === "translate") {
|
|
151
|
+
await runTranslate();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
console.error(`Unknown command: ${command}`);
|
|
155
|
+
printUsage();
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
function printUsage() {
|
|
159
|
+
console.log(`
|
|
160
|
+
solid-translate \u2014 AI-powered translation CLI for SolidJS apps
|
|
161
|
+
|
|
162
|
+
Usage:
|
|
163
|
+
solid-translate init Create a config file
|
|
164
|
+
solid-translate extract Extract strings from source files
|
|
165
|
+
solid-translate translate Translate source strings + files to target locales
|
|
166
|
+
|
|
167
|
+
Config: solid-translate.config.json (or .js/.ts)
|
|
168
|
+
|
|
169
|
+
Environment variables:
|
|
170
|
+
OPENROUTER_API_KEY OpenRouter API key
|
|
171
|
+
OPENAI_API_KEY OpenAI API key
|
|
172
|
+
ANTHROPIC_API_KEY Anthropic API key
|
|
173
|
+
GOOGLE_API_KEY Google AI API key
|
|
174
|
+
`);
|
|
175
|
+
}
|
|
176
|
+
async function initConfig() {
|
|
177
|
+
const configPath = resolve("solid-translate.config.json");
|
|
178
|
+
if (existsSync(configPath)) {
|
|
179
|
+
console.log("Config already exists: solid-translate.config.json");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const config = {
|
|
183
|
+
sourceLocale: "en",
|
|
184
|
+
targetLocales: ["es", "fr", "de"],
|
|
185
|
+
localesDir: "./src/locales",
|
|
186
|
+
provider: "openai",
|
|
187
|
+
model: "gpt-4o-mini",
|
|
188
|
+
batchSize: 50,
|
|
189
|
+
include: ["src/**/*.tsx", "src/**/*.ts"],
|
|
190
|
+
files: {
|
|
191
|
+
json: { include: [] },
|
|
192
|
+
md: { include: [] },
|
|
193
|
+
mdx: { include: [] }
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
197
|
+
console.log("Created solid-translate.config.json");
|
|
198
|
+
console.log("Edit it to set your target locales and AI provider.");
|
|
199
|
+
}
|
|
200
|
+
async function loadConfig() {
|
|
201
|
+
for (const name of CONFIG_FILENAMES) {
|
|
202
|
+
const path = resolve(name);
|
|
203
|
+
if (existsSync(path)) {
|
|
204
|
+
if (name.endsWith(".json")) {
|
|
205
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
206
|
+
}
|
|
207
|
+
const mod = await import(path);
|
|
208
|
+
return mod.default || mod;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
console.error(
|
|
212
|
+
"No config file found. Run `solid-translate init` to create one."
|
|
213
|
+
);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
async function createModel(config) {
|
|
217
|
+
const provider = config.provider || "openai";
|
|
218
|
+
const modelId = config.model || "gpt-4o-mini";
|
|
219
|
+
try {
|
|
220
|
+
if (provider === "openai" || provider === "openrouter") {
|
|
221
|
+
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
222
|
+
if (provider === "openrouter") {
|
|
223
|
+
const openrouter = createOpenAI({
|
|
224
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
225
|
+
apiKey: process.env.OPENROUTER_API_KEY
|
|
226
|
+
});
|
|
227
|
+
return openrouter(modelId);
|
|
228
|
+
}
|
|
229
|
+
const openai = createOpenAI({
|
|
230
|
+
apiKey: process.env.OPENAI_API_KEY
|
|
231
|
+
});
|
|
232
|
+
return openai(modelId);
|
|
233
|
+
}
|
|
234
|
+
if (provider === "anthropic") {
|
|
235
|
+
const { createAnthropic } = await import("@ai-sdk/anthropic");
|
|
236
|
+
const anthropic = createAnthropic({
|
|
237
|
+
apiKey: process.env.ANTHROPIC_API_KEY
|
|
238
|
+
});
|
|
239
|
+
return anthropic(modelId);
|
|
240
|
+
}
|
|
241
|
+
if (provider === "google") {
|
|
242
|
+
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
|
|
243
|
+
const google = createGoogleGenerativeAI({
|
|
244
|
+
apiKey: process.env.GOOGLE_API_KEY
|
|
245
|
+
});
|
|
246
|
+
return google(modelId);
|
|
247
|
+
}
|
|
248
|
+
console.error(`Unknown provider: ${provider}`);
|
|
249
|
+
console.error("Supported: openai, openrouter, anthropic, google");
|
|
250
|
+
process.exit(1);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error(
|
|
253
|
+
`Failed to load AI provider "${provider}". Make sure @ai-sdk/${provider} is installed.`
|
|
254
|
+
);
|
|
255
|
+
console.error(err.message);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function runExtract() {
|
|
260
|
+
const config = await loadConfig();
|
|
261
|
+
const root = process.cwd();
|
|
262
|
+
const sourceLocale = config.sourceLocale || "en";
|
|
263
|
+
const localesDir = resolve(config.localesDir || "./src/locales");
|
|
264
|
+
const patterns = config.include || [
|
|
265
|
+
"src/**/*.tsx",
|
|
266
|
+
"src/**/*.ts",
|
|
267
|
+
"src/**/*.jsx"
|
|
268
|
+
];
|
|
269
|
+
if (!existsSync(localesDir)) {
|
|
270
|
+
mkdirSync(localesDir, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
const { glob } = await import("glob");
|
|
273
|
+
const strings = {};
|
|
274
|
+
let total = 0;
|
|
275
|
+
for (const pattern of patterns) {
|
|
276
|
+
const files = await glob(pattern, { cwd: root, absolute: true });
|
|
277
|
+
for (const file of files) {
|
|
278
|
+
try {
|
|
279
|
+
const code = readFileSync(file, "utf-8");
|
|
280
|
+
const extracted = extractStringsFromSource(
|
|
281
|
+
code,
|
|
282
|
+
relative(root, file)
|
|
283
|
+
);
|
|
284
|
+
for (const entry of extracted) {
|
|
285
|
+
strings[entry.key] = entry.source;
|
|
286
|
+
total++;
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const sourceFilePath = join(localesDir, `${sourceLocale}.json`);
|
|
293
|
+
let existing = {};
|
|
294
|
+
if (existsSync(sourceFilePath)) {
|
|
295
|
+
try {
|
|
296
|
+
existing = JSON.parse(readFileSync(sourceFilePath, "utf-8"));
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
let newKeys = 0;
|
|
301
|
+
for (const [key, value] of Object.entries(strings)) {
|
|
302
|
+
if (!(key in existing)) {
|
|
303
|
+
existing[key] = value;
|
|
304
|
+
newKeys++;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const sorted = Object.fromEntries(
|
|
308
|
+
Object.entries(existing).sort(([a], [b]) => a.localeCompare(b))
|
|
309
|
+
);
|
|
310
|
+
writeFileSync(sourceFilePath, JSON.stringify(sorted, null, 2) + "\n");
|
|
311
|
+
console.log(
|
|
312
|
+
`Extracted ${total} strings (${newKeys} new) \u2192 ${relative(root, sourceFilePath)}`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
async function runTranslate() {
|
|
316
|
+
const config = await loadConfig();
|
|
317
|
+
const root = process.cwd();
|
|
318
|
+
const sourceLocale = config.sourceLocale || "en";
|
|
319
|
+
const targetLocales = config.targetLocales;
|
|
320
|
+
const localesDir = resolve(config.localesDir || "./src/locales");
|
|
321
|
+
const batchSize = config.batchSize || 50;
|
|
322
|
+
if (!targetLocales || targetLocales.length === 0) {
|
|
323
|
+
console.error("No targetLocales configured.");
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
const model = await createModel(config);
|
|
327
|
+
await translateLocaleFiles(
|
|
328
|
+
model,
|
|
329
|
+
localesDir,
|
|
330
|
+
sourceLocale,
|
|
331
|
+
targetLocales,
|
|
332
|
+
batchSize,
|
|
333
|
+
config.systemPrompt
|
|
334
|
+
);
|
|
335
|
+
if (config.files) {
|
|
336
|
+
const { glob } = await import("glob");
|
|
337
|
+
for (const [format, opts] of Object.entries(config.files)) {
|
|
338
|
+
if (!opts?.include?.length) continue;
|
|
339
|
+
for (const pattern of opts.include) {
|
|
340
|
+
if (!pattern.includes("[locale]")) {
|
|
341
|
+
console.warn(
|
|
342
|
+
`Pattern "${pattern}" missing [locale] placeholder, skipping`
|
|
343
|
+
);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const sourcePattern = pattern.replace("[locale]", sourceLocale);
|
|
347
|
+
const files = await glob(sourcePattern, {
|
|
348
|
+
cwd: root,
|
|
349
|
+
absolute: true
|
|
350
|
+
});
|
|
351
|
+
for (const file of files) {
|
|
352
|
+
const content = readFileSync(file, "utf-8");
|
|
353
|
+
for (const targetLocale of targetLocales) {
|
|
354
|
+
const targetPath = resolve(
|
|
355
|
+
root,
|
|
356
|
+
pattern.replace("[locale]", targetLocale).replace(
|
|
357
|
+
basename(file),
|
|
358
|
+
basename(file)
|
|
359
|
+
)
|
|
360
|
+
);
|
|
361
|
+
const actualTarget = file.replace(
|
|
362
|
+
`/${sourceLocale}/`,
|
|
363
|
+
`/${targetLocale}/`
|
|
364
|
+
);
|
|
365
|
+
if (format === "json") {
|
|
366
|
+
try {
|
|
367
|
+
const sourceDict = JSON.parse(content);
|
|
368
|
+
const translated = await translateBatch(
|
|
369
|
+
model,
|
|
370
|
+
sourceDict,
|
|
371
|
+
targetLocale,
|
|
372
|
+
sourceLocale,
|
|
373
|
+
config.systemPrompt
|
|
374
|
+
);
|
|
375
|
+
mkdirSync(dirname(actualTarget), { recursive: true });
|
|
376
|
+
writeFileSync(
|
|
377
|
+
actualTarget,
|
|
378
|
+
JSON.stringify(translated, null, 2) + "\n"
|
|
379
|
+
);
|
|
380
|
+
console.log(
|
|
381
|
+
`${format}: ${relative(root, actualTarget)}`
|
|
382
|
+
);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
console.error(
|
|
385
|
+
`Failed to translate ${relative(root, file)}:`,
|
|
386
|
+
err
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
try {
|
|
391
|
+
const translated = await translateMarkdown(
|
|
392
|
+
model,
|
|
393
|
+
content,
|
|
394
|
+
targetLocale,
|
|
395
|
+
sourceLocale,
|
|
396
|
+
config.systemPrompt
|
|
397
|
+
);
|
|
398
|
+
mkdirSync(dirname(actualTarget), { recursive: true });
|
|
399
|
+
writeFileSync(actualTarget, translated);
|
|
400
|
+
console.log(
|
|
401
|
+
`${format}: ${relative(root, actualTarget)}`
|
|
402
|
+
);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
console.error(
|
|
405
|
+
`Failed to translate ${relative(root, file)}:`,
|
|
406
|
+
err
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
console.log("\nTranslation complete.");
|
|
416
|
+
}
|
|
417
|
+
async function translateLocaleFiles(model, localesDir, sourceLocale, targetLocales, batchSize, systemPrompt) {
|
|
418
|
+
const sourceFilePath = join(localesDir, `${sourceLocale}.json`);
|
|
419
|
+
if (!existsSync(sourceFilePath)) {
|
|
420
|
+
console.log(
|
|
421
|
+
"No source locale file found. Run `solid-translate extract` first."
|
|
422
|
+
);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const sourceDict = JSON.parse(
|
|
426
|
+
readFileSync(sourceFilePath, "utf-8")
|
|
427
|
+
);
|
|
428
|
+
const lockFilePath = join(localesDir, ".solid-translate.lock");
|
|
429
|
+
let lock = { version: 1, sourceLocale, keys: {} };
|
|
430
|
+
if (existsSync(lockFilePath)) {
|
|
431
|
+
try {
|
|
432
|
+
lock = JSON.parse(readFileSync(lockFilePath, "utf-8"));
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const changedKeys = {};
|
|
437
|
+
for (const [key, value] of Object.entries(sourceDict)) {
|
|
438
|
+
const hash = hashContent(value);
|
|
439
|
+
const existing = lock.keys[key];
|
|
440
|
+
if (!existing || existing.hash !== hash) {
|
|
441
|
+
changedKeys[key] = value;
|
|
442
|
+
lock.keys[key] = { hash, source: value };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const key of Object.keys(lock.keys)) {
|
|
446
|
+
if (!(key in sourceDict)) {
|
|
447
|
+
delete lock.keys[key];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (Object.keys(changedKeys).length === 0) {
|
|
451
|
+
console.log("No changes detected in locale files.");
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const count = Object.keys(changedKeys).length;
|
|
455
|
+
console.log(
|
|
456
|
+
`Translating ${count} key${count > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
|
|
457
|
+
);
|
|
458
|
+
for (const targetLocale of targetLocales) {
|
|
459
|
+
const targetFilePath = join(localesDir, `${targetLocale}.json`);
|
|
460
|
+
let existing = {};
|
|
461
|
+
if (existsSync(targetFilePath)) {
|
|
462
|
+
try {
|
|
463
|
+
existing = JSON.parse(readFileSync(targetFilePath, "utf-8"));
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const entries = Object.entries(changedKeys);
|
|
468
|
+
for (let i = 0; i < entries.length; i += batchSize) {
|
|
469
|
+
const batch = Object.fromEntries(entries.slice(i, i + batchSize));
|
|
470
|
+
try {
|
|
471
|
+
const translated = await translateBatch(
|
|
472
|
+
model,
|
|
473
|
+
batch,
|
|
474
|
+
targetLocale,
|
|
475
|
+
sourceLocale,
|
|
476
|
+
systemPrompt
|
|
477
|
+
);
|
|
478
|
+
Object.assign(existing, translated);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error(
|
|
481
|
+
`Failed to translate batch for ${targetLocale}:`,
|
|
482
|
+
err
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
for (const key of Object.keys(existing)) {
|
|
487
|
+
if (!(key in sourceDict)) {
|
|
488
|
+
delete existing[key];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const sorted = Object.fromEntries(
|
|
492
|
+
Object.entries(existing).sort(([a], [b]) => a.localeCompare(b))
|
|
493
|
+
);
|
|
494
|
+
writeFileSync(targetFilePath, JSON.stringify(sorted, null, 2) + "\n");
|
|
495
|
+
console.log(` ${targetLocale}: ${Object.keys(sorted).length} keys`);
|
|
496
|
+
}
|
|
497
|
+
writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
|
|
498
|
+
}
|
|
499
|
+
main().catch((err) => {
|
|
500
|
+
console.error(err);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
});
|