solid-translate 1.1.0 → 1.1.1
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 +209 -97
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/vite.js +209 -113
- package/dist/vite.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
#!/usr/bin/env node
|
|
3
2
|
import "./chunk-FYS2JH42.js";
|
|
4
3
|
|
|
5
4
|
// src/cli.ts
|
|
6
5
|
import {
|
|
7
|
-
readFileSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
existsSync,
|
|
6
|
+
readFileSync as readFileSync2,
|
|
7
|
+
writeFileSync as writeFileSync2,
|
|
8
|
+
existsSync as existsSync2,
|
|
10
9
|
mkdirSync
|
|
11
10
|
} from "fs";
|
|
12
|
-
import { resolve, join, dirname, relative, basename } from "path";
|
|
13
|
-
|
|
14
|
-
// src/hash.ts
|
|
15
|
-
import { createHash } from "crypto";
|
|
16
|
-
function hashContent(content) {
|
|
17
|
-
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
18
|
-
}
|
|
11
|
+
import { resolve, join as join2, dirname, relative, basename } from "path";
|
|
19
12
|
|
|
20
13
|
// src/translate.ts
|
|
21
|
-
import { generateObject } from "ai";
|
|
22
14
|
import { z } from "zod";
|
|
15
|
+
async function loadGenerateObject() {
|
|
16
|
+
const { generateObject } = await import("ai");
|
|
17
|
+
return generateObject;
|
|
18
|
+
}
|
|
23
19
|
async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts) {
|
|
24
20
|
const keys = Object.keys(entries);
|
|
25
21
|
if (keys.length === 0) return {};
|
|
@@ -45,6 +41,7 @@ async function translateBatch(model, entries, targetLocale, sourceLocale, system
|
|
|
45
41
|
].join("\n");
|
|
46
42
|
}
|
|
47
43
|
}
|
|
44
|
+
const generateObject = await loadGenerateObject();
|
|
48
45
|
const { object } = await generateObject({
|
|
49
46
|
model,
|
|
50
47
|
schema: z.object({
|
|
@@ -72,6 +69,7 @@ async function translateMarkdown(model, content, targetLocale, sourceLocale, sys
|
|
|
72
69
|
`- Preserve URLs and file paths unchanged`,
|
|
73
70
|
`- Return natural, idiomatic translations`
|
|
74
71
|
].join("\n");
|
|
72
|
+
const generateObject = await loadGenerateObject();
|
|
75
73
|
const { object } = await generateObject({
|
|
76
74
|
model,
|
|
77
75
|
schema: z.object({
|
|
@@ -127,6 +125,169 @@ function extractStringsFromSource(code, filePath) {
|
|
|
127
125
|
return results;
|
|
128
126
|
}
|
|
129
127
|
|
|
128
|
+
// src/lock.ts
|
|
129
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
130
|
+
import { join } from "path";
|
|
131
|
+
|
|
132
|
+
// src/hash.ts
|
|
133
|
+
import { createHash } from "crypto";
|
|
134
|
+
function hashContent(content) {
|
|
135
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/lock.ts
|
|
139
|
+
function diffLock(sourceDict, lock, contexts) {
|
|
140
|
+
const changedKeys = {};
|
|
141
|
+
const pendingEntries = {};
|
|
142
|
+
for (const [key, value] of Object.entries(sourceDict)) {
|
|
143
|
+
const hash = hashContent(value);
|
|
144
|
+
const existing = lock.keys[key];
|
|
145
|
+
const newContext = contexts ? contexts[key] : existing?.context;
|
|
146
|
+
const contextChanged = contexts !== void 0 && existing?.context !== contexts[key];
|
|
147
|
+
if (!existing || existing.hash !== hash || contextChanged) {
|
|
148
|
+
changedKeys[key] = value;
|
|
149
|
+
pendingEntries[key] = { hash, source: value, context: newContext };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const deletedKeys = Object.keys(lock.keys).filter(
|
|
153
|
+
(key) => !(key in sourceDict)
|
|
154
|
+
);
|
|
155
|
+
return { changedKeys, pendingEntries, deletedKeys };
|
|
156
|
+
}
|
|
157
|
+
async function syncLocaleFiles(options) {
|
|
158
|
+
const {
|
|
159
|
+
localesDir,
|
|
160
|
+
sourceLocale,
|
|
161
|
+
targetLocales,
|
|
162
|
+
batchSize,
|
|
163
|
+
translate,
|
|
164
|
+
contexts,
|
|
165
|
+
log = () => {
|
|
166
|
+
}
|
|
167
|
+
} = options;
|
|
168
|
+
const sourceFilePath = join(localesDir, `${sourceLocale}.json`);
|
|
169
|
+
if (!existsSync(sourceFilePath)) {
|
|
170
|
+
return {
|
|
171
|
+
status: "no-source",
|
|
172
|
+
translatedKeys: [],
|
|
173
|
+
deletedKeys: [],
|
|
174
|
+
failures: []
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const sourceDict = JSON.parse(
|
|
178
|
+
readFileSync(sourceFilePath, "utf-8")
|
|
179
|
+
);
|
|
180
|
+
const lockFilePath = join(localesDir, ".solid-translate.lock");
|
|
181
|
+
let lock = { version: 1, sourceLocale, keys: {} };
|
|
182
|
+
if (existsSync(lockFilePath)) {
|
|
183
|
+
try {
|
|
184
|
+
lock = JSON.parse(readFileSync(lockFilePath, "utf-8"));
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const { changedKeys, pendingEntries, deletedKeys } = diffLock(
|
|
189
|
+
sourceDict,
|
|
190
|
+
lock,
|
|
191
|
+
contexts
|
|
192
|
+
);
|
|
193
|
+
for (const key of deletedKeys) {
|
|
194
|
+
delete lock.keys[key];
|
|
195
|
+
}
|
|
196
|
+
const changedCount = Object.keys(changedKeys).length;
|
|
197
|
+
if (changedCount === 0 && deletedKeys.length === 0) {
|
|
198
|
+
log("No changes detected in locale files.");
|
|
199
|
+
return {
|
|
200
|
+
status: "no-changes",
|
|
201
|
+
translatedKeys: [],
|
|
202
|
+
deletedKeys: [],
|
|
203
|
+
failures: []
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (changedCount === 0) {
|
|
207
|
+
for (const targetLocale of targetLocales) {
|
|
208
|
+
const targetFilePath = join(localesDir, `${targetLocale}.json`);
|
|
209
|
+
const existing = readTargetFile(targetFilePath);
|
|
210
|
+
writeTargetFile(targetFilePath, existing, sourceDict);
|
|
211
|
+
log(` ${targetLocale}: pruned deleted keys`);
|
|
212
|
+
}
|
|
213
|
+
writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
|
|
214
|
+
log(
|
|
215
|
+
`Removed ${deletedKeys.length} deleted key${deletedKeys.length > 1 ? "s" : ""} from target locales.`
|
|
216
|
+
);
|
|
217
|
+
return { status: "synced", translatedKeys: [], deletedKeys, failures: [] };
|
|
218
|
+
}
|
|
219
|
+
log(
|
|
220
|
+
`Translating ${changedCount} key${changedCount > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
|
|
221
|
+
);
|
|
222
|
+
const changedContexts = {};
|
|
223
|
+
for (const key of Object.keys(changedKeys)) {
|
|
224
|
+
const ctx = pendingEntries[key]?.context;
|
|
225
|
+
if (ctx) changedContexts[key] = ctx;
|
|
226
|
+
}
|
|
227
|
+
const failures = [];
|
|
228
|
+
const failedKeys = /* @__PURE__ */ new Set();
|
|
229
|
+
for (const targetLocale of targetLocales) {
|
|
230
|
+
const targetFilePath = join(localesDir, `${targetLocale}.json`);
|
|
231
|
+
const existing = readTargetFile(targetFilePath);
|
|
232
|
+
const entries = Object.entries(changedKeys);
|
|
233
|
+
for (let i = 0; i < entries.length; i += batchSize) {
|
|
234
|
+
const batch = Object.fromEntries(entries.slice(i, i + batchSize));
|
|
235
|
+
try {
|
|
236
|
+
const translated = await translate(
|
|
237
|
+
batch,
|
|
238
|
+
targetLocale,
|
|
239
|
+
changedContexts
|
|
240
|
+
);
|
|
241
|
+
Object.assign(existing, translated);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
failures.push({
|
|
244
|
+
locale: targetLocale,
|
|
245
|
+
keys: Object.keys(batch),
|
|
246
|
+
error: err
|
|
247
|
+
});
|
|
248
|
+
for (const key of Object.keys(batch)) {
|
|
249
|
+
failedKeys.add(key);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
writeTargetFile(targetFilePath, existing, sourceDict);
|
|
254
|
+
log(` ${targetLocale}: ${Object.keys(existing).length} keys`);
|
|
255
|
+
}
|
|
256
|
+
const translatedKeys = [];
|
|
257
|
+
for (const [key, entry] of Object.entries(pendingEntries)) {
|
|
258
|
+
if (failedKeys.has(key)) continue;
|
|
259
|
+
lock.keys[key] = entry;
|
|
260
|
+
translatedKeys.push(key);
|
|
261
|
+
}
|
|
262
|
+
writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
|
|
263
|
+
return { status: "synced", translatedKeys, deletedKeys, failures };
|
|
264
|
+
}
|
|
265
|
+
function formatSyncFailures(failures) {
|
|
266
|
+
return failures.map((failure) => {
|
|
267
|
+
const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
|
|
268
|
+
return `${failure.locale}: ${failure.keys.length} key${failure.keys.length > 1 ? "s" : ""} [${failure.keys.join(", ")}] \u2014 ${message}`;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function readTargetFile(targetFilePath) {
|
|
272
|
+
if (!existsSync(targetFilePath)) return {};
|
|
273
|
+
try {
|
|
274
|
+
return JSON.parse(readFileSync(targetFilePath, "utf-8"));
|
|
275
|
+
} catch {
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function writeTargetFile(targetFilePath, translations, sourceDict) {
|
|
280
|
+
for (const key of Object.keys(translations)) {
|
|
281
|
+
if (!(key in sourceDict)) {
|
|
282
|
+
delete translations[key];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const sorted = Object.fromEntries(
|
|
286
|
+
Object.entries(translations).sort(([a], [b]) => a.localeCompare(b))
|
|
287
|
+
);
|
|
288
|
+
writeFileSync(targetFilePath, JSON.stringify(sorted, null, 2) + "\n");
|
|
289
|
+
}
|
|
290
|
+
|
|
130
291
|
// src/cli.ts
|
|
131
292
|
var CONFIG_FILENAMES = [
|
|
132
293
|
"solid-translate.config.json",
|
|
@@ -176,7 +337,7 @@ Environment variables:
|
|
|
176
337
|
}
|
|
177
338
|
async function initConfig() {
|
|
178
339
|
const configPath = resolve("solid-translate.config.json");
|
|
179
|
-
if (
|
|
340
|
+
if (existsSync2(configPath)) {
|
|
180
341
|
console.log("Config already exists: solid-translate.config.json");
|
|
181
342
|
return;
|
|
182
343
|
}
|
|
@@ -194,16 +355,16 @@ async function initConfig() {
|
|
|
194
355
|
mdx: { include: [] }
|
|
195
356
|
}
|
|
196
357
|
};
|
|
197
|
-
|
|
358
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
198
359
|
console.log("Created solid-translate.config.json");
|
|
199
360
|
console.log("Edit it to set your target locales and AI provider.");
|
|
200
361
|
}
|
|
201
362
|
async function loadConfig() {
|
|
202
363
|
for (const name of CONFIG_FILENAMES) {
|
|
203
364
|
const path = resolve(name);
|
|
204
|
-
if (
|
|
365
|
+
if (existsSync2(path)) {
|
|
205
366
|
if (name.endsWith(".json")) {
|
|
206
|
-
return JSON.parse(
|
|
367
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
207
368
|
}
|
|
208
369
|
const mod = await import(path);
|
|
209
370
|
return mod.default || mod;
|
|
@@ -267,7 +428,7 @@ async function runExtract() {
|
|
|
267
428
|
"src/**/*.ts",
|
|
268
429
|
"src/**/*.jsx"
|
|
269
430
|
];
|
|
270
|
-
if (!
|
|
431
|
+
if (!existsSync2(localesDir)) {
|
|
271
432
|
mkdirSync(localesDir, { recursive: true });
|
|
272
433
|
}
|
|
273
434
|
const { glob } = await import("glob");
|
|
@@ -277,7 +438,7 @@ async function runExtract() {
|
|
|
277
438
|
const files = await glob(pattern, { cwd: root, absolute: true });
|
|
278
439
|
for (const file of files) {
|
|
279
440
|
try {
|
|
280
|
-
const code =
|
|
441
|
+
const code = readFileSync2(file, "utf-8");
|
|
281
442
|
const extracted = extractStringsFromSource(
|
|
282
443
|
code,
|
|
283
444
|
relative(root, file)
|
|
@@ -290,11 +451,11 @@ async function runExtract() {
|
|
|
290
451
|
}
|
|
291
452
|
}
|
|
292
453
|
}
|
|
293
|
-
const sourceFilePath =
|
|
454
|
+
const sourceFilePath = join2(localesDir, `${sourceLocale}.json`);
|
|
294
455
|
let existing = {};
|
|
295
|
-
if (
|
|
456
|
+
if (existsSync2(sourceFilePath)) {
|
|
296
457
|
try {
|
|
297
|
-
existing = JSON.parse(
|
|
458
|
+
existing = JSON.parse(readFileSync2(sourceFilePath, "utf-8"));
|
|
298
459
|
} catch {
|
|
299
460
|
}
|
|
300
461
|
}
|
|
@@ -308,7 +469,7 @@ async function runExtract() {
|
|
|
308
469
|
const sorted = Object.fromEntries(
|
|
309
470
|
Object.entries(existing).sort(([a], [b]) => a.localeCompare(b))
|
|
310
471
|
);
|
|
311
|
-
|
|
472
|
+
writeFileSync2(sourceFilePath, JSON.stringify(sorted, null, 2) + "\n");
|
|
312
473
|
console.log(
|
|
313
474
|
`Extracted ${total} strings (${newKeys} new) \u2192 ${relative(root, sourceFilePath)}`
|
|
314
475
|
);
|
|
@@ -350,7 +511,7 @@ async function runTranslate() {
|
|
|
350
511
|
absolute: true
|
|
351
512
|
});
|
|
352
513
|
for (const file of files) {
|
|
353
|
-
const content =
|
|
514
|
+
const content = readFileSync2(file, "utf-8");
|
|
354
515
|
for (const targetLocale of targetLocales) {
|
|
355
516
|
const targetPath = resolve(
|
|
356
517
|
root,
|
|
@@ -374,7 +535,7 @@ async function runTranslate() {
|
|
|
374
535
|
config.systemPrompt
|
|
375
536
|
);
|
|
376
537
|
mkdirSync(dirname(actualTarget), { recursive: true });
|
|
377
|
-
|
|
538
|
+
writeFileSync2(
|
|
378
539
|
actualTarget,
|
|
379
540
|
JSON.stringify(translated, null, 2) + "\n"
|
|
380
541
|
);
|
|
@@ -397,7 +558,7 @@ async function runTranslate() {
|
|
|
397
558
|
config.systemPrompt
|
|
398
559
|
);
|
|
399
560
|
mkdirSync(dirname(actualTarget), { recursive: true });
|
|
400
|
-
|
|
561
|
+
writeFileSync2(actualTarget, translated);
|
|
401
562
|
console.log(
|
|
402
563
|
`${format}: ${relative(root, actualTarget)}`
|
|
403
564
|
);
|
|
@@ -416,86 +577,37 @@ async function runTranslate() {
|
|
|
416
577
|
console.log("\nTranslation complete.");
|
|
417
578
|
}
|
|
418
579
|
async function translateLocaleFiles(model, localesDir, sourceLocale, targetLocales, batchSize, systemPrompt) {
|
|
419
|
-
const
|
|
420
|
-
|
|
580
|
+
const result = await syncLocaleFiles({
|
|
581
|
+
localesDir,
|
|
582
|
+
sourceLocale,
|
|
583
|
+
targetLocales,
|
|
584
|
+
batchSize,
|
|
585
|
+
translate: (batch, targetLocale, contexts) => translateBatch(
|
|
586
|
+
model,
|
|
587
|
+
batch,
|
|
588
|
+
targetLocale,
|
|
589
|
+
sourceLocale,
|
|
590
|
+
systemPrompt,
|
|
591
|
+
contexts
|
|
592
|
+
),
|
|
593
|
+
log: (message) => console.log(message)
|
|
594
|
+
});
|
|
595
|
+
if (result.status === "no-source") {
|
|
421
596
|
console.log(
|
|
422
597
|
"No source locale file found. Run `solid-translate extract` first."
|
|
423
598
|
);
|
|
424
599
|
return;
|
|
425
600
|
}
|
|
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
|
-
}
|
|
601
|
+
if (result.failures.length > 0) {
|
|
602
|
+
console.error("\nTranslation failed for some batches:");
|
|
603
|
+
for (const line of formatSyncFailures(result.failures)) {
|
|
604
|
+
console.error(` ${line}`);
|
|
467
605
|
}
|
|
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))
|
|
606
|
+
console.error(
|
|
607
|
+
"Failed keys were not recorded in the lock file \u2014 fix the error and rerun `solid-translate translate` to retry them."
|
|
494
608
|
);
|
|
495
|
-
|
|
496
|
-
console.log(` ${targetLocale}: ${Object.keys(sorted).length} keys`);
|
|
609
|
+
process.exit(1);
|
|
497
610
|
}
|
|
498
|
-
writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
|
|
499
611
|
}
|
|
500
612
|
main().catch((err) => {
|
|
501
613
|
console.error(err);
|
package/dist/index.js
CHANGED
|
@@ -18,13 +18,19 @@ function detectLocale(availableLocales) {
|
|
|
18
18
|
if (!availableLocales || availableLocales.length === 0) {
|
|
19
19
|
return normalizeLocale(browserLocales[0] || "en");
|
|
20
20
|
}
|
|
21
|
+
const canonical = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const al of availableLocales) {
|
|
23
|
+
const normalized = normalizeLocale(al);
|
|
24
|
+
if (!canonical.has(normalized)) canonical.set(normalized, al);
|
|
25
|
+
}
|
|
21
26
|
for (const bl of browserLocales) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
27
|
+
const match = canonical.get(normalizeLocale(bl));
|
|
28
|
+
if (match) return match;
|
|
24
29
|
}
|
|
25
30
|
for (const bl of browserLocales) {
|
|
26
|
-
const lang = bl.split("-")[0]
|
|
27
|
-
|
|
31
|
+
const lang = normalizeLocale(bl).split("-")[0];
|
|
32
|
+
const match = canonical.get(lang);
|
|
33
|
+
if (match) return match;
|
|
28
34
|
}
|
|
29
35
|
return availableLocales[0] || "en";
|
|
30
36
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/context.ts","../src/locale-detect.ts","../src/components.tsx","../src/msg.ts"],"sourcesContent":["import {\n createComponent,\n useContext,\n createSignal,\n createMemo,\n children as resolveChildren,\n type JSX,\n} from \"solid-js\";\nimport {\n TranslationContext,\n type TranslationContextValue,\n} from \"./context.js\";\nimport { detectLocale } from \"./locale-detect.js\";\nimport type { TranslationDictionary, Translations } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { TranslationContextValue } from \"./context.js\";\nexport type { TranslationDictionary, Translations } from \"./types.js\";\nexport type { SolidTranslatePluginConfig } from \"./types.js\";\nexport { Var, Num, Currency, DateTime, Plural, LocaleSelector } from \"./components.js\";\nexport type {\n VarProps,\n NumProps,\n CurrencyProps,\n DateTimeProps,\n PluralProps,\n LocaleSelectorProps,\n} from \"./components.js\";\nexport { msg } from \"./msg.js\";\nexport { detectLocale } from \"./locale-detect.js\";\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport interface TranslationProviderProps {\n /**\n * Initial locale. If omitted, auto-detects from the browser's\n * `navigator.languages` header, falling back to `sourceLocale`.\n */\n locale?: string;\n /** Source locale code (default: \"en\") */\n sourceLocale?: string;\n /** Translation dictionaries keyed by locale */\n translations: Translations;\n children: JSX.Element;\n}\n\nexport function TranslationProvider(props: TranslationProviderProps) {\n const sourceLocale = props.sourceLocale || \"en\";\n const availableLocales = createMemo(() => Object.keys(props.translations));\n\n // Auto-detect locale from browser if not explicitly provided\n const initialLocale =\n props.locale || detectLocale(availableLocales()) || sourceLocale;\n const [locale, setLocale] = createSignal(initialLocale);\n\n const t = (\n key: string,\n params?: Record<string, string | number>,\n ): string => {\n const cur = locale();\n let text = key;\n\n // Look up in translation dictionary (works for both source and target locales)\n const dict = props.translations[cur];\n if (dict && key in dict) {\n text = dict[key]!;\n }\n\n // Interpolate {{variable}} and {variable} placeholders\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n text = text.replace(\n new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}|\\\\{${k}\\\\}`, \"g\"),\n String(v),\n );\n }\n }\n\n return text;\n };\n\n const value: TranslationContextValue = {\n locale,\n setLocale,\n t,\n sourceLocale,\n availableLocales,\n translations: props.translations,\n };\n\n return createComponent(TranslationContext.Provider, {\n value,\n get children() {\n return props.children;\n },\n });\n}\n\n// ---------------------------------------------------------------------------\n// Hooks\n// ---------------------------------------------------------------------------\n\n/** Access the full translation context. Must be inside a TranslationProvider. */\nexport function useTranslation(): TranslationContextValue {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"useTranslation() must be used within a <TranslationProvider>\",\n );\n }\n return ctx;\n}\n\n/** Access just the current locale and setter. */\nexport function useLocale(): {\n locale: () => string;\n setLocale: (locale: string) => void;\n sourceLocale: string;\n availableLocales: () => string[];\n} {\n const ctx = useTranslation();\n return {\n locale: ctx.locale,\n setLocale: ctx.setLocale,\n sourceLocale: ctx.sourceLocale,\n availableLocales: ctx.availableLocales,\n };\n}\n\n// ---------------------------------------------------------------------------\n// <T> Component\n// ---------------------------------------------------------------------------\n\nexport interface TProps {\n /** Explicit translation key. If omitted, children text is used as the key. */\n id?: string;\n /** Interpolation parameters */\n params?: Record<string, string | number>;\n /**\n * AI context hint — tells the AI translator about the meaning of this text.\n * Only used at build time for disambiguation; has no runtime effect.\n *\n * ```tsx\n * <T context=\"Button to save a document, not save money\">Save</T>\n * ```\n */\n context?: string;\n /** Source text / JSX content */\n children?: JSX.Element;\n}\n\n/**\n * Translatable content component.\n *\n * ```tsx\n * <T>Hello world</T>\n * <T id=\"greeting\" params={{ name: \"Alice\" }}>Hello {{name}}</T>\n * <T context=\"the physical bank\">Bank</T>\n * <T>Welcome <Var>{userName()}</Var>, you have <Num>{count()}</Num> items</T>\n * ```\n */\nexport function T(props: TProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n const resolved = resolveChildren(() => props.children);\n\n return createMemo(() => {\n const kids = resolved.toArray();\n\n // No context — just render children\n if (!ctx) return kids.length === 1 ? kids[0] : kids;\n\n // Simple case: single text child\n if (kids.length === 1 && typeof kids[0] === \"string\") {\n const key = props.id || (kids[0] as string);\n return ctx.t(key, props.params);\n }\n\n // Explicit id with non-text children — translate via id\n if (props.id) {\n const translated = ctx.t(props.id, props.params);\n\n // If translation is just text (no slot placeholders), return it\n if (!/{(\\d+)}/.test(translated)) return translated;\n\n // Collect non-text children (Var, Num, etc.) as ordered slots\n const slots: JSX.Element[] = [];\n for (const kid of kids) {\n if (typeof kid !== \"string\" && typeof kid !== \"number\") {\n slots.push(kid as JSX.Element);\n }\n }\n\n return interpolateSlots(translated, slots);\n }\n\n // Mixed children without explicit id — build a template key\n const slots: JSX.Element[] = [];\n let template = \"\";\n for (const kid of kids) {\n if (typeof kid === \"string\") {\n template += kid;\n } else if (typeof kid === \"number\") {\n template += String(kid);\n } else {\n template += `{${slots.length}}`;\n slots.push(kid as JSX.Element);\n }\n }\n\n const translated = ctx.t(template, props.params);\n if (slots.length === 0) return translated;\n return interpolateSlots(translated, slots);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Split a translated string by `{0}`, `{1}`, etc. and interleave with slots */\nfunction interpolateSlots(\n text: string,\n slots: JSX.Element[],\n): (string | JSX.Element)[] {\n const parts = text.split(/\\{(\\d+)\\}/);\n const result: (string | JSX.Element)[] = [];\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n if (parts[i]) result.push(parts[i]!);\n } else {\n const idx = parseInt(parts[i]!, 10);\n if (slots[idx] !== undefined) result.push(slots[idx]!);\n }\n }\n return result;\n}\n","import { createContext } from \"solid-js\";\nimport type { Translations } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Context value type\n// ---------------------------------------------------------------------------\n\nexport interface TranslationContextValue {\n /** Current locale as a reactive signal */\n locale: () => string;\n /** Switch to a different locale */\n setLocale: (locale: string) => void;\n /** Translate a key with optional interpolation params */\n t: (key: string, params?: Record<string, string | number>) => string;\n /** The source locale code */\n sourceLocale: string;\n /** All available locale codes (reactive) */\n availableLocales: () => string[];\n /** Raw translations object */\n translations: Translations;\n}\n\n// ---------------------------------------------------------------------------\n// Shared context instance\n// ---------------------------------------------------------------------------\n\nexport const TranslationContext = createContext<TranslationContextValue>();\n","/**\n * Detect the user's preferred locale from browser settings.\n *\n * Checks `navigator.languages` (and falls back to `navigator.language`)\n * then matches against the list of available locales. Tries exact match\n * first, then language-only match (e.g. \"en-US\" → \"en\").\n */\nexport function detectLocale(availableLocales?: string[]): string {\n if (typeof navigator === \"undefined\") return \"en\";\n\n const browserLocales = navigator.languages\n ? [...navigator.languages]\n : [navigator.language || \"en\"];\n\n if (!availableLocales || availableLocales.length === 0) {\n return normalizeLocale(browserLocales[0] || \"en\");\n }\n\n // Exact match\n for (const bl of browserLocales) {\n const normalized = normalizeLocale(bl);\n if (availableLocales.includes(normalized)) return normalized;\n }\n\n // Language-only match (e.g. \"en-US\" → \"en\")\n for (const bl of browserLocales) {\n const lang = bl.split(\"-\")[0]!.toLowerCase();\n if (availableLocales.includes(lang)) return lang;\n }\n\n return availableLocales[0] || \"en\";\n}\n\nfunction normalizeLocale(locale: string): string {\n return locale.toLowerCase().replace(\"_\", \"-\");\n}\n","import { useContext, type JSX, For, createMemo } from \"solid-js\";\nimport { TranslationContext } from \"./context.js\";\n\n// ---------------------------------------------------------------------------\n// <Var> — protect dynamic content from translation\n// ---------------------------------------------------------------------------\n\nexport interface VarProps {\n /** Optional name for the variable (used as placeholder in templates) */\n name?: string;\n children: JSX.Element;\n}\n\n/**\n * Marks content as untranslatable. When used inside `<T>`, the content\n * is preserved as-is while surrounding text is translated.\n *\n * ```tsx\n * <T>Hello <Var>{userName()}</Var>, welcome!</T>\n * ```\n */\nexport function Var(props: VarProps): JSX.Element {\n return (() => props.children) as unknown as JSX.Element;\n}\n\n// Mark Var for identification by T component\n(Var as any).__st_var = true;\n\n// ---------------------------------------------------------------------------\n// <Num> — locale-aware number formatting\n// ---------------------------------------------------------------------------\n\nexport interface NumProps {\n /** The number to format */\n children: number;\n /** Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number according to the current locale using `Intl.NumberFormat`.\n *\n * ```tsx\n * <Num>{1000000}</Num> // \"1,000,000\" in en, \"1.000.000\" in de\n * <Num options={{ style: \"percent\" }}>{0.42}</Num> // \"42%\"\n * ```\n */\nexport function Num(props: NumProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, props.options).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Currency> — locale-aware currency formatting\n// ---------------------------------------------------------------------------\n\nexport interface CurrencyProps {\n /** The numeric value */\n children: number;\n /** ISO 4217 currency code (e.g. \"USD\", \"EUR\") */\n currency: string;\n /** Additional Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number as currency according to the current locale.\n *\n * ```tsx\n * <Currency currency=\"USD\">{29.99}</Currency> // \"$29.99\" in en-US\n * <Currency currency=\"EUR\">{29.99}</Currency> // \"29,99 €\" in de\n * ```\n */\nexport function Currency(props: CurrencyProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: props.currency,\n ...props.options,\n }).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <DateTime> — locale-aware date/time formatting\n// ---------------------------------------------------------------------------\n\nexport interface DateTimeProps {\n /** The date to format (Date object, timestamp, or ISO string) */\n children: Date | number | string;\n /** Intl.DateTimeFormat options */\n options?: Intl.DateTimeFormatOptions;\n}\n\n/**\n * Formats a date/time according to the current locale using `Intl.DateTimeFormat`.\n *\n * ```tsx\n * <DateTime>{new Date()}</DateTime>\n * <DateTime options={{ dateStyle: \"long\" }}>{new Date()}</DateTime>\n * ```\n */\nexport function DateTime(props: DateTimeProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const date =\n props.children instanceof Date\n ? props.children\n : new Date(props.children);\n return new Intl.DateTimeFormat(locale, props.options).format(date);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Plural> — CLDR plural rules\n// ---------------------------------------------------------------------------\n\nexport interface PluralProps {\n /** The count value to determine which plural form to use */\n n: number;\n /** Form for zero items */\n zero?: JSX.Element;\n /** Form for exactly one item */\n one?: JSX.Element;\n /** Form for exactly two items */\n two?: JSX.Element;\n /** Form for \"few\" items (language-dependent) */\n few?: JSX.Element;\n /** Form for \"many\" items (language-dependent) */\n many?: JSX.Element;\n /** Default/fallback form */\n other: JSX.Element;\n}\n\n/**\n * Renders the appropriate plural form based on CLDR plural rules for the current locale.\n *\n * ```tsx\n * <Plural n={count()}\n * zero=\"No items\"\n * one=\"1 item\"\n * other={`${count()} items`}\n * />\n * ```\n */\nexport function Plural(props: PluralProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const rules = new Intl.PluralRules(locale);\n const category = rules.select(props.n);\n\n const forms: Record<string, JSX.Element | undefined> = {\n zero: props.zero,\n one: props.one,\n two: props.two,\n few: props.few,\n many: props.many,\n other: props.other,\n };\n\n return forms[category] ?? props.other;\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <LocaleSelector> — drop-in locale picker\n// ---------------------------------------------------------------------------\n\nexport interface LocaleSelectorProps {\n /** Override which locales to show (defaults to all available) */\n locales?: string[];\n /** Map locale codes to display names, e.g. { en: \"English\", es: \"Español\" } */\n labels?: Record<string, string>;\n /** Additional CSS class */\n class?: string;\n}\n\n/**\n * A ready-to-use locale selector dropdown.\n *\n * ```tsx\n * <LocaleSelector labels={{ en: \"English\", es: \"Español\", fr: \"Français\" }} />\n * ```\n */\nexport function LocaleSelector(props: LocaleSelectorProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"<LocaleSelector> must be used within a <TranslationProvider>\",\n );\n }\n\n const locales = createMemo(() => props.locales || ctx.availableLocales());\n\n const displayName = (code: string): string => {\n if (props.labels?.[code]) return props.labels[code]!;\n try {\n const dn = new Intl.DisplayNames([code], { type: \"language\" });\n return dn.of(code) || code;\n } catch {\n return code;\n }\n };\n\n return (\n <select\n class={props.class}\n value={ctx.locale()}\n onChange={(e) => ctx.setLocale(e.currentTarget.value)}\n >\n <For each={locales()}>\n {(code) => <option value={code}>{displayName(code)}</option>}\n </For>\n </select>\n ) as JSX.Element;\n}\n","/**\n * Mark a string for translation extraction.\n *\n * At build time, the Vite plugin and CLI scan for `msg()` calls and add\n * the strings to the source locale file for AI translation.\n *\n * At runtime, `msg()` is a no-op — it returns the source text as-is.\n * Use `t()` from `useTranslation()` for runtime translation.\n *\n * ```ts\n * // Marks \"Save changes\" for extraction\n * const label = msg(\"Save changes\");\n *\n * // With interpolation template\n * const greeting = msg(\"Hello {{name}}\", { name: \"World\" });\n *\n * // In a component, translate at runtime:\n * const { t } = useTranslation();\n * <button>{t(label)}</button>\n * ```\n */\nexport function msg(\n text: string,\n _params?: Record<string, string | number>,\n): string {\n return text;\n}\n"],"mappings":"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","names":["useContext","createMemo","createMemo","useContext","translated","slots"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/context.ts","../src/locale-detect.ts","../src/components.tsx","../src/msg.ts"],"sourcesContent":["import {\n createComponent,\n useContext,\n createSignal,\n createMemo,\n children as resolveChildren,\n type JSX,\n} from \"solid-js\";\nimport {\n TranslationContext,\n type TranslationContextValue,\n} from \"./context.js\";\nimport { detectLocale } from \"./locale-detect.js\";\nimport type { TranslationDictionary, Translations } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { TranslationContextValue } from \"./context.js\";\nexport type { TranslationDictionary, Translations } from \"./types.js\";\nexport type { SolidTranslatePluginConfig } from \"./types.js\";\nexport { Var, Num, Currency, DateTime, Plural, LocaleSelector } from \"./components.js\";\nexport type {\n VarProps,\n NumProps,\n CurrencyProps,\n DateTimeProps,\n PluralProps,\n LocaleSelectorProps,\n} from \"./components.js\";\nexport { msg } from \"./msg.js\";\nexport { detectLocale } from \"./locale-detect.js\";\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport interface TranslationProviderProps {\n /**\n * Initial locale. If omitted, auto-detects from the browser's\n * `navigator.languages` header, falling back to `sourceLocale`.\n */\n locale?: string;\n /** Source locale code (default: \"en\") */\n sourceLocale?: string;\n /** Translation dictionaries keyed by locale */\n translations: Translations;\n children: JSX.Element;\n}\n\nexport function TranslationProvider(props: TranslationProviderProps) {\n const sourceLocale = props.sourceLocale || \"en\";\n const availableLocales = createMemo(() => Object.keys(props.translations));\n\n // Auto-detect locale from browser if not explicitly provided\n const initialLocale =\n props.locale || detectLocale(availableLocales()) || sourceLocale;\n const [locale, setLocale] = createSignal(initialLocale);\n\n const t = (\n key: string,\n params?: Record<string, string | number>,\n ): string => {\n const cur = locale();\n let text = key;\n\n // Look up in translation dictionary (works for both source and target locales)\n const dict = props.translations[cur];\n if (dict && key in dict) {\n text = dict[key]!;\n }\n\n // Interpolate {{variable}} and {variable} placeholders\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n text = text.replace(\n new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}|\\\\{${k}\\\\}`, \"g\"),\n String(v),\n );\n }\n }\n\n return text;\n };\n\n const value: TranslationContextValue = {\n locale,\n setLocale,\n t,\n sourceLocale,\n availableLocales,\n translations: props.translations,\n };\n\n return createComponent(TranslationContext.Provider, {\n value,\n get children() {\n return props.children;\n },\n });\n}\n\n// ---------------------------------------------------------------------------\n// Hooks\n// ---------------------------------------------------------------------------\n\n/** Access the full translation context. Must be inside a TranslationProvider. */\nexport function useTranslation(): TranslationContextValue {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"useTranslation() must be used within a <TranslationProvider>\",\n );\n }\n return ctx;\n}\n\n/** Access just the current locale and setter. */\nexport function useLocale(): {\n locale: () => string;\n setLocale: (locale: string) => void;\n sourceLocale: string;\n availableLocales: () => string[];\n} {\n const ctx = useTranslation();\n return {\n locale: ctx.locale,\n setLocale: ctx.setLocale,\n sourceLocale: ctx.sourceLocale,\n availableLocales: ctx.availableLocales,\n };\n}\n\n// ---------------------------------------------------------------------------\n// <T> Component\n// ---------------------------------------------------------------------------\n\nexport interface TProps {\n /** Explicit translation key. If omitted, children text is used as the key. */\n id?: string;\n /** Interpolation parameters */\n params?: Record<string, string | number>;\n /**\n * AI context hint — tells the AI translator about the meaning of this text.\n * Only used at build time for disambiguation; has no runtime effect.\n *\n * ```tsx\n * <T context=\"Button to save a document, not save money\">Save</T>\n * ```\n */\n context?: string;\n /** Source text / JSX content */\n children?: JSX.Element;\n}\n\n/**\n * Translatable content component.\n *\n * ```tsx\n * <T>Hello world</T>\n * <T id=\"greeting\" params={{ name: \"Alice\" }}>Hello {{name}}</T>\n * <T context=\"the physical bank\">Bank</T>\n * <T>Welcome <Var>{userName()}</Var>, you have <Num>{count()}</Num> items</T>\n * ```\n */\nexport function T(props: TProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n const resolved = resolveChildren(() => props.children);\n\n return createMemo(() => {\n const kids = resolved.toArray();\n\n // No context — just render children\n if (!ctx) return kids.length === 1 ? kids[0] : kids;\n\n // Simple case: single text child\n if (kids.length === 1 && typeof kids[0] === \"string\") {\n const key = props.id || (kids[0] as string);\n return ctx.t(key, props.params);\n }\n\n // Explicit id with non-text children — translate via id\n if (props.id) {\n const translated = ctx.t(props.id, props.params);\n\n // If translation is just text (no slot placeholders), return it\n if (!/{(\\d+)}/.test(translated)) return translated;\n\n // Collect non-text children (Var, Num, etc.) as ordered slots\n const slots: JSX.Element[] = [];\n for (const kid of kids) {\n if (typeof kid !== \"string\" && typeof kid !== \"number\") {\n slots.push(kid as JSX.Element);\n }\n }\n\n return interpolateSlots(translated, slots);\n }\n\n // Mixed children without explicit id — build a template key\n const slots: JSX.Element[] = [];\n let template = \"\";\n for (const kid of kids) {\n if (typeof kid === \"string\") {\n template += kid;\n } else if (typeof kid === \"number\") {\n template += String(kid);\n } else {\n template += `{${slots.length}}`;\n slots.push(kid as JSX.Element);\n }\n }\n\n const translated = ctx.t(template, props.params);\n if (slots.length === 0) return translated;\n return interpolateSlots(translated, slots);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Split a translated string by `{0}`, `{1}`, etc. and interleave with slots */\nfunction interpolateSlots(\n text: string,\n slots: JSX.Element[],\n): (string | JSX.Element)[] {\n const parts = text.split(/\\{(\\d+)\\}/);\n const result: (string | JSX.Element)[] = [];\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n if (parts[i]) result.push(parts[i]!);\n } else {\n const idx = parseInt(parts[i]!, 10);\n if (slots[idx] !== undefined) result.push(slots[idx]!);\n }\n }\n return result;\n}\n","import { createContext } from \"solid-js\";\nimport type { Translations } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Context value type\n// ---------------------------------------------------------------------------\n\nexport interface TranslationContextValue {\n /** Current locale as a reactive signal */\n locale: () => string;\n /** Switch to a different locale */\n setLocale: (locale: string) => void;\n /** Translate a key with optional interpolation params */\n t: (key: string, params?: Record<string, string | number>) => string;\n /** The source locale code */\n sourceLocale: string;\n /** All available locale codes (reactive) */\n availableLocales: () => string[];\n /** Raw translations object */\n translations: Translations;\n}\n\n// ---------------------------------------------------------------------------\n// Shared context instance\n// ---------------------------------------------------------------------------\n\nexport const TranslationContext = createContext<TranslationContextValue>();\n","/**\n * Detect the user's preferred locale from browser settings.\n *\n * Checks `navigator.languages` (and falls back to `navigator.language`)\n * then matches against the list of available locales. Tries exact match\n * first, then language-only match (e.g. \"en-US\" → \"en\").\n */\nexport function detectLocale(availableLocales?: string[]): string {\n if (typeof navigator === \"undefined\") return \"en\";\n\n const browserLocales = navigator.languages\n ? [...navigator.languages]\n : [navigator.language || \"en\"];\n\n if (!availableLocales || availableLocales.length === 0) {\n return normalizeLocale(browserLocales[0] || \"en\");\n }\n\n // Map normalized available locales back to their canonical casing so a\n // browser \"pt-br\" can match an available \"pt-BR\" (and return \"pt-BR\").\n const canonical = new Map<string, string>();\n for (const al of availableLocales) {\n const normalized = normalizeLocale(al);\n if (!canonical.has(normalized)) canonical.set(normalized, al);\n }\n\n // Exact match (case-insensitive)\n for (const bl of browserLocales) {\n const match = canonical.get(normalizeLocale(bl));\n if (match) return match;\n }\n\n // Language-only match (e.g. \"en-US\" → \"en\")\n for (const bl of browserLocales) {\n const lang = normalizeLocale(bl).split(\"-\")[0]!;\n const match = canonical.get(lang);\n if (match) return match;\n }\n\n return availableLocales[0] || \"en\";\n}\n\nfunction normalizeLocale(locale: string): string {\n return locale.toLowerCase().replace(\"_\", \"-\");\n}\n","import { useContext, type JSX, For, createMemo } from \"solid-js\";\nimport { TranslationContext } from \"./context.js\";\n\n// ---------------------------------------------------------------------------\n// <Var> — protect dynamic content from translation\n// ---------------------------------------------------------------------------\n\nexport interface VarProps {\n /** Optional name for the variable (used as placeholder in templates) */\n name?: string;\n children: JSX.Element;\n}\n\n/**\n * Marks content as untranslatable. When used inside `<T>`, the content\n * is preserved as-is while surrounding text is translated.\n *\n * ```tsx\n * <T>Hello <Var>{userName()}</Var>, welcome!</T>\n * ```\n */\nexport function Var(props: VarProps): JSX.Element {\n return (() => props.children) as unknown as JSX.Element;\n}\n\n// Mark Var for identification by T component\n(Var as any).__st_var = true;\n\n// ---------------------------------------------------------------------------\n// <Num> — locale-aware number formatting\n// ---------------------------------------------------------------------------\n\nexport interface NumProps {\n /** The number to format */\n children: number;\n /** Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number according to the current locale using `Intl.NumberFormat`.\n *\n * ```tsx\n * <Num>{1000000}</Num> // \"1,000,000\" in en, \"1.000.000\" in de\n * <Num options={{ style: \"percent\" }}>{0.42}</Num> // \"42%\"\n * ```\n */\nexport function Num(props: NumProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, props.options).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Currency> — locale-aware currency formatting\n// ---------------------------------------------------------------------------\n\nexport interface CurrencyProps {\n /** The numeric value */\n children: number;\n /** ISO 4217 currency code (e.g. \"USD\", \"EUR\") */\n currency: string;\n /** Additional Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number as currency according to the current locale.\n *\n * ```tsx\n * <Currency currency=\"USD\">{29.99}</Currency> // \"$29.99\" in en-US\n * <Currency currency=\"EUR\">{29.99}</Currency> // \"29,99 €\" in de\n * ```\n */\nexport function Currency(props: CurrencyProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: props.currency,\n ...props.options,\n }).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <DateTime> — locale-aware date/time formatting\n// ---------------------------------------------------------------------------\n\nexport interface DateTimeProps {\n /** The date to format (Date object, timestamp, or ISO string) */\n children: Date | number | string;\n /** Intl.DateTimeFormat options */\n options?: Intl.DateTimeFormatOptions;\n}\n\n/**\n * Formats a date/time according to the current locale using `Intl.DateTimeFormat`.\n *\n * ```tsx\n * <DateTime>{new Date()}</DateTime>\n * <DateTime options={{ dateStyle: \"long\" }}>{new Date()}</DateTime>\n * ```\n */\nexport function DateTime(props: DateTimeProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const date =\n props.children instanceof Date\n ? props.children\n : new Date(props.children);\n return new Intl.DateTimeFormat(locale, props.options).format(date);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Plural> — CLDR plural rules\n// ---------------------------------------------------------------------------\n\nexport interface PluralProps {\n /** The count value to determine which plural form to use */\n n: number;\n /** Form for zero items */\n zero?: JSX.Element;\n /** Form for exactly one item */\n one?: JSX.Element;\n /** Form for exactly two items */\n two?: JSX.Element;\n /** Form for \"few\" items (language-dependent) */\n few?: JSX.Element;\n /** Form for \"many\" items (language-dependent) */\n many?: JSX.Element;\n /** Default/fallback form */\n other: JSX.Element;\n}\n\n/**\n * Renders the appropriate plural form based on CLDR plural rules for the current locale.\n *\n * ```tsx\n * <Plural n={count()}\n * zero=\"No items\"\n * one=\"1 item\"\n * other={`${count()} items`}\n * />\n * ```\n */\nexport function Plural(props: PluralProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const rules = new Intl.PluralRules(locale);\n const category = rules.select(props.n);\n\n const forms: Record<string, JSX.Element | undefined> = {\n zero: props.zero,\n one: props.one,\n two: props.two,\n few: props.few,\n many: props.many,\n other: props.other,\n };\n\n return forms[category] ?? props.other;\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <LocaleSelector> — drop-in locale picker\n// ---------------------------------------------------------------------------\n\nexport interface LocaleSelectorProps {\n /** Override which locales to show (defaults to all available) */\n locales?: string[];\n /** Map locale codes to display names, e.g. { en: \"English\", es: \"Español\" } */\n labels?: Record<string, string>;\n /** Additional CSS class */\n class?: string;\n}\n\n/**\n * A ready-to-use locale selector dropdown.\n *\n * ```tsx\n * <LocaleSelector labels={{ en: \"English\", es: \"Español\", fr: \"Français\" }} />\n * ```\n */\nexport function LocaleSelector(props: LocaleSelectorProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"<LocaleSelector> must be used within a <TranslationProvider>\",\n );\n }\n\n const locales = createMemo(() => props.locales || ctx.availableLocales());\n\n const displayName = (code: string): string => {\n if (props.labels?.[code]) return props.labels[code]!;\n try {\n const dn = new Intl.DisplayNames([code], { type: \"language\" });\n return dn.of(code) || code;\n } catch {\n return code;\n }\n };\n\n return (\n <select\n class={props.class}\n value={ctx.locale()}\n onChange={(e) => ctx.setLocale(e.currentTarget.value)}\n >\n <For each={locales()}>\n {(code) => <option value={code}>{displayName(code)}</option>}\n </For>\n </select>\n ) as JSX.Element;\n}\n","/**\n * Mark a string for translation extraction.\n *\n * At build time, the Vite plugin and CLI scan for `msg()` calls and add\n * the strings to the source locale file for AI translation.\n *\n * At runtime, `msg()` is a no-op — it returns the source text as-is.\n * Use `t()` from `useTranslation()` for runtime translation.\n *\n * ```ts\n * // Marks \"Save changes\" for extraction\n * const label = msg(\"Save changes\");\n *\n * // With interpolation template\n * const greeting = msg(\"Hello {{name}}\", { name: \"World\" });\n *\n * // In a component, translate at runtime:\n * const { t } = useTranslation();\n * <button>{t(label)}</button>\n * ```\n */\nexport function msg(\n text: string,\n _params?: Record<string, string | number>,\n): string {\n return text;\n}\n"],"mappings":"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","names":["useContext","createMemo","createMemo","useContext","translated","slots"]}
|
package/dist/vite.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
// src/vite.ts
|
|
2
2
|
import {
|
|
3
|
-
readFileSync,
|
|
4
|
-
writeFileSync,
|
|
5
|
-
existsSync,
|
|
3
|
+
readFileSync as readFileSync2,
|
|
4
|
+
writeFileSync as writeFileSync2,
|
|
5
|
+
existsSync as existsSync2,
|
|
6
6
|
mkdirSync,
|
|
7
7
|
readdirSync
|
|
8
8
|
} from "fs";
|
|
9
|
-
import { resolve, join, relative } from "path";
|
|
10
|
-
|
|
11
|
-
// src/hash.ts
|
|
12
|
-
import { createHash } from "crypto";
|
|
13
|
-
function hashContent(content) {
|
|
14
|
-
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
15
|
-
}
|
|
9
|
+
import { resolve, join as join2, relative } from "path";
|
|
16
10
|
|
|
17
11
|
// src/translate.ts
|
|
18
|
-
import { generateObject } from "ai";
|
|
19
12
|
import { z } from "zod";
|
|
13
|
+
async function loadGenerateObject() {
|
|
14
|
+
const { generateObject } = await import("ai");
|
|
15
|
+
return generateObject;
|
|
16
|
+
}
|
|
20
17
|
async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts) {
|
|
21
18
|
const keys = Object.keys(entries);
|
|
22
19
|
if (keys.length === 0) return {};
|
|
@@ -42,6 +39,7 @@ async function translateBatch(model, entries, targetLocale, sourceLocale, system
|
|
|
42
39
|
].join("\n");
|
|
43
40
|
}
|
|
44
41
|
}
|
|
42
|
+
const generateObject = await loadGenerateObject();
|
|
45
43
|
const { object } = await generateObject({
|
|
46
44
|
model,
|
|
47
45
|
schema: z.object({
|
|
@@ -97,6 +95,169 @@ function extractStringsFromSource(code, filePath) {
|
|
|
97
95
|
return results;
|
|
98
96
|
}
|
|
99
97
|
|
|
98
|
+
// src/lock.ts
|
|
99
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
100
|
+
import { join } from "path";
|
|
101
|
+
|
|
102
|
+
// src/hash.ts
|
|
103
|
+
import { createHash } from "crypto";
|
|
104
|
+
function hashContent(content) {
|
|
105
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/lock.ts
|
|
109
|
+
function diffLock(sourceDict, lock, contexts) {
|
|
110
|
+
const changedKeys = {};
|
|
111
|
+
const pendingEntries = {};
|
|
112
|
+
for (const [key, value] of Object.entries(sourceDict)) {
|
|
113
|
+
const hash = hashContent(value);
|
|
114
|
+
const existing = lock.keys[key];
|
|
115
|
+
const newContext = contexts ? contexts[key] : existing?.context;
|
|
116
|
+
const contextChanged = contexts !== void 0 && existing?.context !== contexts[key];
|
|
117
|
+
if (!existing || existing.hash !== hash || contextChanged) {
|
|
118
|
+
changedKeys[key] = value;
|
|
119
|
+
pendingEntries[key] = { hash, source: value, context: newContext };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const deletedKeys = Object.keys(lock.keys).filter(
|
|
123
|
+
(key) => !(key in sourceDict)
|
|
124
|
+
);
|
|
125
|
+
return { changedKeys, pendingEntries, deletedKeys };
|
|
126
|
+
}
|
|
127
|
+
async function syncLocaleFiles(options) {
|
|
128
|
+
const {
|
|
129
|
+
localesDir,
|
|
130
|
+
sourceLocale,
|
|
131
|
+
targetLocales,
|
|
132
|
+
batchSize,
|
|
133
|
+
translate,
|
|
134
|
+
contexts,
|
|
135
|
+
log = () => {
|
|
136
|
+
}
|
|
137
|
+
} = options;
|
|
138
|
+
const sourceFilePath = join(localesDir, `${sourceLocale}.json`);
|
|
139
|
+
if (!existsSync(sourceFilePath)) {
|
|
140
|
+
return {
|
|
141
|
+
status: "no-source",
|
|
142
|
+
translatedKeys: [],
|
|
143
|
+
deletedKeys: [],
|
|
144
|
+
failures: []
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const sourceDict = JSON.parse(
|
|
148
|
+
readFileSync(sourceFilePath, "utf-8")
|
|
149
|
+
);
|
|
150
|
+
const lockFilePath = join(localesDir, ".solid-translate.lock");
|
|
151
|
+
let lock = { version: 1, sourceLocale, keys: {} };
|
|
152
|
+
if (existsSync(lockFilePath)) {
|
|
153
|
+
try {
|
|
154
|
+
lock = JSON.parse(readFileSync(lockFilePath, "utf-8"));
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const { changedKeys, pendingEntries, deletedKeys } = diffLock(
|
|
159
|
+
sourceDict,
|
|
160
|
+
lock,
|
|
161
|
+
contexts
|
|
162
|
+
);
|
|
163
|
+
for (const key of deletedKeys) {
|
|
164
|
+
delete lock.keys[key];
|
|
165
|
+
}
|
|
166
|
+
const changedCount = Object.keys(changedKeys).length;
|
|
167
|
+
if (changedCount === 0 && deletedKeys.length === 0) {
|
|
168
|
+
log("No changes detected in locale files.");
|
|
169
|
+
return {
|
|
170
|
+
status: "no-changes",
|
|
171
|
+
translatedKeys: [],
|
|
172
|
+
deletedKeys: [],
|
|
173
|
+
failures: []
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (changedCount === 0) {
|
|
177
|
+
for (const targetLocale of targetLocales) {
|
|
178
|
+
const targetFilePath = join(localesDir, `${targetLocale}.json`);
|
|
179
|
+
const existing = readTargetFile(targetFilePath);
|
|
180
|
+
writeTargetFile(targetFilePath, existing, sourceDict);
|
|
181
|
+
log(` ${targetLocale}: pruned deleted keys`);
|
|
182
|
+
}
|
|
183
|
+
writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
|
|
184
|
+
log(
|
|
185
|
+
`Removed ${deletedKeys.length} deleted key${deletedKeys.length > 1 ? "s" : ""} from target locales.`
|
|
186
|
+
);
|
|
187
|
+
return { status: "synced", translatedKeys: [], deletedKeys, failures: [] };
|
|
188
|
+
}
|
|
189
|
+
log(
|
|
190
|
+
`Translating ${changedCount} key${changedCount > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
|
|
191
|
+
);
|
|
192
|
+
const changedContexts = {};
|
|
193
|
+
for (const key of Object.keys(changedKeys)) {
|
|
194
|
+
const ctx = pendingEntries[key]?.context;
|
|
195
|
+
if (ctx) changedContexts[key] = ctx;
|
|
196
|
+
}
|
|
197
|
+
const failures = [];
|
|
198
|
+
const failedKeys = /* @__PURE__ */ new Set();
|
|
199
|
+
for (const targetLocale of targetLocales) {
|
|
200
|
+
const targetFilePath = join(localesDir, `${targetLocale}.json`);
|
|
201
|
+
const existing = readTargetFile(targetFilePath);
|
|
202
|
+
const entries = Object.entries(changedKeys);
|
|
203
|
+
for (let i = 0; i < entries.length; i += batchSize) {
|
|
204
|
+
const batch = Object.fromEntries(entries.slice(i, i + batchSize));
|
|
205
|
+
try {
|
|
206
|
+
const translated = await translate(
|
|
207
|
+
batch,
|
|
208
|
+
targetLocale,
|
|
209
|
+
changedContexts
|
|
210
|
+
);
|
|
211
|
+
Object.assign(existing, translated);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
failures.push({
|
|
214
|
+
locale: targetLocale,
|
|
215
|
+
keys: Object.keys(batch),
|
|
216
|
+
error: err
|
|
217
|
+
});
|
|
218
|
+
for (const key of Object.keys(batch)) {
|
|
219
|
+
failedKeys.add(key);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
writeTargetFile(targetFilePath, existing, sourceDict);
|
|
224
|
+
log(` ${targetLocale}: ${Object.keys(existing).length} keys`);
|
|
225
|
+
}
|
|
226
|
+
const translatedKeys = [];
|
|
227
|
+
for (const [key, entry] of Object.entries(pendingEntries)) {
|
|
228
|
+
if (failedKeys.has(key)) continue;
|
|
229
|
+
lock.keys[key] = entry;
|
|
230
|
+
translatedKeys.push(key);
|
|
231
|
+
}
|
|
232
|
+
writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
|
|
233
|
+
return { status: "synced", translatedKeys, deletedKeys, failures };
|
|
234
|
+
}
|
|
235
|
+
function formatSyncFailures(failures) {
|
|
236
|
+
return failures.map((failure) => {
|
|
237
|
+
const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
|
|
238
|
+
return `${failure.locale}: ${failure.keys.length} key${failure.keys.length > 1 ? "s" : ""} [${failure.keys.join(", ")}] \u2014 ${message}`;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function readTargetFile(targetFilePath) {
|
|
242
|
+
if (!existsSync(targetFilePath)) return {};
|
|
243
|
+
try {
|
|
244
|
+
return JSON.parse(readFileSync(targetFilePath, "utf-8"));
|
|
245
|
+
} catch {
|
|
246
|
+
return {};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function writeTargetFile(targetFilePath, translations, sourceDict) {
|
|
250
|
+
for (const key of Object.keys(translations)) {
|
|
251
|
+
if (!(key in sourceDict)) {
|
|
252
|
+
delete translations[key];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const sorted = Object.fromEntries(
|
|
256
|
+
Object.entries(translations).sort(([a], [b]) => a.localeCompare(b))
|
|
257
|
+
);
|
|
258
|
+
writeFileSync(targetFilePath, JSON.stringify(sorted, null, 2) + "\n");
|
|
259
|
+
}
|
|
260
|
+
|
|
100
261
|
// src/vite.ts
|
|
101
262
|
var VIRTUAL_MODULE_ID = "virtual:solid-translate";
|
|
102
263
|
var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
|
|
@@ -113,19 +274,17 @@ function solidTranslate(config) {
|
|
|
113
274
|
} = config;
|
|
114
275
|
let root;
|
|
115
276
|
let resolvedLocalesDir;
|
|
116
|
-
let lockFilePath;
|
|
117
277
|
return {
|
|
118
278
|
name: "solid-translate",
|
|
119
279
|
configResolved(resolvedConfig) {
|
|
120
280
|
root = resolvedConfig.root;
|
|
121
281
|
resolvedLocalesDir = resolve(root, localesDir);
|
|
122
|
-
lockFilePath = join(resolvedLocalesDir, ".solid-translate.lock");
|
|
123
282
|
},
|
|
124
283
|
async buildStart() {
|
|
125
|
-
if (!
|
|
284
|
+
if (!existsSync2(resolvedLocalesDir)) {
|
|
126
285
|
mkdirSync(resolvedLocalesDir, { recursive: true });
|
|
127
286
|
}
|
|
128
|
-
const sourceFilePath =
|
|
287
|
+
const sourceFilePath = join2(
|
|
129
288
|
resolvedLocalesDir,
|
|
130
289
|
`${sourceLocale}.json`
|
|
131
290
|
);
|
|
@@ -134,10 +293,10 @@ function solidTranslate(config) {
|
|
|
134
293
|
const extracted = await autoExtractStrings(root, include);
|
|
135
294
|
contexts = extracted.contexts;
|
|
136
295
|
let existingSource = {};
|
|
137
|
-
if (
|
|
296
|
+
if (existsSync2(sourceFilePath)) {
|
|
138
297
|
try {
|
|
139
298
|
existingSource = JSON.parse(
|
|
140
|
-
|
|
299
|
+
readFileSync2(sourceFilePath, "utf-8")
|
|
141
300
|
);
|
|
142
301
|
} catch {
|
|
143
302
|
}
|
|
@@ -155,7 +314,7 @@ function solidTranslate(config) {
|
|
|
155
314
|
([a], [b]) => a.localeCompare(b)
|
|
156
315
|
)
|
|
157
316
|
);
|
|
158
|
-
|
|
317
|
+
writeFileSync2(
|
|
159
318
|
sourceFilePath,
|
|
160
319
|
JSON.stringify(sorted, null, 2) + "\n"
|
|
161
320
|
);
|
|
@@ -164,7 +323,7 @@ function solidTranslate(config) {
|
|
|
164
323
|
);
|
|
165
324
|
}
|
|
166
325
|
}
|
|
167
|
-
if (!
|
|
326
|
+
if (!existsSync2(sourceFilePath)) {
|
|
168
327
|
console.warn(
|
|
169
328
|
`[solid-translate] Source locale file not found: ${relative(root, sourceFilePath)}`
|
|
170
329
|
);
|
|
@@ -173,99 +332,36 @@ function solidTranslate(config) {
|
|
|
173
332
|
);
|
|
174
333
|
return;
|
|
175
334
|
}
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
if (Object.keys(changedKeys).length === 0) {
|
|
203
|
-
console.log(
|
|
204
|
-
"[solid-translate] No changes detected, skipping translation."
|
|
335
|
+
const result = await syncLocaleFiles({
|
|
336
|
+
localesDir: resolvedLocalesDir,
|
|
337
|
+
sourceLocale,
|
|
338
|
+
targetLocales,
|
|
339
|
+
batchSize,
|
|
340
|
+
// Only pass extraction contexts when autoExtract ran; otherwise
|
|
341
|
+
// preserve the contexts already recorded in the lock file.
|
|
342
|
+
contexts: autoExtract ? contexts : void 0,
|
|
343
|
+
translate: (batch, targetLocale, changedContexts) => translateBatch(
|
|
344
|
+
model,
|
|
345
|
+
batch,
|
|
346
|
+
targetLocale,
|
|
347
|
+
sourceLocale,
|
|
348
|
+
systemPrompt,
|
|
349
|
+
changedContexts
|
|
350
|
+
),
|
|
351
|
+
log: (message) => console.log(`[solid-translate] ${message}`)
|
|
352
|
+
});
|
|
353
|
+
if (result.failures.length > 0) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
[
|
|
356
|
+
"[solid-translate] Translation failed for some batches:",
|
|
357
|
+
...formatSyncFailures(result.failures).map((line) => ` ${line}`),
|
|
358
|
+
"Failed keys were not recorded in the lock file \u2014 fix the error and rebuild to retry them."
|
|
359
|
+
].join("\n")
|
|
205
360
|
);
|
|
206
|
-
return;
|
|
207
361
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
`[solid-translate] Translating ${count} key${count > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
|
|
211
|
-
);
|
|
212
|
-
const changedContexts = {};
|
|
213
|
-
for (const key of Object.keys(changedKeys)) {
|
|
214
|
-
const ctx = lock.keys[key]?.context;
|
|
215
|
-
if (ctx) changedContexts[key] = ctx;
|
|
216
|
-
}
|
|
217
|
-
for (const targetLocale of targetLocales) {
|
|
218
|
-
const targetFilePath = join(
|
|
219
|
-
resolvedLocalesDir,
|
|
220
|
-
`${targetLocale}.json`
|
|
221
|
-
);
|
|
222
|
-
let existing = {};
|
|
223
|
-
if (existsSync(targetFilePath)) {
|
|
224
|
-
try {
|
|
225
|
-
existing = JSON.parse(readFileSync(targetFilePath, "utf-8"));
|
|
226
|
-
} catch {
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
const entries = Object.entries(changedKeys);
|
|
230
|
-
for (let i = 0; i < entries.length; i += batchSize) {
|
|
231
|
-
const batch = Object.fromEntries(
|
|
232
|
-
entries.slice(i, i + batchSize)
|
|
233
|
-
);
|
|
234
|
-
try {
|
|
235
|
-
const translated = await translateBatch(
|
|
236
|
-
model,
|
|
237
|
-
batch,
|
|
238
|
-
targetLocale,
|
|
239
|
-
sourceLocale,
|
|
240
|
-
systemPrompt,
|
|
241
|
-
changedContexts
|
|
242
|
-
);
|
|
243
|
-
Object.assign(existing, translated);
|
|
244
|
-
} catch (err) {
|
|
245
|
-
console.error(
|
|
246
|
-
`[solid-translate] Failed to translate batch for ${targetLocale}:`,
|
|
247
|
-
err
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
for (const key of Object.keys(existing)) {
|
|
252
|
-
if (!(key in sourceDict)) {
|
|
253
|
-
delete existing[key];
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const sorted = Object.fromEntries(
|
|
257
|
-
Object.entries(existing).sort(([a], [b]) => a.localeCompare(b))
|
|
258
|
-
);
|
|
259
|
-
writeFileSync(
|
|
260
|
-
targetFilePath,
|
|
261
|
-
JSON.stringify(sorted, null, 2) + "\n"
|
|
262
|
-
);
|
|
263
|
-
console.log(
|
|
264
|
-
`[solid-translate] ${targetLocale}: ${Object.keys(sorted).length} keys`
|
|
265
|
-
);
|
|
362
|
+
if (result.status === "synced") {
|
|
363
|
+
console.log("[solid-translate] Translation complete.");
|
|
266
364
|
}
|
|
267
|
-
writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
|
|
268
|
-
console.log("[solid-translate] Translation complete.");
|
|
269
365
|
},
|
|
270
366
|
resolveId(id) {
|
|
271
367
|
if (id === VIRTUAL_MODULE_ID) {
|
|
@@ -275,15 +371,15 @@ function solidTranslate(config) {
|
|
|
275
371
|
load(id) {
|
|
276
372
|
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
277
373
|
const translations = {};
|
|
278
|
-
if (
|
|
374
|
+
if (existsSync2(resolvedLocalesDir)) {
|
|
279
375
|
for (const file of readdirSync(resolvedLocalesDir)) {
|
|
280
376
|
if (!file.endsWith(".json")) continue;
|
|
281
377
|
if (file.startsWith(".")) continue;
|
|
282
378
|
const locale = file.replace(".json", "");
|
|
283
|
-
const filePath =
|
|
379
|
+
const filePath = join2(resolvedLocalesDir, file);
|
|
284
380
|
try {
|
|
285
381
|
translations[locale] = JSON.parse(
|
|
286
|
-
|
|
382
|
+
readFileSync2(filePath, "utf-8")
|
|
287
383
|
);
|
|
288
384
|
} catch {
|
|
289
385
|
}
|
|
@@ -315,7 +411,7 @@ async function autoExtractStrings(root, patterns) {
|
|
|
315
411
|
const files = await glob(pattern, { cwd: root, absolute: true });
|
|
316
412
|
for (const file of files) {
|
|
317
413
|
try {
|
|
318
|
-
const code =
|
|
414
|
+
const code = readFileSync2(file, "utf-8");
|
|
319
415
|
const extracted = extractStringsFromSource(
|
|
320
416
|
code,
|
|
321
417
|
relative(root, file)
|
package/dist/vite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/vite.ts","../src/hash.ts","../src/translate.ts","../src/extract.ts"],"sourcesContent":["import type { Plugin, ResolvedConfig } from \"vite\";\nimport {\n readFileSync,\n writeFileSync,\n existsSync,\n mkdirSync,\n readdirSync,\n} from \"node:fs\";\nimport { resolve, join, relative } from \"node:path\";\nimport { hashContent } from \"./hash.js\";\nimport { translateBatch } from \"./translate.js\";\nimport { extractStringsFromSource } from \"./extract.js\";\nimport type { SolidTranslatePluginConfig, LockFile } from \"./types.js\";\n\nexport type { SolidTranslatePluginConfig };\n\nconst VIRTUAL_MODULE_ID = \"virtual:solid-translate\";\nconst RESOLVED_VIRTUAL_MODULE_ID = \"\\0\" + VIRTUAL_MODULE_ID;\n\n/**\n * Vite plugin for solid-translate.\n *\n * Handles:\n * 1. Optional extraction of <T>, msg() strings from source files\n * 2. AI translation of source locale to target locales (with context support)\n * 3. Lock file management for efficient re-translation\n * 4. Virtual module serving translations at runtime\n */\nexport function solidTranslate(config: SolidTranslatePluginConfig): Plugin {\n const {\n sourceLocale = \"en\",\n targetLocales,\n localesDir = \"./src/locales\",\n model,\n systemPrompt,\n batchSize = 50,\n autoExtract = false,\n include = [\"src/**/*.tsx\", \"src/**/*.ts\", \"src/**/*.jsx\"],\n } = config;\n\n let root: string;\n let resolvedLocalesDir: string;\n let lockFilePath: string;\n\n return {\n name: \"solid-translate\",\n\n configResolved(resolvedConfig: ResolvedConfig) {\n root = resolvedConfig.root;\n resolvedLocalesDir = resolve(root, localesDir);\n lockFilePath = join(resolvedLocalesDir, \".solid-translate.lock\");\n },\n\n async buildStart() {\n // Ensure locales directory exists\n if (!existsSync(resolvedLocalesDir)) {\n mkdirSync(resolvedLocalesDir, { recursive: true });\n }\n\n const sourceFilePath = join(\n resolvedLocalesDir,\n `${sourceLocale}.json`,\n );\n\n // Auto-extraction: scan source files for <T> and msg() strings\n let contexts: Record<string, string> = {};\n if (autoExtract) {\n const extracted = await autoExtractStrings(root, include);\n contexts = extracted.contexts;\n\n // Merge into source locale file\n let existingSource: Record<string, string> = {};\n if (existsSync(sourceFilePath)) {\n try {\n existingSource = JSON.parse(\n readFileSync(sourceFilePath, \"utf-8\"),\n );\n } catch {\n // start fresh\n }\n }\n\n let changed = false;\n for (const [key, value] of Object.entries(extracted.strings)) {\n if (!(key in existingSource)) {\n existingSource[key] = value;\n changed = true;\n }\n }\n\n if (changed) {\n const sorted = Object.fromEntries(\n Object.entries(existingSource).sort(([a], [b]) =>\n a.localeCompare(b),\n ),\n );\n writeFileSync(\n sourceFilePath,\n JSON.stringify(sorted, null, 2) + \"\\n\",\n );\n console.log(\n `[solid-translate] Auto-extracted ${Object.keys(extracted.strings).length} strings from source`,\n );\n }\n }\n\n // Read source locale file\n if (!existsSync(sourceFilePath)) {\n console.warn(\n `[solid-translate] Source locale file not found: ${relative(root, sourceFilePath)}`,\n );\n console.warn(\n `[solid-translate] Create it with your source strings, or enable autoExtract`,\n );\n return;\n }\n\n const sourceDict: Record<string, string> = JSON.parse(\n readFileSync(sourceFilePath, \"utf-8\"),\n );\n\n // Read or initialize lock file\n let lock: LockFile = { version: 1, sourceLocale, keys: {} };\n if (existsSync(lockFilePath)) {\n try {\n lock = JSON.parse(readFileSync(lockFilePath, \"utf-8\"));\n } catch {\n // Corrupted lock file — start fresh\n }\n }\n\n // Determine which keys have changed or are new\n const changedKeys: Record<string, string> = {};\n for (const [key, value] of Object.entries(sourceDict)) {\n const hash = hashContent(value);\n const existing = lock.keys[key];\n const existingContext = existing?.context;\n const newContext = contexts[key];\n\n // Re-translate if content changed OR context changed\n if (\n !existing ||\n existing.hash !== hash ||\n existingContext !== newContext\n ) {\n changedKeys[key] = value;\n lock.keys[key] = { hash, source: value, context: newContext };\n }\n }\n\n // Remove keys that no longer exist in source\n for (const key of Object.keys(lock.keys)) {\n if (!(key in sourceDict)) {\n delete lock.keys[key];\n }\n }\n\n if (Object.keys(changedKeys).length === 0) {\n console.log(\n \"[solid-translate] No changes detected, skipping translation.\",\n );\n return;\n }\n\n const count = Object.keys(changedKeys).length;\n console.log(\n `[solid-translate] Translating ${count} key${count > 1 ? \"s\" : \"\"} to ${targetLocales.length} locale${targetLocales.length > 1 ? \"s\" : \"\"}...`,\n );\n\n // Build context map for changed keys\n const changedContexts: Record<string, string> = {};\n for (const key of Object.keys(changedKeys)) {\n const ctx = lock.keys[key]?.context;\n if (ctx) changedContexts[key] = ctx;\n }\n\n // Translate for each target locale\n for (const targetLocale of targetLocales) {\n const targetFilePath = join(\n resolvedLocalesDir,\n `${targetLocale}.json`,\n );\n\n // Load existing translations to preserve unchanged keys\n let existing: Record<string, string> = {};\n if (existsSync(targetFilePath)) {\n try {\n existing = JSON.parse(readFileSync(targetFilePath, \"utf-8\"));\n } catch {\n // Corrupted file — regenerate\n }\n }\n\n // Batch translate changed keys\n const entries = Object.entries(changedKeys);\n for (let i = 0; i < entries.length; i += batchSize) {\n const batch = Object.fromEntries(\n entries.slice(i, i + batchSize),\n );\n try {\n const translated = await translateBatch(\n model,\n batch,\n targetLocale,\n sourceLocale,\n systemPrompt,\n changedContexts,\n );\n Object.assign(existing, translated);\n } catch (err) {\n console.error(\n `[solid-translate] Failed to translate batch for ${targetLocale}:`,\n err,\n );\n }\n }\n\n // Remove keys that no longer exist in source\n for (const key of Object.keys(existing)) {\n if (!(key in sourceDict)) {\n delete existing[key];\n }\n }\n\n // Sort keys for stable, diff-friendly output\n const sorted = Object.fromEntries(\n Object.entries(existing).sort(([a], [b]) => a.localeCompare(b)),\n );\n\n writeFileSync(\n targetFilePath,\n JSON.stringify(sorted, null, 2) + \"\\n\",\n );\n console.log(\n `[solid-translate] ${targetLocale}: ${Object.keys(sorted).length} keys`,\n );\n }\n\n // Write updated lock file\n writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + \"\\n\");\n console.log(\"[solid-translate] Translation complete.\");\n },\n\n resolveId(id: string) {\n if (id === VIRTUAL_MODULE_ID) {\n return RESOLVED_VIRTUAL_MODULE_ID;\n }\n },\n\n load(id: string) {\n if (id === RESOLVED_VIRTUAL_MODULE_ID) {\n // Load all locale JSON files and export as a single object\n const translations: Record<string, Record<string, string>> = {};\n\n if (existsSync(resolvedLocalesDir)) {\n for (const file of readdirSync(resolvedLocalesDir)) {\n if (!file.endsWith(\".json\")) continue;\n if (file.startsWith(\".\")) continue;\n const locale = file.replace(\".json\", \"\");\n const filePath = join(resolvedLocalesDir, file);\n try {\n translations[locale] = JSON.parse(\n readFileSync(filePath, \"utf-8\"),\n );\n } catch {\n // Skip malformed files\n }\n }\n }\n\n return `export default ${JSON.stringify(translations)};`;\n }\n },\n\n // HMR: reload translations when locale files change\n handleHotUpdate({ file, server }) {\n if (\n file.startsWith(resolvedLocalesDir) &&\n file.endsWith(\".json\")\n ) {\n const mod = server.moduleGraph.getModuleById(\n RESOLVED_VIRTUAL_MODULE_ID,\n );\n if (mod) {\n server.moduleGraph.invalidateModule(mod);\n return [mod];\n }\n }\n },\n };\n}\n\nexport default solidTranslate;\n\n// ---------------------------------------------------------------------------\n// Auto-extraction helper\n// ---------------------------------------------------------------------------\n\nasync function autoExtractStrings(\n root: string,\n patterns: string[],\n): Promise<{ strings: Record<string, string>; contexts: Record<string, string> }> {\n const strings: Record<string, string> = {};\n const contexts: Record<string, string> = {};\n\n // Dynamically import glob for file matching\n const { glob } = await import(\"glob\");\n\n for (const pattern of patterns) {\n const files = await glob(pattern, { cwd: root, absolute: true });\n for (const file of files) {\n try {\n const code = readFileSync(file, \"utf-8\");\n const extracted = extractStringsFromSource(\n code,\n relative(root, file),\n );\n for (const entry of extracted) {\n strings[entry.key] = entry.source;\n if (entry.context) {\n contexts[entry.key] = entry.context;\n }\n }\n } catch {\n // Skip unreadable files\n }\n }\n }\n\n return { strings, contexts };\n}\n","import { createHash } from \"node:crypto\";\n\n/** Create a short content hash for change detection */\nexport function hashContent(content: string): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 16);\n}\n","import { generateObject } from \"ai\";\nimport { z } from \"zod\";\nimport type { LanguageModelV1 } from \"ai\";\n\n/**\n * Translate a batch of key-value pairs from one locale to another using AI.\n * Supports optional per-key context hints for disambiguation.\n */\nexport async function translateBatch(\n model: LanguageModelV1,\n entries: Record<string, string>,\n targetLocale: string,\n sourceLocale: string,\n systemPrompt?: string,\n contexts?: Record<string, string>,\n): Promise<Record<string, string>> {\n const keys = Object.keys(entries);\n if (keys.length === 0) return {};\n\n const defaultSystem = [\n `You are a professional translator specializing in software localization.`,\n `Translate text from \"${sourceLocale}\" to \"${targetLocale}\".`,\n `Rules:`,\n `- Preserve the original tone and meaning`,\n `- Keep placeholders like {{variable}}, {variable}, {0}, {1} unchanged`,\n `- Keep HTML tags unchanged`,\n `- Do not add or remove content`,\n `- Return natural, idiomatic translations`,\n ].join(\"\\n\");\n\n // Build context section if any keys have context hints\n let contextSection = \"\";\n if (contexts && Object.keys(contexts).length > 0) {\n const contextLines = Object.entries(contexts)\n .filter(([key]) => key in entries)\n .map(([key, ctx]) => ` \"${key}\": ${ctx}`);\n if (contextLines.length > 0) {\n contextSection = [\n ``,\n `Context hints for disambiguation:`,\n ...contextLines,\n ``,\n ].join(\"\\n\");\n }\n }\n\n const { object } = await generateObject({\n model,\n schema: z.object({\n translations: z.record(z.string(), z.string()),\n }),\n system: systemPrompt || defaultSystem,\n prompt: [\n `Translate each value in this JSON object from \"${sourceLocale}\" to \"${targetLocale}\".`,\n `Return a JSON object with the exact same keys and the translated values.`,\n contextSection,\n JSON.stringify(entries, null, 2),\n ].join(\"\\n\"),\n });\n\n return object.translations;\n}\n\n/**\n * Translate a markdown or MDX string from one locale to another.\n * Preserves code blocks, frontmatter, and MDX components.\n */\nexport async function translateMarkdown(\n model: LanguageModelV1,\n content: string,\n targetLocale: string,\n sourceLocale: string,\n systemPrompt?: string,\n): Promise<string> {\n const defaultSystem = [\n `You are a professional translator specializing in documentation.`,\n `Translate Markdown/MDX content from \"${sourceLocale}\" to \"${targetLocale}\".`,\n `Rules:`,\n `- Preserve all Markdown formatting (headers, lists, bold, italic, links, etc.)`,\n `- Preserve code blocks and inline code unchanged`,\n `- Preserve frontmatter YAML keys (only translate values)`,\n `- Preserve MDX component syntax and JSX expressions`,\n `- Preserve URLs and file paths unchanged`,\n `- Return natural, idiomatic translations`,\n ].join(\"\\n\");\n\n const { object } = await generateObject({\n model,\n schema: z.object({\n translated: z.string(),\n }),\n system: systemPrompt || defaultSystem,\n prompt: [\n `Translate this Markdown/MDX content from \"${sourceLocale}\" to \"${targetLocale}\".`,\n `Return the complete translated document.`,\n ``,\n content,\n ].join(\"\\n\"),\n });\n\n return object.translated;\n}\n","/** Extracted translatable string from source code */\nexport interface ExtractedString {\n key: string;\n source: string;\n file: string;\n line: number;\n /** AI context hint from the `context` prop */\n context?: string;\n}\n\n/**\n * Extract translatable strings from source code by finding:\n * - `<T>text</T>` — source text is used as the key\n * - `<T id=\"key\">fallback</T>` — explicit key\n * - `<T context=\"hint\">text</T>` — with AI context\n * - `<T id=\"key\" context=\"hint\">text</T>` — both\n * - `<T>text <Var>...</Var> more</T>` — builds template with {0} placeholders\n * - `msg(\"text\")` — shared string marker\n */\nexport function extractStringsFromSource(\n code: string,\n filePath: string,\n): ExtractedString[] {\n const results: ExtractedString[] = [];\n const seen = new Set<string>();\n\n // Match <T ...props>children</T>\n const tComponentRegex = /<T(\\s[^>]*)?>([^]*?)<\\/T>/g;\n let match: RegExpExecArray | null;\n\n while ((match = tComponentRegex.exec(code)) !== null) {\n const attrs = match[1] || \"\";\n const rawChildren = match[2]!;\n const line = code.substring(0, match.index).split(\"\\n\").length;\n\n // Parse id attribute\n const idMatch = attrs.match(/id=[\"']([^\"']+)[\"']/);\n // Parse context attribute\n const contextMatch = attrs.match(/context=[\"']([^\"']+)[\"']/);\n\n // Build source text: replace <Var>, <Num>, <Currency>, <DateTime> with {n} placeholders\n // Single-pass replacement to preserve document order\n let slotIndex = 0;\n const source = rawChildren\n .replace(\n /<(?:Var|Num|Currency|DateTime)(?:\\s[^>]*)?>([^]*?)<\\/(?:Var|Num|Currency|DateTime)>/g,\n () => `{${slotIndex++}}`,\n )\n .trim();\n\n const key = idMatch ? idMatch[1]! : source;\n if (!key || seen.has(key)) continue;\n seen.add(key);\n\n results.push({\n key,\n source,\n file: filePath,\n line,\n context: contextMatch ? contextMatch[1] : undefined,\n });\n }\n\n // Match msg(\"text\") and msg('text') calls\n const msgRegex = /\\bmsg\\(\\s*[\"']([^\"']+)[\"']\\s*(?:,\\s*\\{[^}]*\\})?\\s*\\)/g;\n while ((match = msgRegex.exec(code)) !== null) {\n const source = match[1]!;\n if (seen.has(source)) continue;\n seen.add(source);\n const line = code.substring(0, match.index).split(\"\\n\").length;\n results.push({ key: source, source, file: filePath, line });\n }\n\n return results;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/vite.ts","../src/translate.ts","../src/extract.ts","../src/lock.ts","../src/hash.ts"],"sourcesContent":["import type { Plugin, ResolvedConfig } from \"vite\";\nimport {\n readFileSync,\n writeFileSync,\n existsSync,\n mkdirSync,\n readdirSync,\n} from \"node:fs\";\nimport { resolve, join, relative } from \"node:path\";\nimport { translateBatch } from \"./translate.js\";\nimport { extractStringsFromSource } from \"./extract.js\";\nimport { syncLocaleFiles, formatSyncFailures } from \"./lock.js\";\nimport type { SolidTranslatePluginConfig } from \"./types.js\";\n\nexport type { SolidTranslatePluginConfig };\n\nconst VIRTUAL_MODULE_ID = \"virtual:solid-translate\";\nconst RESOLVED_VIRTUAL_MODULE_ID = \"\\0\" + VIRTUAL_MODULE_ID;\n\n/**\n * Vite plugin for solid-translate.\n *\n * Handles:\n * 1. Optional extraction of <T>, msg() strings from source files\n * 2. AI translation of source locale to target locales (with context support)\n * 3. Lock file management for efficient re-translation\n * 4. Virtual module serving translations at runtime\n */\nexport function solidTranslate(config: SolidTranslatePluginConfig): Plugin {\n const {\n sourceLocale = \"en\",\n targetLocales,\n localesDir = \"./src/locales\",\n model,\n systemPrompt,\n batchSize = 50,\n autoExtract = false,\n include = [\"src/**/*.tsx\", \"src/**/*.ts\", \"src/**/*.jsx\"],\n } = config;\n\n let root: string;\n let resolvedLocalesDir: string;\n\n return {\n name: \"solid-translate\",\n\n configResolved(resolvedConfig: ResolvedConfig) {\n root = resolvedConfig.root;\n resolvedLocalesDir = resolve(root, localesDir);\n },\n\n async buildStart() {\n // Ensure locales directory exists\n if (!existsSync(resolvedLocalesDir)) {\n mkdirSync(resolvedLocalesDir, { recursive: true });\n }\n\n const sourceFilePath = join(\n resolvedLocalesDir,\n `${sourceLocale}.json`,\n );\n\n // Auto-extraction: scan source files for <T> and msg() strings\n let contexts: Record<string, string> = {};\n if (autoExtract) {\n const extracted = await autoExtractStrings(root, include);\n contexts = extracted.contexts;\n\n // Merge into source locale file\n let existingSource: Record<string, string> = {};\n if (existsSync(sourceFilePath)) {\n try {\n existingSource = JSON.parse(\n readFileSync(sourceFilePath, \"utf-8\"),\n );\n } catch {\n // start fresh\n }\n }\n\n let changed = false;\n for (const [key, value] of Object.entries(extracted.strings)) {\n if (!(key in existingSource)) {\n existingSource[key] = value;\n changed = true;\n }\n }\n\n if (changed) {\n const sorted = Object.fromEntries(\n Object.entries(existingSource).sort(([a], [b]) =>\n a.localeCompare(b),\n ),\n );\n writeFileSync(\n sourceFilePath,\n JSON.stringify(sorted, null, 2) + \"\\n\",\n );\n console.log(\n `[solid-translate] Auto-extracted ${Object.keys(extracted.strings).length} strings from source`,\n );\n }\n }\n\n // Read source locale file\n if (!existsSync(sourceFilePath)) {\n console.warn(\n `[solid-translate] Source locale file not found: ${relative(root, sourceFilePath)}`,\n );\n console.warn(\n `[solid-translate] Create it with your source strings, or enable autoExtract`,\n );\n return;\n }\n\n const result = await syncLocaleFiles({\n localesDir: resolvedLocalesDir,\n sourceLocale,\n targetLocales,\n batchSize,\n // Only pass extraction contexts when autoExtract ran; otherwise\n // preserve the contexts already recorded in the lock file.\n contexts: autoExtract ? contexts : undefined,\n translate: (batch, targetLocale, changedContexts) =>\n translateBatch(\n model,\n batch,\n targetLocale,\n sourceLocale,\n systemPrompt,\n changedContexts,\n ),\n log: (message) => console.log(`[solid-translate] ${message}`),\n });\n\n if (result.failures.length > 0) {\n // Fail the build: successfully translated batches were written, but\n // failed keys were NOT recorded in the lock, so they retry next run.\n throw new Error(\n [\n \"[solid-translate] Translation failed for some batches:\",\n ...formatSyncFailures(result.failures).map((line) => ` ${line}`),\n \"Failed keys were not recorded in the lock file — fix the error and rebuild to retry them.\",\n ].join(\"\\n\"),\n );\n }\n\n if (result.status === \"synced\") {\n console.log(\"[solid-translate] Translation complete.\");\n }\n },\n\n resolveId(id: string) {\n if (id === VIRTUAL_MODULE_ID) {\n return RESOLVED_VIRTUAL_MODULE_ID;\n }\n },\n\n load(id: string) {\n if (id === RESOLVED_VIRTUAL_MODULE_ID) {\n // Load all locale JSON files and export as a single object\n const translations: Record<string, Record<string, string>> = {};\n\n if (existsSync(resolvedLocalesDir)) {\n for (const file of readdirSync(resolvedLocalesDir)) {\n if (!file.endsWith(\".json\")) continue;\n if (file.startsWith(\".\")) continue;\n const locale = file.replace(\".json\", \"\");\n const filePath = join(resolvedLocalesDir, file);\n try {\n translations[locale] = JSON.parse(\n readFileSync(filePath, \"utf-8\"),\n );\n } catch {\n // Skip malformed files\n }\n }\n }\n\n return `export default ${JSON.stringify(translations)};`;\n }\n },\n\n // HMR: reload translations when locale files change\n handleHotUpdate({ file, server }) {\n if (\n file.startsWith(resolvedLocalesDir) &&\n file.endsWith(\".json\")\n ) {\n const mod = server.moduleGraph.getModuleById(\n RESOLVED_VIRTUAL_MODULE_ID,\n );\n if (mod) {\n server.moduleGraph.invalidateModule(mod);\n return [mod];\n }\n }\n },\n };\n}\n\nexport default solidTranslate;\n\n// ---------------------------------------------------------------------------\n// Auto-extraction helper\n// ---------------------------------------------------------------------------\n\nasync function autoExtractStrings(\n root: string,\n patterns: string[],\n): Promise<{ strings: Record<string, string>; contexts: Record<string, string> }> {\n const strings: Record<string, string> = {};\n const contexts: Record<string, string> = {};\n\n // Dynamically import glob for file matching\n const { glob } = await import(\"glob\");\n\n for (const pattern of patterns) {\n const files = await glob(pattern, { cwd: root, absolute: true });\n for (const file of files) {\n try {\n const code = readFileSync(file, \"utf-8\");\n const extracted = extractStringsFromSource(\n code,\n relative(root, file),\n );\n for (const entry of extracted) {\n strings[entry.key] = entry.source;\n if (entry.context) {\n contexts[entry.key] = entry.context;\n }\n }\n } catch {\n // Skip unreadable files\n }\n }\n }\n\n return { strings, contexts };\n}\n","import { z } from \"zod\";\nimport type { LanguageModelV1 } from \"ai\";\n\n/**\n * Lazily import the `ai` package so that merely loading this module (e.g.\n * via the Vite plugin on extract-only or fresh-lock builds) does not require\n * `ai` to be installed. It is only needed when translation actually runs.\n */\nasync function loadGenerateObject() {\n const { generateObject } = await import(\"ai\");\n return generateObject;\n}\n\n/**\n * Translate a batch of key-value pairs from one locale to another using AI.\n * Supports optional per-key context hints for disambiguation.\n */\nexport async function translateBatch(\n model: LanguageModelV1,\n entries: Record<string, string>,\n targetLocale: string,\n sourceLocale: string,\n systemPrompt?: string,\n contexts?: Record<string, string>,\n): Promise<Record<string, string>> {\n const keys = Object.keys(entries);\n if (keys.length === 0) return {};\n\n const defaultSystem = [\n `You are a professional translator specializing in software localization.`,\n `Translate text from \"${sourceLocale}\" to \"${targetLocale}\".`,\n `Rules:`,\n `- Preserve the original tone and meaning`,\n `- Keep placeholders like {{variable}}, {variable}, {0}, {1} unchanged`,\n `- Keep HTML tags unchanged`,\n `- Do not add or remove content`,\n `- Return natural, idiomatic translations`,\n ].join(\"\\n\");\n\n // Build context section if any keys have context hints\n let contextSection = \"\";\n if (contexts && Object.keys(contexts).length > 0) {\n const contextLines = Object.entries(contexts)\n .filter(([key]) => key in entries)\n .map(([key, ctx]) => ` \"${key}\": ${ctx}`);\n if (contextLines.length > 0) {\n contextSection = [\n ``,\n `Context hints for disambiguation:`,\n ...contextLines,\n ``,\n ].join(\"\\n\");\n }\n }\n\n const generateObject = await loadGenerateObject();\n const { object } = await generateObject({\n model,\n schema: z.object({\n translations: z.record(z.string(), z.string()),\n }),\n system: systemPrompt || defaultSystem,\n prompt: [\n `Translate each value in this JSON object from \"${sourceLocale}\" to \"${targetLocale}\".`,\n `Return a JSON object with the exact same keys and the translated values.`,\n contextSection,\n JSON.stringify(entries, null, 2),\n ].join(\"\\n\"),\n });\n\n return object.translations;\n}\n\n/**\n * Translate a markdown or MDX string from one locale to another.\n * Preserves code blocks, frontmatter, and MDX components.\n */\nexport async function translateMarkdown(\n model: LanguageModelV1,\n content: string,\n targetLocale: string,\n sourceLocale: string,\n systemPrompt?: string,\n): Promise<string> {\n const defaultSystem = [\n `You are a professional translator specializing in documentation.`,\n `Translate Markdown/MDX content from \"${sourceLocale}\" to \"${targetLocale}\".`,\n `Rules:`,\n `- Preserve all Markdown formatting (headers, lists, bold, italic, links, etc.)`,\n `- Preserve code blocks and inline code unchanged`,\n `- Preserve frontmatter YAML keys (only translate values)`,\n `- Preserve MDX component syntax and JSX expressions`,\n `- Preserve URLs and file paths unchanged`,\n `- Return natural, idiomatic translations`,\n ].join(\"\\n\");\n\n const generateObject = await loadGenerateObject();\n const { object } = await generateObject({\n model,\n schema: z.object({\n translated: z.string(),\n }),\n system: systemPrompt || defaultSystem,\n prompt: [\n `Translate this Markdown/MDX content from \"${sourceLocale}\" to \"${targetLocale}\".`,\n `Return the complete translated document.`,\n ``,\n content,\n ].join(\"\\n\"),\n });\n\n return object.translated;\n}\n","/** Extracted translatable string from source code */\nexport interface ExtractedString {\n key: string;\n source: string;\n file: string;\n line: number;\n /** AI context hint from the `context` prop */\n context?: string;\n}\n\n/**\n * Extract translatable strings from source code by finding:\n * - `<T>text</T>` — source text is used as the key\n * - `<T id=\"key\">fallback</T>` — explicit key\n * - `<T context=\"hint\">text</T>` — with AI context\n * - `<T id=\"key\" context=\"hint\">text</T>` — both\n * - `<T>text <Var>...</Var> more</T>` — builds template with {0} placeholders\n * - `msg(\"text\")` — shared string marker\n */\nexport function extractStringsFromSource(\n code: string,\n filePath: string,\n): ExtractedString[] {\n const results: ExtractedString[] = [];\n const seen = new Set<string>();\n\n // Match <T ...props>children</T>\n const tComponentRegex = /<T(\\s[^>]*)?>([^]*?)<\\/T>/g;\n let match: RegExpExecArray | null;\n\n while ((match = tComponentRegex.exec(code)) !== null) {\n const attrs = match[1] || \"\";\n const rawChildren = match[2]!;\n const line = code.substring(0, match.index).split(\"\\n\").length;\n\n // Parse id attribute\n const idMatch = attrs.match(/id=[\"']([^\"']+)[\"']/);\n // Parse context attribute\n const contextMatch = attrs.match(/context=[\"']([^\"']+)[\"']/);\n\n // Build source text: replace <Var>, <Num>, <Currency>, <DateTime> with {n} placeholders\n // Single-pass replacement to preserve document order\n let slotIndex = 0;\n const source = rawChildren\n .replace(\n /<(?:Var|Num|Currency|DateTime)(?:\\s[^>]*)?>([^]*?)<\\/(?:Var|Num|Currency|DateTime)>/g,\n () => `{${slotIndex++}}`,\n )\n .trim();\n\n const key = idMatch ? idMatch[1]! : source;\n if (!key || seen.has(key)) continue;\n seen.add(key);\n\n results.push({\n key,\n source,\n file: filePath,\n line,\n context: contextMatch ? contextMatch[1] : undefined,\n });\n }\n\n // Match msg(\"text\") and msg('text') calls\n const msgRegex = /\\bmsg\\(\\s*[\"']([^\"']+)[\"']\\s*(?:,\\s*\\{[^}]*\\})?\\s*\\)/g;\n while ((match = msgRegex.exec(code)) !== null) {\n const source = match[1]!;\n if (seen.has(source)) continue;\n seen.add(source);\n const line = code.substring(0, match.index).split(\"\\n\").length;\n results.push({ key: source, source, file: filePath, line });\n }\n\n return results;\n}\n","import { readFileSync, writeFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { hashContent } from \"./hash.js\";\nimport type { LockFile, LockFileEntry } from \"./types.js\";\n\n/**\n * Shared lock-file + locale-sync logic used by both the CLI and the Vite\n * plugin. Keeping this single-sourced guarantees the two entry points agree\n * on what the lock file means: an entry exists for a key if and only if that\n * key has been successfully translated to every target locale.\n */\n\n/** Result of comparing the source dictionary against the lock file */\nexport interface LockDiff {\n /** Keys that are new or changed and need translation */\n changedKeys: Record<string, string>;\n /**\n * Lock entries for the changed keys. These are *pending*: they must only\n * be committed to the lock after translation succeeds for all locales.\n */\n pendingEntries: Record<string, LockFileEntry>;\n /** Keys present in the lock but no longer in the source dictionary */\n deletedKeys: string[];\n}\n\n/**\n * Compare the source dictionary against the lock file.\n *\n * When `contexts` is provided (Vite auto-extraction), a context change also\n * marks a key as changed and the new context is recorded in the pending\n * entry. When `contexts` is omitted (CLI), existing lock contexts are\n * preserved unchanged so a CLI run never clobbers Vite-written contexts.\n */\nexport function diffLock(\n sourceDict: Record<string, string>,\n lock: LockFile,\n contexts?: Record<string, string>,\n): LockDiff {\n const changedKeys: Record<string, string> = {};\n const pendingEntries: Record<string, LockFileEntry> = {};\n\n for (const [key, value] of Object.entries(sourceDict)) {\n const hash = hashContent(value);\n const existing = lock.keys[key];\n const newContext = contexts ? contexts[key] : existing?.context;\n const contextChanged =\n contexts !== undefined && existing?.context !== contexts[key];\n\n // Re-translate if the key is new, content changed, or context changed\n if (!existing || existing.hash !== hash || contextChanged) {\n changedKeys[key] = value;\n pendingEntries[key] = { hash, source: value, context: newContext };\n }\n }\n\n const deletedKeys = Object.keys(lock.keys).filter(\n (key) => !(key in sourceDict),\n );\n\n return { changedKeys, pendingEntries, deletedKeys };\n}\n\n/** Translate one batch of changed keys for one target locale */\nexport type TranslateFn = (\n batch: Record<string, string>,\n targetLocale: string,\n contexts: Record<string, string>,\n) => Promise<Record<string, string>>;\n\n/** A translation batch that failed for a target locale */\nexport interface SyncFailure {\n locale: string;\n keys: string[];\n error: unknown;\n}\n\nexport interface SyncResult {\n status: \"no-source\" | \"no-changes\" | \"synced\";\n /** Keys translated successfully for ALL target locales (recorded in lock) */\n translatedKeys: string[];\n /** Keys removed from source and pruned from targets + lock */\n deletedKeys: string[];\n /** Failed batches. Non-empty means the run must be treated as failed. */\n failures: SyncFailure[];\n}\n\nexport interface SyncOptions {\n localesDir: string;\n sourceLocale: string;\n targetLocales: string[];\n batchSize: number;\n translate: TranslateFn;\n /**\n * Context hints from auto-extraction (Vite). Omit to preserve existing\n * lock contexts (CLI).\n */\n contexts?: Record<string, string>;\n log?: (message: string) => void;\n}\n\n/**\n * Sync source locale changes into target locale files and the lock file.\n *\n * Guarantees:\n * - Lock entries are committed only for keys whose batches succeeded for\n * every target locale — the lock never claims a key is translated when\n * it isn't. Failed keys stay \"changed\" and are retried on the next run.\n * - Successfully translated batches are still written even when other\n * batches fail; callers must surface `failures` (exit non-zero / throw).\n * - Deleted source keys are pruned from target files and the lock even\n * when there is nothing to translate (no AI calls needed).\n */\nexport async function syncLocaleFiles(\n options: SyncOptions,\n): Promise<SyncResult> {\n const {\n localesDir,\n sourceLocale,\n targetLocales,\n batchSize,\n translate,\n contexts,\n log = () => {},\n } = options;\n\n const sourceFilePath = join(localesDir, `${sourceLocale}.json`);\n if (!existsSync(sourceFilePath)) {\n return {\n status: \"no-source\",\n translatedKeys: [],\n deletedKeys: [],\n failures: [],\n };\n }\n\n const sourceDict: Record<string, string> = JSON.parse(\n readFileSync(sourceFilePath, \"utf-8\"),\n );\n\n const lockFilePath = join(localesDir, \".solid-translate.lock\");\n let lock: LockFile = { version: 1, sourceLocale, keys: {} };\n if (existsSync(lockFilePath)) {\n try {\n lock = JSON.parse(readFileSync(lockFilePath, \"utf-8\"));\n } catch {\n // Corrupted lock file — start fresh\n }\n }\n\n const { changedKeys, pendingEntries, deletedKeys } = diffLock(\n sourceDict,\n lock,\n contexts,\n );\n\n // Remove keys that no longer exist in source\n for (const key of deletedKeys) {\n delete lock.keys[key];\n }\n\n const changedCount = Object.keys(changedKeys).length;\n\n if (changedCount === 0 && deletedKeys.length === 0) {\n log(\"No changes detected in locale files.\");\n return {\n status: \"no-changes\",\n translatedKeys: [],\n deletedKeys: [],\n failures: [],\n };\n }\n\n if (changedCount === 0) {\n // Deletions only — prune target files and the lock, no AI calls needed\n for (const targetLocale of targetLocales) {\n const targetFilePath = join(localesDir, `${targetLocale}.json`);\n const existing = readTargetFile(targetFilePath);\n writeTargetFile(targetFilePath, existing, sourceDict);\n log(` ${targetLocale}: pruned deleted keys`);\n }\n writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + \"\\n\");\n log(\n `Removed ${deletedKeys.length} deleted key${deletedKeys.length > 1 ? \"s\" : \"\"} from target locales.`,\n );\n return { status: \"synced\", translatedKeys: [], deletedKeys, failures: [] };\n }\n\n log(\n `Translating ${changedCount} key${changedCount > 1 ? \"s\" : \"\"} to ${targetLocales.length} locale${targetLocales.length > 1 ? \"s\" : \"\"}...`,\n );\n\n // Context hints for the changed keys, passed to the translator\n const changedContexts: Record<string, string> = {};\n for (const key of Object.keys(changedKeys)) {\n const ctx = pendingEntries[key]?.context;\n if (ctx) changedContexts[key] = ctx;\n }\n\n const failures: SyncFailure[] = [];\n const failedKeys = new Set<string>();\n\n for (const targetLocale of targetLocales) {\n const targetFilePath = join(localesDir, `${targetLocale}.json`);\n\n // Load existing translations to preserve unchanged keys\n const existing = readTargetFile(targetFilePath);\n\n // Batch translate changed keys\n const entries = Object.entries(changedKeys);\n for (let i = 0; i < entries.length; i += batchSize) {\n const batch = Object.fromEntries(entries.slice(i, i + batchSize));\n try {\n const translated = await translate(\n batch,\n targetLocale,\n changedContexts,\n );\n Object.assign(existing, translated);\n } catch (err) {\n failures.push({\n locale: targetLocale,\n keys: Object.keys(batch),\n error: err,\n });\n for (const key of Object.keys(batch)) {\n failedKeys.add(key);\n }\n }\n }\n\n writeTargetFile(targetFilePath, existing, sourceDict);\n log(` ${targetLocale}: ${Object.keys(existing).length} keys`);\n }\n\n // Commit lock entries only for keys that succeeded for ALL target locales.\n // Failed keys keep their old entry (or none), so the next run retries them.\n const translatedKeys: string[] = [];\n for (const [key, entry] of Object.entries(pendingEntries)) {\n if (failedKeys.has(key)) continue;\n lock.keys[key] = entry;\n translatedKeys.push(key);\n }\n\n writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + \"\\n\");\n\n return { status: \"synced\", translatedKeys, deletedKeys, failures };\n}\n\n/** Format sync failures into a human-readable, single-line-per-batch report */\nexport function formatSyncFailures(failures: SyncFailure[]): string[] {\n return failures.map((failure) => {\n const message =\n failure.error instanceof Error\n ? failure.error.message\n : String(failure.error);\n return `${failure.locale}: ${failure.keys.length} key${failure.keys.length > 1 ? \"s\" : \"\"} [${failure.keys.join(\", \")}] — ${message}`;\n });\n}\n\nfunction readTargetFile(targetFilePath: string): Record<string, string> {\n if (!existsSync(targetFilePath)) return {};\n try {\n return JSON.parse(readFileSync(targetFilePath, \"utf-8\"));\n } catch {\n // Corrupted file — regenerate\n return {};\n }\n}\n\nfunction writeTargetFile(\n targetFilePath: string,\n translations: Record<string, string>,\n sourceDict: Record<string, string>,\n): void {\n // Remove keys that no longer exist in source\n for (const key of Object.keys(translations)) {\n if (!(key in sourceDict)) {\n delete translations[key];\n }\n }\n\n // Sort keys for stable, diff-friendly output\n const sorted = Object.fromEntries(\n Object.entries(translations).sort(([a], [b]) => a.localeCompare(b)),\n );\n\n writeFileSync(targetFilePath, JSON.stringify(sorted, null, 2) + \"\\n\");\n}\n","import { createHash } from \"node:crypto\";\n\n/** Create a short content hash for change detection */\nexport function hashContent(content: string): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 16);\n}\n"],"mappings":"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","names":["readFileSync","writeFileSync","existsSync","join","existsSync","join","readFileSync","writeFileSync"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid-translate",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "AI-powered build-time translations for SolidJS. Full i18n with <T>, <Var>, <Num>, <Currency>, <Plural>, <DateTime>, locale detection, and a CLI — all BYOK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"solid-js": ">=1.7.0",
|
|
67
67
|
"vite": ">=4.0.0",
|
|
68
|
-
"ai": ">=3.0.0"
|
|
68
|
+
"ai": ">=3.0.0 <5.0.0"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
71
71
|
"vite": {
|