solid-translate 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -6
- package/dist/chunk-2BKJUY37.js +82 -0
- package/dist/cli.js +351 -163
- package/dist/index.d.ts +33 -5
- package/dist/index.js +65 -9
- package/dist/index.js.map +1 -1
- package/dist/translate-M737VQHG.js +10 -0
- package/dist/vite.js +262 -113
- package/dist/vite.js.map +1 -1
- package/package.json +6 -2
- package/virtual.d.ts +45 -0
package/dist/vite.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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";
|
|
9
|
+
import { resolve, join as join2, relative, basename } from "path";
|
|
10
10
|
|
|
11
11
|
// src/hash.ts
|
|
12
12
|
import { createHash } from "crypto";
|
|
@@ -15,8 +15,11 @@ function hashContent(content) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// src/translate.ts
|
|
18
|
-
import { generateObject } from "ai";
|
|
19
18
|
import { z } from "zod";
|
|
19
|
+
async function loadGenerateObject() {
|
|
20
|
+
const { generateObject } = await import("ai");
|
|
21
|
+
return generateObject;
|
|
22
|
+
}
|
|
20
23
|
async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts) {
|
|
21
24
|
const keys = Object.keys(entries);
|
|
22
25
|
if (keys.length === 0) return {};
|
|
@@ -42,6 +45,7 @@ async function translateBatch(model, entries, targetLocale, sourceLocale, system
|
|
|
42
45
|
].join("\n");
|
|
43
46
|
}
|
|
44
47
|
}
|
|
48
|
+
const generateObject = await loadGenerateObject();
|
|
45
49
|
const { object } = await generateObject({
|
|
46
50
|
model,
|
|
47
51
|
schema: z.object({
|
|
@@ -97,9 +101,169 @@ function extractStringsFromSource(code, filePath) {
|
|
|
97
101
|
return results;
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
// src/lock.ts
|
|
105
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
106
|
+
import { join } from "path";
|
|
107
|
+
function diffLock(sourceDict, lock, contexts) {
|
|
108
|
+
const changedKeys = {};
|
|
109
|
+
const pendingEntries = {};
|
|
110
|
+
for (const [key, value] of Object.entries(sourceDict)) {
|
|
111
|
+
const hash = hashContent(value);
|
|
112
|
+
const existing = lock.keys[key];
|
|
113
|
+
const newContext = contexts ? contexts[key] : existing?.context;
|
|
114
|
+
const contextChanged = contexts !== void 0 && existing?.context !== contexts[key];
|
|
115
|
+
if (!existing || existing.hash !== hash || contextChanged) {
|
|
116
|
+
changedKeys[key] = value;
|
|
117
|
+
pendingEntries[key] = { hash, source: value, context: newContext };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const deletedKeys = Object.keys(lock.keys).filter(
|
|
121
|
+
(key) => !(key in sourceDict)
|
|
122
|
+
);
|
|
123
|
+
return { changedKeys, pendingEntries, deletedKeys };
|
|
124
|
+
}
|
|
125
|
+
async function syncLocaleFiles(options) {
|
|
126
|
+
const {
|
|
127
|
+
localesDir,
|
|
128
|
+
sourceLocale,
|
|
129
|
+
targetLocales,
|
|
130
|
+
batchSize,
|
|
131
|
+
translate,
|
|
132
|
+
contexts,
|
|
133
|
+
log = () => {
|
|
134
|
+
}
|
|
135
|
+
} = options;
|
|
136
|
+
const sourceFilePath = join(localesDir, `${sourceLocale}.json`);
|
|
137
|
+
if (!existsSync(sourceFilePath)) {
|
|
138
|
+
return {
|
|
139
|
+
status: "no-source",
|
|
140
|
+
translatedKeys: [],
|
|
141
|
+
deletedKeys: [],
|
|
142
|
+
failures: []
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const sourceDict = JSON.parse(
|
|
146
|
+
readFileSync(sourceFilePath, "utf-8")
|
|
147
|
+
);
|
|
148
|
+
const lockFilePath = join(localesDir, ".solid-translate.lock");
|
|
149
|
+
let lock = { version: 1, sourceLocale, keys: {} };
|
|
150
|
+
if (existsSync(lockFilePath)) {
|
|
151
|
+
try {
|
|
152
|
+
lock = JSON.parse(readFileSync(lockFilePath, "utf-8"));
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const { changedKeys, pendingEntries, deletedKeys } = diffLock(
|
|
157
|
+
sourceDict,
|
|
158
|
+
lock,
|
|
159
|
+
contexts
|
|
160
|
+
);
|
|
161
|
+
for (const key of deletedKeys) {
|
|
162
|
+
delete lock.keys[key];
|
|
163
|
+
}
|
|
164
|
+
const changedCount = Object.keys(changedKeys).length;
|
|
165
|
+
if (changedCount === 0 && deletedKeys.length === 0) {
|
|
166
|
+
log("No changes detected in locale files.");
|
|
167
|
+
return {
|
|
168
|
+
status: "no-changes",
|
|
169
|
+
translatedKeys: [],
|
|
170
|
+
deletedKeys: [],
|
|
171
|
+
failures: []
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (changedCount === 0) {
|
|
175
|
+
for (const targetLocale of targetLocales) {
|
|
176
|
+
const targetFilePath = join(localesDir, `${targetLocale}.json`);
|
|
177
|
+
const existing = readTargetFile(targetFilePath);
|
|
178
|
+
writeTargetFile(targetFilePath, existing, sourceDict);
|
|
179
|
+
log(` ${targetLocale}: pruned deleted keys`);
|
|
180
|
+
}
|
|
181
|
+
writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
|
|
182
|
+
log(
|
|
183
|
+
`Removed ${deletedKeys.length} deleted key${deletedKeys.length > 1 ? "s" : ""} from target locales.`
|
|
184
|
+
);
|
|
185
|
+
return { status: "synced", translatedKeys: [], deletedKeys, failures: [] };
|
|
186
|
+
}
|
|
187
|
+
log(
|
|
188
|
+
`Translating ${changedCount} key${changedCount > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
|
|
189
|
+
);
|
|
190
|
+
const changedContexts = {};
|
|
191
|
+
for (const key of Object.keys(changedKeys)) {
|
|
192
|
+
const ctx = pendingEntries[key]?.context;
|
|
193
|
+
if (ctx) changedContexts[key] = ctx;
|
|
194
|
+
}
|
|
195
|
+
const failures = [];
|
|
196
|
+
const failedKeys = /* @__PURE__ */ new Set();
|
|
197
|
+
for (const targetLocale of targetLocales) {
|
|
198
|
+
const targetFilePath = join(localesDir, `${targetLocale}.json`);
|
|
199
|
+
const existing = readTargetFile(targetFilePath);
|
|
200
|
+
const entries = Object.entries(changedKeys);
|
|
201
|
+
for (let i = 0; i < entries.length; i += batchSize) {
|
|
202
|
+
const batch = Object.fromEntries(entries.slice(i, i + batchSize));
|
|
203
|
+
try {
|
|
204
|
+
const translated = await translate(
|
|
205
|
+
batch,
|
|
206
|
+
targetLocale,
|
|
207
|
+
changedContexts
|
|
208
|
+
);
|
|
209
|
+
Object.assign(existing, translated);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
failures.push({
|
|
212
|
+
locale: targetLocale,
|
|
213
|
+
keys: Object.keys(batch),
|
|
214
|
+
error: err
|
|
215
|
+
});
|
|
216
|
+
for (const key of Object.keys(batch)) {
|
|
217
|
+
failedKeys.add(key);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
writeTargetFile(targetFilePath, existing, sourceDict);
|
|
222
|
+
log(` ${targetLocale}: ${Object.keys(existing).length} keys`);
|
|
223
|
+
}
|
|
224
|
+
const translatedKeys = [];
|
|
225
|
+
for (const [key, entry] of Object.entries(pendingEntries)) {
|
|
226
|
+
if (failedKeys.has(key)) continue;
|
|
227
|
+
lock.keys[key] = entry;
|
|
228
|
+
translatedKeys.push(key);
|
|
229
|
+
}
|
|
230
|
+
writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
|
|
231
|
+
return { status: "synced", translatedKeys, deletedKeys, failures };
|
|
232
|
+
}
|
|
233
|
+
function formatSyncFailures(failures) {
|
|
234
|
+
return failures.map((failure) => {
|
|
235
|
+
const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
|
|
236
|
+
return `${failure.locale}: ${failure.keys.length} key${failure.keys.length > 1 ? "s" : ""} [${failure.keys.join(", ")}] \u2014 ${message}`;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function readTargetFile(targetFilePath) {
|
|
240
|
+
if (!existsSync(targetFilePath)) return {};
|
|
241
|
+
try {
|
|
242
|
+
return JSON.parse(readFileSync(targetFilePath, "utf-8"));
|
|
243
|
+
} catch {
|
|
244
|
+
return {};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function writeTargetFile(targetFilePath, translations, sourceDict) {
|
|
248
|
+
for (const key of Object.keys(translations)) {
|
|
249
|
+
if (!(key in sourceDict)) {
|
|
250
|
+
delete translations[key];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const sorted = Object.fromEntries(
|
|
254
|
+
Object.entries(translations).sort(([a], [b]) => a.localeCompare(b))
|
|
255
|
+
);
|
|
256
|
+
writeFileSync(targetFilePath, JSON.stringify(sorted, null, 2) + "\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
100
259
|
// src/vite.ts
|
|
101
260
|
var VIRTUAL_MODULE_ID = "virtual:solid-translate";
|
|
102
261
|
var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
|
|
262
|
+
var VIRTUAL_LAZY_MODULE_ID = "virtual:solid-translate/lazy";
|
|
263
|
+
var RESOLVED_VIRTUAL_LAZY_MODULE_ID = "\0" + VIRTUAL_LAZY_MODULE_ID;
|
|
264
|
+
var VIRTUAL_LOCALE_MODULE_PREFIX = "virtual:solid-translate/locale/";
|
|
265
|
+
var RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX = "\0" + VIRTUAL_LOCALE_MODULE_PREFIX;
|
|
266
|
+
var LOCALE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
103
267
|
function solidTranslate(config) {
|
|
104
268
|
const {
|
|
105
269
|
sourceLocale = "en",
|
|
@@ -113,19 +277,17 @@ function solidTranslate(config) {
|
|
|
113
277
|
} = config;
|
|
114
278
|
let root;
|
|
115
279
|
let resolvedLocalesDir;
|
|
116
|
-
let lockFilePath;
|
|
117
280
|
return {
|
|
118
281
|
name: "solid-translate",
|
|
119
282
|
configResolved(resolvedConfig) {
|
|
120
283
|
root = resolvedConfig.root;
|
|
121
284
|
resolvedLocalesDir = resolve(root, localesDir);
|
|
122
|
-
lockFilePath = join(resolvedLocalesDir, ".solid-translate.lock");
|
|
123
285
|
},
|
|
124
286
|
async buildStart() {
|
|
125
|
-
if (!
|
|
287
|
+
if (!existsSync2(resolvedLocalesDir)) {
|
|
126
288
|
mkdirSync(resolvedLocalesDir, { recursive: true });
|
|
127
289
|
}
|
|
128
|
-
const sourceFilePath =
|
|
290
|
+
const sourceFilePath = join2(
|
|
129
291
|
resolvedLocalesDir,
|
|
130
292
|
`${sourceLocale}.json`
|
|
131
293
|
);
|
|
@@ -134,10 +296,10 @@ function solidTranslate(config) {
|
|
|
134
296
|
const extracted = await autoExtractStrings(root, include);
|
|
135
297
|
contexts = extracted.contexts;
|
|
136
298
|
let existingSource = {};
|
|
137
|
-
if (
|
|
299
|
+
if (existsSync2(sourceFilePath)) {
|
|
138
300
|
try {
|
|
139
301
|
existingSource = JSON.parse(
|
|
140
|
-
|
|
302
|
+
readFileSync2(sourceFilePath, "utf-8")
|
|
141
303
|
);
|
|
142
304
|
} catch {
|
|
143
305
|
}
|
|
@@ -155,7 +317,7 @@ function solidTranslate(config) {
|
|
|
155
317
|
([a], [b]) => a.localeCompare(b)
|
|
156
318
|
)
|
|
157
319
|
);
|
|
158
|
-
|
|
320
|
+
writeFileSync2(
|
|
159
321
|
sourceFilePath,
|
|
160
322
|
JSON.stringify(sorted, null, 2) + "\n"
|
|
161
323
|
);
|
|
@@ -164,7 +326,7 @@ function solidTranslate(config) {
|
|
|
164
326
|
);
|
|
165
327
|
}
|
|
166
328
|
}
|
|
167
|
-
if (!
|
|
329
|
+
if (!existsSync2(sourceFilePath)) {
|
|
168
330
|
console.warn(
|
|
169
331
|
`[solid-translate] Source locale file not found: ${relative(root, sourceFilePath)}`
|
|
170
332
|
);
|
|
@@ -173,117 +335,63 @@ function solidTranslate(config) {
|
|
|
173
335
|
);
|
|
174
336
|
return;
|
|
175
337
|
}
|
|
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."
|
|
338
|
+
const result = await syncLocaleFiles({
|
|
339
|
+
localesDir: resolvedLocalesDir,
|
|
340
|
+
sourceLocale,
|
|
341
|
+
targetLocales,
|
|
342
|
+
batchSize,
|
|
343
|
+
// Only pass extraction contexts when autoExtract ran; otherwise
|
|
344
|
+
// preserve the contexts already recorded in the lock file.
|
|
345
|
+
contexts: autoExtract ? contexts : void 0,
|
|
346
|
+
translate: (batch, targetLocale, changedContexts) => translateBatch(
|
|
347
|
+
model,
|
|
348
|
+
batch,
|
|
349
|
+
targetLocale,
|
|
350
|
+
sourceLocale,
|
|
351
|
+
systemPrompt,
|
|
352
|
+
changedContexts
|
|
353
|
+
),
|
|
354
|
+
log: (message) => console.log(`[solid-translate] ${message}`)
|
|
355
|
+
});
|
|
356
|
+
if (result.failures.length > 0) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
[
|
|
359
|
+
"[solid-translate] Translation failed for some batches:",
|
|
360
|
+
...formatSyncFailures(result.failures).map((line) => ` ${line}`),
|
|
361
|
+
"Failed keys were not recorded in the lock file \u2014 fix the error and rebuild to retry them."
|
|
362
|
+
].join("\n")
|
|
205
363
|
);
|
|
206
|
-
return;
|
|
207
364
|
}
|
|
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;
|
|
365
|
+
if (result.status === "synced") {
|
|
366
|
+
console.log("[solid-translate] Translation complete.");
|
|
216
367
|
}
|
|
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
|
-
);
|
|
266
|
-
}
|
|
267
|
-
writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
|
|
268
|
-
console.log("[solid-translate] Translation complete.");
|
|
269
368
|
},
|
|
270
369
|
resolveId(id) {
|
|
271
370
|
if (id === VIRTUAL_MODULE_ID) {
|
|
272
371
|
return RESOLVED_VIRTUAL_MODULE_ID;
|
|
273
372
|
}
|
|
373
|
+
if (id === VIRTUAL_LAZY_MODULE_ID) {
|
|
374
|
+
return RESOLVED_VIRTUAL_LAZY_MODULE_ID;
|
|
375
|
+
}
|
|
376
|
+
if (id.startsWith(VIRTUAL_LOCALE_MODULE_PREFIX)) {
|
|
377
|
+
const locale = id.slice(VIRTUAL_LOCALE_MODULE_PREFIX.length);
|
|
378
|
+
if (LOCALE_ID_PATTERN.test(locale)) {
|
|
379
|
+
return RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX + locale;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
274
382
|
},
|
|
275
383
|
load(id) {
|
|
276
384
|
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
277
385
|
const translations = {};
|
|
278
|
-
if (
|
|
386
|
+
if (existsSync2(resolvedLocalesDir)) {
|
|
279
387
|
for (const file of readdirSync(resolvedLocalesDir)) {
|
|
280
388
|
if (!file.endsWith(".json")) continue;
|
|
281
389
|
if (file.startsWith(".")) continue;
|
|
282
390
|
const locale = file.replace(".json", "");
|
|
283
|
-
const filePath =
|
|
391
|
+
const filePath = join2(resolvedLocalesDir, file);
|
|
284
392
|
try {
|
|
285
393
|
translations[locale] = JSON.parse(
|
|
286
|
-
|
|
394
|
+
readFileSync2(filePath, "utf-8")
|
|
287
395
|
);
|
|
288
396
|
} catch {
|
|
289
397
|
}
|
|
@@ -291,16 +399,57 @@ function solidTranslate(config) {
|
|
|
291
399
|
}
|
|
292
400
|
return `export default ${JSON.stringify(translations)};`;
|
|
293
401
|
}
|
|
402
|
+
if (id === RESOLVED_VIRTUAL_LAZY_MODULE_ID) {
|
|
403
|
+
const locales = [sourceLocale, ...targetLocales].filter(
|
|
404
|
+
(locale, i, all) => all.indexOf(locale) === i
|
|
405
|
+
);
|
|
406
|
+
const loaderEntries = locales.map(
|
|
407
|
+
(locale) => ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(
|
|
408
|
+
VIRTUAL_LOCALE_MODULE_PREFIX + locale
|
|
409
|
+
)}).then((m) => m.default),`
|
|
410
|
+
).join("\n");
|
|
411
|
+
return [
|
|
412
|
+
`export const sourceLocale = ${JSON.stringify(sourceLocale)};`,
|
|
413
|
+
`export const locales = ${JSON.stringify(locales)};`,
|
|
414
|
+
`export const loaders = {`,
|
|
415
|
+
loaderEntries,
|
|
416
|
+
`};`,
|
|
417
|
+
`export default { sourceLocale, locales, loaders };`
|
|
418
|
+
].join("\n");
|
|
419
|
+
}
|
|
420
|
+
if (id.startsWith(RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX)) {
|
|
421
|
+
const locale = id.slice(
|
|
422
|
+
RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX.length
|
|
423
|
+
);
|
|
424
|
+
let dict = {};
|
|
425
|
+
const filePath = join2(resolvedLocalesDir, `${locale}.json`);
|
|
426
|
+
if (existsSync2(filePath)) {
|
|
427
|
+
try {
|
|
428
|
+
dict = JSON.parse(readFileSync2(filePath, "utf-8"));
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return `export default ${JSON.stringify(dict)};`;
|
|
433
|
+
}
|
|
294
434
|
},
|
|
295
435
|
// HMR: reload translations when locale files change
|
|
296
436
|
handleHotUpdate({ file, server }) {
|
|
297
437
|
if (file.startsWith(resolvedLocalesDir) && file.endsWith(".json")) {
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
438
|
+
const invalidated = [];
|
|
439
|
+
const locale = basename(file, ".json");
|
|
440
|
+
const ids = [
|
|
441
|
+
RESOLVED_VIRTUAL_MODULE_ID,
|
|
442
|
+
RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX + locale
|
|
443
|
+
];
|
|
444
|
+
for (const id of ids) {
|
|
445
|
+
const mod = server.moduleGraph.getModuleById(id);
|
|
446
|
+
if (mod) {
|
|
447
|
+
server.moduleGraph.invalidateModule(mod);
|
|
448
|
+
invalidated.push(mod);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (invalidated.length > 0) {
|
|
452
|
+
return invalidated;
|
|
304
453
|
}
|
|
305
454
|
}
|
|
306
455
|
}
|
|
@@ -315,7 +464,7 @@ async function autoExtractStrings(root, patterns) {
|
|
|
315
464
|
const files = await glob(pattern, { cwd: root, absolute: true });
|
|
316
465
|
for (const file of files) {
|
|
317
466
|
try {
|
|
318
|
-
const code =
|
|
467
|
+
const code = readFileSync2(file, "utf-8");
|
|
319
468
|
const extracted = extractStringsFromSource(
|
|
320
469
|
code,
|
|
321
470
|
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/hash.ts","../src/translate.ts","../src/extract.ts","../src/lock.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, basename } from \"node:path\";\nimport { hashContent } from \"./hash.js\";\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\nconst VIRTUAL_LAZY_MODULE_ID = \"virtual:solid-translate/lazy\";\nconst RESOLVED_VIRTUAL_LAZY_MODULE_ID = \"\\0\" + VIRTUAL_LAZY_MODULE_ID;\n\nconst VIRTUAL_LOCALE_MODULE_PREFIX = \"virtual:solid-translate/locale/\";\nconst RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX =\n \"\\0\" + VIRTUAL_LOCALE_MODULE_PREFIX;\n\n/** Locale codes must be simple path-safe tokens (e.g. \"en\", \"pt-BR\", \"zh_Hant\") */\nconst LOCALE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;\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 if (id === VIRTUAL_LAZY_MODULE_ID) {\n return RESOLVED_VIRTUAL_LAZY_MODULE_ID;\n }\n if (id.startsWith(VIRTUAL_LOCALE_MODULE_PREFIX)) {\n const locale = id.slice(VIRTUAL_LOCALE_MODULE_PREFIX.length);\n if (LOCALE_ID_PATTERN.test(locale)) {\n return RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX + locale;\n }\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 if (id === RESOLVED_VIRTUAL_LAZY_MODULE_ID) {\n // Lazy manifest: per-locale dictionaries stay out of the main bundle\n // and are code-split into their own chunks via dynamic import.\n const locales = [sourceLocale, ...targetLocales].filter(\n (locale, i, all) => all.indexOf(locale) === i,\n );\n const loaderEntries = locales\n .map(\n (locale) =>\n ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(\n VIRTUAL_LOCALE_MODULE_PREFIX + locale,\n )}).then((m) => m.default),`,\n )\n .join(\"\\n\");\n return [\n `export const sourceLocale = ${JSON.stringify(sourceLocale)};`,\n `export const locales = ${JSON.stringify(locales)};`,\n `export const loaders = {`,\n loaderEntries,\n `};`,\n `export default { sourceLocale, locales, loaders };`,\n ].join(\"\\n\");\n }\n\n if (id.startsWith(RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX)) {\n // Single locale dictionary module\n const locale = id.slice(\n RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX.length,\n );\n let dict: Record<string, string> = {};\n const filePath = join(resolvedLocalesDir, `${locale}.json`);\n if (existsSync(filePath)) {\n try {\n dict = JSON.parse(readFileSync(filePath, \"utf-8\"));\n } catch {\n // Malformed file — serve empty dict\n }\n }\n return `export default ${JSON.stringify(dict)};`;\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 invalidated = [];\n const locale = basename(file, \".json\");\n const ids = [\n RESOLVED_VIRTUAL_MODULE_ID,\n RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX + locale,\n ];\n for (const id of ids) {\n const mod = server.moduleGraph.getModuleById(id);\n if (mod) {\n server.moduleGraph.invalidateModule(mod);\n invalidated.push(mod);\n }\n }\n if (invalidated.length > 0) {\n return invalidated;\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 { 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"],"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.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
@@ -17,10 +17,14 @@
|
|
|
17
17
|
"./vite": {
|
|
18
18
|
"types": "./dist/vite.d.ts",
|
|
19
19
|
"import": "./dist/vite.js"
|
|
20
|
+
},
|
|
21
|
+
"./virtual": {
|
|
22
|
+
"types": "./virtual.d.ts"
|
|
20
23
|
}
|
|
21
24
|
},
|
|
22
25
|
"files": [
|
|
23
26
|
"dist",
|
|
27
|
+
"virtual.d.ts",
|
|
24
28
|
"README.md",
|
|
25
29
|
"LICENSE"
|
|
26
30
|
],
|
|
@@ -65,7 +69,7 @@
|
|
|
65
69
|
"peerDependencies": {
|
|
66
70
|
"solid-js": ">=1.7.0",
|
|
67
71
|
"vite": ">=4.0.0",
|
|
68
|
-
"ai": ">=3.0.0"
|
|
72
|
+
"ai": ">=3.0.0 <5.0.0"
|
|
69
73
|
},
|
|
70
74
|
"peerDependenciesMeta": {
|
|
71
75
|
"vite": {
|