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/README.md +161 -6
- package/dist/chunk-2BKJUY37.js +82 -0
- package/dist/cli.js +351 -163
- package/dist/index.d.ts +33 -5
- package/dist/index.js +65 -9
- package/dist/index.js.map +1 -1
- package/dist/translate-M737VQHG.js +10 -0
- package/dist/vite.js +262 -113
- package/dist/vite.js.map +1 -1
- package/package.json +6 -2
- package/virtual.d.ts +45 -0
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
296
|
+
if (existsSync2(path)) {
|
|
205
297
|
if (name.endsWith(".json")) {
|
|
206
|
-
return JSON.parse(
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
385
|
+
const sourceFilePath = join2(localesDir, `${sourceLocale}.json`);
|
|
294
386
|
let existing = {};
|
|
295
|
-
if (
|
|
387
|
+
if (existsSync2(sourceFilePath)) {
|
|
296
388
|
try {
|
|
297
|
-
existing = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
420
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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);
|