react-autolocale 0.1.0 → 0.3.0-beta.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 +248 -16
- package/dist/cli.js +800 -46
- package/dist/config-CYHyGpdX.js +21 -0
- package/dist/config.d.ts +56 -0
- package/dist/config.js +1 -0
- package/dist/index.d.ts +64 -3
- package/dist/index.js +154 -24
- package/dist/project-CGAWETvT.js +1400 -0
- package/dist/{redirect-CKNY4ru5.js → redirect-pDFblLdm.js} +5 -2
- package/dist/suggestion-CkZQqMvZ.js +24 -0
- package/dist/translate-COrJX1KS.d.ts +22 -0
- package/dist/vite.d.ts +12 -3
- package/dist/vite.js +82 -782
- package/package.json +22 -6
package/dist/vite.js
CHANGED
|
@@ -1,692 +1,14 @@
|
|
|
1
|
-
import { n as redirectScript } from "./redirect-
|
|
2
|
-
import
|
|
1
|
+
import { n as redirectScript } from "./redirect-pDFblLdm.js";
|
|
2
|
+
import { A as readBanner, C as isSourceFile, D as translateHead, E as transformJsx, I as getLanguage, O as messageId, P as ollama, T as parserPlugins, _ as loadDicts, d as OVERRIDES_FILE, m as readOverrides, p as mergeOverrides, r as loadProject, w as walk } from "./project-CGAWETvT.js";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import os from "node:os";
|
|
7
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { JSDOM, VirtualConsole } from "jsdom";
|
|
8
6
|
import { parse } from "@babel/parser";
|
|
9
7
|
import _traverse from "@babel/traverse";
|
|
10
|
-
import _generate from "@babel/generator";
|
|
11
8
|
import * as t from "@babel/types";
|
|
12
|
-
import { JSDOM, VirtualConsole } from "jsdom";
|
|
13
9
|
import { createServer } from "vite";
|
|
14
|
-
const byCode = new Map([
|
|
15
|
-
{
|
|
16
|
-
code: "en",
|
|
17
|
-
native: "English",
|
|
18
|
-
english: "English",
|
|
19
|
-
nllb: "eng_Latn"
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
code: "fr",
|
|
23
|
-
native: "Français",
|
|
24
|
-
english: "French",
|
|
25
|
-
nllb: "fra_Latn",
|
|
26
|
-
opus: "fr"
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
code: "es",
|
|
30
|
-
native: "Español",
|
|
31
|
-
english: "Spanish",
|
|
32
|
-
nllb: "spa_Latn",
|
|
33
|
-
opus: "es"
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
code: "de",
|
|
37
|
-
native: "Deutsch",
|
|
38
|
-
english: "German",
|
|
39
|
-
nllb: "deu_Latn",
|
|
40
|
-
opus: "de"
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
code: "it",
|
|
44
|
-
native: "Italiano",
|
|
45
|
-
english: "Italian",
|
|
46
|
-
nllb: "ita_Latn",
|
|
47
|
-
opus: "it"
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
code: "pt",
|
|
51
|
-
native: "Português",
|
|
52
|
-
english: "Portuguese",
|
|
53
|
-
nllb: "por_Latn"
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
code: "nl",
|
|
57
|
-
native: "Nederlands",
|
|
58
|
-
english: "Dutch",
|
|
59
|
-
nllb: "nld_Latn",
|
|
60
|
-
opus: "nl"
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
code: "ru",
|
|
64
|
-
native: "Русский",
|
|
65
|
-
english: "Russian",
|
|
66
|
-
nllb: "rus_Cyrl",
|
|
67
|
-
opus: "ru"
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
code: "zh",
|
|
71
|
-
native: "中文",
|
|
72
|
-
english: "Chinese (Simplified)",
|
|
73
|
-
nllb: "zho_Hans",
|
|
74
|
-
opus: "zh"
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
code: "ja",
|
|
78
|
-
native: "日本語",
|
|
79
|
-
english: "Japanese",
|
|
80
|
-
nllb: "jpn_Jpan"
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
code: "ko",
|
|
84
|
-
native: "한국어",
|
|
85
|
-
english: "Korean",
|
|
86
|
-
nllb: "kor_Hang"
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
code: "ar",
|
|
90
|
-
native: "العربية",
|
|
91
|
-
english: "Arabic",
|
|
92
|
-
nllb: "arb_Arab",
|
|
93
|
-
opus: "ar",
|
|
94
|
-
rtl: true
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
code: "hi",
|
|
98
|
-
native: "हिन्दी",
|
|
99
|
-
english: "Hindi",
|
|
100
|
-
nllb: "hin_Deva",
|
|
101
|
-
opus: "hi"
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
code: "tr",
|
|
105
|
-
native: "Türkçe",
|
|
106
|
-
english: "Turkish",
|
|
107
|
-
nllb: "tur_Latn"
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
code: "pl",
|
|
111
|
-
native: "Polski",
|
|
112
|
-
english: "Polish",
|
|
113
|
-
nllb: "pol_Latn"
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
code: "sv",
|
|
117
|
-
native: "Svenska",
|
|
118
|
-
english: "Swedish",
|
|
119
|
-
nllb: "swe_Latn",
|
|
120
|
-
opus: "sv"
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
code: "da",
|
|
124
|
-
native: "Dansk",
|
|
125
|
-
english: "Danish",
|
|
126
|
-
nllb: "dan_Latn",
|
|
127
|
-
opus: "da"
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
code: "no",
|
|
131
|
-
native: "Norsk",
|
|
132
|
-
english: "Norwegian",
|
|
133
|
-
nllb: "nob_Latn"
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
code: "fi",
|
|
137
|
-
native: "Suomi",
|
|
138
|
-
english: "Finnish",
|
|
139
|
-
nllb: "fin_Latn",
|
|
140
|
-
opus: "fi"
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
code: "cs",
|
|
144
|
-
native: "Čeština",
|
|
145
|
-
english: "Czech",
|
|
146
|
-
nllb: "ces_Latn",
|
|
147
|
-
opus: "cs"
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
code: "hu",
|
|
151
|
-
native: "Magyar",
|
|
152
|
-
english: "Hungarian",
|
|
153
|
-
nllb: "hun_Latn",
|
|
154
|
-
opus: "hu"
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
code: "ro",
|
|
158
|
-
native: "Română",
|
|
159
|
-
english: "Romanian",
|
|
160
|
-
nllb: "ron_Latn",
|
|
161
|
-
opus: "ro"
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
code: "uk",
|
|
165
|
-
native: "Українська",
|
|
166
|
-
english: "Ukrainian",
|
|
167
|
-
nllb: "ukr_Cyrl",
|
|
168
|
-
opus: "uk"
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
code: "el",
|
|
172
|
-
native: "Ελληνικά",
|
|
173
|
-
english: "Greek",
|
|
174
|
-
nllb: "ell_Grek"
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
code: "he",
|
|
178
|
-
native: "עברית",
|
|
179
|
-
english: "Hebrew",
|
|
180
|
-
nllb: "heb_Hebr",
|
|
181
|
-
rtl: true
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
code: "vi",
|
|
185
|
-
native: "Tiếng Việt",
|
|
186
|
-
english: "Vietnamese",
|
|
187
|
-
nllb: "vie_Latn",
|
|
188
|
-
opus: "vi"
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
code: "id",
|
|
192
|
-
native: "Bahasa Indonesia",
|
|
193
|
-
english: "Indonesian",
|
|
194
|
-
nllb: "ind_Latn",
|
|
195
|
-
opus: "id"
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
code: "th",
|
|
199
|
-
native: "ไทย",
|
|
200
|
-
english: "Thai",
|
|
201
|
-
nllb: "tha_Thai"
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
code: "bn",
|
|
205
|
-
native: "বাংলা",
|
|
206
|
-
english: "Bengali",
|
|
207
|
-
nllb: "ben_Beng"
|
|
208
|
-
},
|
|
209
|
-
{
|
|
210
|
-
code: "fa",
|
|
211
|
-
native: "فارسی",
|
|
212
|
-
english: "Persian",
|
|
213
|
-
nllb: "pes_Arab",
|
|
214
|
-
rtl: true
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
code: "ur",
|
|
218
|
-
native: "اردو",
|
|
219
|
-
english: "Urdu",
|
|
220
|
-
nllb: "urd_Arab",
|
|
221
|
-
rtl: true
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
code: "bg",
|
|
225
|
-
native: "Български",
|
|
226
|
-
english: "Bulgarian",
|
|
227
|
-
nllb: "bul_Cyrl"
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
code: "hr",
|
|
231
|
-
native: "Hrvatski",
|
|
232
|
-
english: "Croatian",
|
|
233
|
-
nllb: "hrv_Latn"
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
code: "sk",
|
|
237
|
-
native: "Slovenčina",
|
|
238
|
-
english: "Slovak",
|
|
239
|
-
nllb: "slk_Latn"
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
code: "ca",
|
|
243
|
-
native: "Català",
|
|
244
|
-
english: "Catalan",
|
|
245
|
-
nllb: "cat_Latn"
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
code: "ms",
|
|
249
|
-
native: "Bahasa Melayu",
|
|
250
|
-
english: "Malay",
|
|
251
|
-
nllb: "zsm_Latn"
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
code: "sw",
|
|
255
|
-
native: "Kiswahili",
|
|
256
|
-
english: "Swahili",
|
|
257
|
-
nllb: "swh_Latn"
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
code: "tl",
|
|
261
|
-
native: "Tagalog",
|
|
262
|
-
english: "Tagalog",
|
|
263
|
-
nllb: "tgl_Latn"
|
|
264
|
-
}
|
|
265
|
-
].map((l) => [l.code, l]));
|
|
266
|
-
function getLanguage(code) {
|
|
267
|
-
return byCode.get(code);
|
|
268
|
-
}
|
|
269
|
-
//#endregion
|
|
270
|
-
//#region src/core/translate.ts
|
|
271
|
-
const ENGINE_PACKAGE = "@huggingface/transformers@^4.3.1";
|
|
272
|
-
const ENGINE_DIR = path.join(os.homedir(), ".cache", "react-autolocale", "engine");
|
|
273
|
-
/**
|
|
274
|
-
* Finds the translation engine. Uses the project's own copy if it has one; otherwise installs it once
|
|
275
|
-
* into ~/.cache/react-autolocale/engine (shared by all projects, never touches the project's node_modules).
|
|
276
|
-
*/
|
|
277
|
-
async function loadEngine(log) {
|
|
278
|
-
try {
|
|
279
|
-
return await import("@huggingface/transformers");
|
|
280
|
-
} catch {}
|
|
281
|
-
const require = createRequire(path.join(ENGINE_DIR, "package.json"));
|
|
282
|
-
const find = () => require.resolve("@huggingface/transformers");
|
|
283
|
-
let entry;
|
|
284
|
-
try {
|
|
285
|
-
entry = find();
|
|
286
|
-
} catch {
|
|
287
|
-
log("[react-autolocale] Installing the translation engine (one time, about 440 MB)...");
|
|
288
|
-
fs.mkdirSync(ENGINE_DIR, { recursive: true });
|
|
289
|
-
const pkg = path.join(ENGINE_DIR, "package.json");
|
|
290
|
-
if (!fs.existsSync(pkg)) fs.writeFileSync(pkg, JSON.stringify({
|
|
291
|
-
name: "react-autolocale-engine",
|
|
292
|
-
private: true
|
|
293
|
-
}));
|
|
294
|
-
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
295
|
-
const res = spawnSync(npm, [
|
|
296
|
-
"install",
|
|
297
|
-
"--prefix",
|
|
298
|
-
ENGINE_DIR,
|
|
299
|
-
"--no-audit",
|
|
300
|
-
"--no-fund",
|
|
301
|
-
"--loglevel=error",
|
|
302
|
-
ENGINE_PACKAGE
|
|
303
|
-
], {
|
|
304
|
-
stdio: "inherit",
|
|
305
|
-
shell: process.platform === "win32"
|
|
306
|
-
});
|
|
307
|
-
try {
|
|
308
|
-
if (res.status !== 0) throw new Error("npm failed");
|
|
309
|
-
entry = find();
|
|
310
|
-
} catch {
|
|
311
|
-
throw new Error(`Could not install the translation engine automatically. Install it yourself: npm install -D @huggingface/transformers`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
const mod = await import(pathToFileURL(entry).href);
|
|
315
|
-
return mod.pipeline ? mod : mod.default;
|
|
316
|
-
}
|
|
317
|
-
const NLLB_MODEL = "Xenova/nllb-200-distilled-600M";
|
|
318
|
-
const BATCH = 1;
|
|
319
|
-
/**
|
|
320
|
-
* Free, offline translation using Transformers.js (ONNX models run on the CPU).
|
|
321
|
-
* Models are downloaded once from Hugging Face and cached in ~/.cache/react-autolocale.
|
|
322
|
-
* English -> X uses small per-language OPUS-MT models where they exist; everything else uses NLLB-200.
|
|
323
|
-
*/
|
|
324
|
-
function createLocalTranslator(log = () => {}) {
|
|
325
|
-
const pipes = /* @__PURE__ */ new Map();
|
|
326
|
-
let hf = null;
|
|
327
|
-
const getPipe = (model) => {
|
|
328
|
-
let p = pipes.get(model);
|
|
329
|
-
if (!p) {
|
|
330
|
-
hf ??= loadEngine(log).then((m) => {
|
|
331
|
-
m.env.cacheDir = path.join(os.homedir(), ".cache", "react-autolocale", "models");
|
|
332
|
-
return m;
|
|
333
|
-
});
|
|
334
|
-
log(`Loading model ${model} (first run downloads it)...`);
|
|
335
|
-
let lastPct = -10;
|
|
336
|
-
p = hf.then((m) => m.pipeline("translation", model, {
|
|
337
|
-
dtype: "q8",
|
|
338
|
-
progress_callback: (e) => {
|
|
339
|
-
if (e.status === "progress" && e.progress - lastPct >= 10) {
|
|
340
|
-
lastPct = e.progress;
|
|
341
|
-
log(` downloading ${e.file}: ${Math.round(e.progress)}%`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}));
|
|
345
|
-
pipes.set(model, p);
|
|
346
|
-
}
|
|
347
|
-
return p;
|
|
348
|
-
};
|
|
349
|
-
const run = async (model, texts, options) => {
|
|
350
|
-
const pipe = await getPipe(model);
|
|
351
|
-
const out = [];
|
|
352
|
-
for (let i = 0; i < texts.length; i += BATCH) {
|
|
353
|
-
const res = await pipe(texts.slice(i, i + BATCH), {
|
|
354
|
-
max_new_tokens: 256,
|
|
355
|
-
...options
|
|
356
|
-
});
|
|
357
|
-
out.push(...res.map((r) => r.translation_text.trim()));
|
|
358
|
-
}
|
|
359
|
-
return out;
|
|
360
|
-
};
|
|
361
|
-
return { async translate(texts, from, to) {
|
|
362
|
-
const src = getLanguage(from);
|
|
363
|
-
const dst = getLanguage(to);
|
|
364
|
-
if (!src || !dst) throw new Error(`Unsupported language: ${!src ? from : to}`);
|
|
365
|
-
if (from === "en" && dst.opus) try {
|
|
366
|
-
return await run(`Xenova/opus-mt-en-${dst.opus}`, texts, {});
|
|
367
|
-
} catch (err) {
|
|
368
|
-
log(` OPUS-MT model for "${to}" unavailable (${err.message}); falling back to NLLB.`);
|
|
369
|
-
}
|
|
370
|
-
return run(NLLB_MODEL, texts, {
|
|
371
|
-
src_lang: src.nllb,
|
|
372
|
-
tgt_lang: dst.nllb
|
|
373
|
-
});
|
|
374
|
-
} };
|
|
375
|
-
}
|
|
376
|
-
const token = (i) => `X${i}X`;
|
|
377
|
-
/** Swaps excluded phrases for placeholders so the model leaves them alone. */
|
|
378
|
-
function mask(text, exclude) {
|
|
379
|
-
const phrases = [];
|
|
380
|
-
let masked = text;
|
|
381
|
-
for (const phrase of [...exclude].sort((a, b) => b.length - a.length)) {
|
|
382
|
-
if (!phrase || !masked.includes(phrase)) continue;
|
|
383
|
-
masked = masked.split(phrase).join(token(phrases.length));
|
|
384
|
-
phrases.push(phrase);
|
|
385
|
-
}
|
|
386
|
-
return {
|
|
387
|
-
masked,
|
|
388
|
-
phrases
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
/** Puts excluded phrases back. Returns null if the model dropped or mangled a placeholder. */
|
|
392
|
-
function unmask(translated, phrases) {
|
|
393
|
-
let out = translated;
|
|
394
|
-
for (let i = 0; i < phrases.length; i++) {
|
|
395
|
-
if (!out.includes(token(i))) return null;
|
|
396
|
-
out = out.split(token(i)).join(phrases[i]);
|
|
397
|
-
}
|
|
398
|
-
return /X\d+X/.test(out) ? null : out;
|
|
399
|
-
}
|
|
400
|
-
//#endregion
|
|
401
|
-
//#region src/core/cache.ts
|
|
402
|
-
/** Generated translations live here, outside the source tree. */
|
|
403
|
-
function cacheDir(root) {
|
|
404
|
-
return path.join(root, "node_modules", ".cache", "react-autolocale");
|
|
405
|
-
}
|
|
406
|
-
function cacheFile(root, lang) {
|
|
407
|
-
return path.join(cacheDir(root), `${lang}.json`);
|
|
408
|
-
}
|
|
409
|
-
function readDict(file) {
|
|
410
|
-
try {
|
|
411
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
412
|
-
} catch {
|
|
413
|
-
return {};
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
/** Strings that are entirely an excluded phrase need no translation. */
|
|
417
|
-
function needsTranslation(s, exclude) {
|
|
418
|
-
return !exclude.includes(s.trim());
|
|
419
|
-
}
|
|
420
|
-
/** Translates only strings missing from the cache, drops unused ones, and returns every language's dictionary. */
|
|
421
|
-
async function ensureTranslations(opts) {
|
|
422
|
-
const { root, site, strings } = opts;
|
|
423
|
-
const log = opts.log ?? (() => {});
|
|
424
|
-
const wanted = strings.filter((s) => needsTranslation(s, site.exclude));
|
|
425
|
-
const result = {};
|
|
426
|
-
fs.mkdirSync(cacheDir(root), { recursive: true });
|
|
427
|
-
for (const lang of site.languages) {
|
|
428
|
-
const file = cacheFile(root, lang);
|
|
429
|
-
const existing = readDict(file);
|
|
430
|
-
const fixed = opts.overrides?.[lang] ?? {};
|
|
431
|
-
const missing = wanted.filter((s) => !existing[s] && !fixed[s]);
|
|
432
|
-
const next = {};
|
|
433
|
-
for (const s of wanted) if (existing[s]) next[s] = existing[s];
|
|
434
|
-
for (const s of wanted) if (fixed[s]) next[s] = fixed[s];
|
|
435
|
-
if (missing.length) {
|
|
436
|
-
log(`[react-autolocale] ${lang}: translating ${missing.length} new string${missing.length === 1 ? "" : "s"}...`);
|
|
437
|
-
const jobs = missing.map((s) => ({
|
|
438
|
-
source: s,
|
|
439
|
-
...mask(s, site.exclude)
|
|
440
|
-
}));
|
|
441
|
-
const out = await opts.getTranslator().translate(jobs.map((j) => j.masked), site.original, lang);
|
|
442
|
-
jobs.forEach((job, i) => {
|
|
443
|
-
const restored = unmask(out[i] ?? "", job.phrases);
|
|
444
|
-
if (restored) next[job.source] = restored;
|
|
445
|
-
else log(`[react-autolocale] ${lang}: kept "${job.source}" untranslated (could not preserve excluded text)`);
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
if (!(JSON.stringify(existing) === JSON.stringify(next))) fs.writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
|
|
449
|
-
result[lang] = next;
|
|
450
|
-
}
|
|
451
|
-
return result;
|
|
452
|
-
}
|
|
453
|
-
//#endregion
|
|
454
|
-
//#region src/core/transform.ts
|
|
455
|
-
const traverse$2 = _traverse.default ?? _traverse;
|
|
456
|
-
const generate = _generate.default ?? _generate;
|
|
457
|
-
/** Attributes whose string value is user-visible text. */
|
|
458
|
-
const TRANSLATABLE_ATTRS = /* @__PURE__ */ new Set([
|
|
459
|
-
"placeholder",
|
|
460
|
-
"title",
|
|
461
|
-
"alt",
|
|
462
|
-
"aria-label",
|
|
463
|
-
"aria-description"
|
|
464
|
-
]);
|
|
465
|
-
/** Elements whose text content is code, not copy. */
|
|
466
|
-
const SKIP_ELEMENTS = /* @__PURE__ */ new Set([
|
|
467
|
-
"script",
|
|
468
|
-
"style",
|
|
469
|
-
"code",
|
|
470
|
-
"pre",
|
|
471
|
-
"kbd",
|
|
472
|
-
"samp",
|
|
473
|
-
"svg",
|
|
474
|
-
"textarea"
|
|
475
|
-
]);
|
|
476
|
-
const T_IMPORT = "__RLT";
|
|
477
|
-
const HOOK_IMPORT = "__rlUseT";
|
|
478
|
-
const HOOK_VAR = "__rl";
|
|
479
|
-
const RUNTIME = "react-autolocale";
|
|
480
|
-
function parserPlugins(file) {
|
|
481
|
-
if (/\.tsx$/.test(file)) return ["typescript", "jsx"];
|
|
482
|
-
if (/\.ts$/.test(file)) return ["typescript"];
|
|
483
|
-
return ["jsx"];
|
|
484
|
-
}
|
|
485
|
-
/** Collapses JSX text the way React does: lines are trimmed and joined with one space. */
|
|
486
|
-
function cleanJsxText(raw) {
|
|
487
|
-
const lines = raw.split(/\r\n|\n|\r/);
|
|
488
|
-
let last = 0;
|
|
489
|
-
for (let i = 0; i < lines.length; i++) if (/[^ \t]/.test(lines[i])) last = i;
|
|
490
|
-
let out = "";
|
|
491
|
-
for (let i = 0; i < lines.length; i++) {
|
|
492
|
-
let line = lines[i].replace(/\t/g, " ");
|
|
493
|
-
if (i !== 0) line = line.replace(/^[ ]+/, "");
|
|
494
|
-
if (i !== lines.length - 1) line = line.replace(/[ ]+$/, "");
|
|
495
|
-
if (line) {
|
|
496
|
-
if (i !== last) line += " ";
|
|
497
|
-
out += line;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return out;
|
|
501
|
-
}
|
|
502
|
-
/** Text worth translating: must contain at least one letter in any script. */
|
|
503
|
-
function isTranslatable(text) {
|
|
504
|
-
return /\p{L}/u.test(text);
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Text next to a `{expression}` is a sentence fragment ("Hello {name}"): translating the
|
|
508
|
-
* pieces separately gives wrong results, so dynamic content is left for v2.
|
|
509
|
-
*/
|
|
510
|
-
function hasDynamicChild(node) {
|
|
511
|
-
return node.children.some((c) => t.isJSXExpressionContainer(c) && !t.isJSXEmptyExpression(c.expression) && !t.isStringLiteral(c.expression));
|
|
512
|
-
}
|
|
513
|
-
function isComponentName(name) {
|
|
514
|
-
return !!name && /^[A-Z]/.test(name);
|
|
515
|
-
}
|
|
516
|
-
/** Finds the function that is a React component (PascalCase name, possibly wrapped in memo/forwardRef). */
|
|
517
|
-
function enclosingComponent(path) {
|
|
518
|
-
let p = path.parentPath;
|
|
519
|
-
while (p) {
|
|
520
|
-
if (p.isFunction()) {
|
|
521
|
-
const fn = p;
|
|
522
|
-
let name;
|
|
523
|
-
if ((fn.node.type === "FunctionDeclaration" || fn.node.type === "FunctionExpression") && fn.node.id) name = fn.node.id.name;
|
|
524
|
-
if (!name) {
|
|
525
|
-
let up = fn.parentPath;
|
|
526
|
-
while (up && up.isCallExpression()) up = up.parentPath;
|
|
527
|
-
if (up?.isVariableDeclarator() && t.isIdentifier(up.node.id)) name = up.node.id.name;
|
|
528
|
-
}
|
|
529
|
-
if (isComponentName(name)) return fn;
|
|
530
|
-
}
|
|
531
|
-
p = p.parentPath;
|
|
532
|
-
}
|
|
533
|
-
return null;
|
|
534
|
-
}
|
|
535
|
-
/** Parses a file, extracts translatable strings, and rewrites JSX to render translations. */
|
|
536
|
-
function transformJsx(code, file, opts = {}) {
|
|
537
|
-
const rewrite = opts.rewrite !== false;
|
|
538
|
-
if (!code.includes("<")) return null;
|
|
539
|
-
let ast;
|
|
540
|
-
try {
|
|
541
|
-
ast = parse(code, {
|
|
542
|
-
sourceType: "module",
|
|
543
|
-
sourceFilename: file,
|
|
544
|
-
plugins: parserPlugins(file)
|
|
545
|
-
});
|
|
546
|
-
} catch {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
const strings = /* @__PURE__ */ new Set();
|
|
550
|
-
const hookTargets = /* @__PURE__ */ new Set();
|
|
551
|
-
let usedT = false;
|
|
552
|
-
traverse$2(ast, {
|
|
553
|
-
"JSXElement|JSXFragment"(path) {
|
|
554
|
-
const node = path.node;
|
|
555
|
-
const name = t.isJSXElement(node) && t.isJSXIdentifier(node.openingElement.name) ? node.openingElement.name.name : null;
|
|
556
|
-
if (name && SKIP_ELEMENTS.has(name)) return;
|
|
557
|
-
if (hasDynamicChild(node)) return;
|
|
558
|
-
const children = [];
|
|
559
|
-
let changed = false;
|
|
560
|
-
for (const child of node.children) {
|
|
561
|
-
const cleaned = t.isJSXText(child) ? cleanJsxText(child.value) : "";
|
|
562
|
-
const text = cleaned.trim();
|
|
563
|
-
if (!t.isJSXText(child) || !text || !isTranslatable(text)) {
|
|
564
|
-
children.push(child);
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
strings.add(text);
|
|
568
|
-
if (!rewrite) {
|
|
569
|
-
children.push(child);
|
|
570
|
-
continue;
|
|
571
|
-
}
|
|
572
|
-
const space = () => t.jsxExpressionContainer(t.stringLiteral(" "));
|
|
573
|
-
if (/^\s/.test(cleaned)) children.push(space());
|
|
574
|
-
children.push(t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier(T_IMPORT), [t.jsxAttribute(t.jsxIdentifier("s"), t.stringLiteral(text))], true), null, []));
|
|
575
|
-
if (/\s$/.test(cleaned)) children.push(space());
|
|
576
|
-
usedT = true;
|
|
577
|
-
changed = true;
|
|
578
|
-
}
|
|
579
|
-
if (changed) node.children = children;
|
|
580
|
-
},
|
|
581
|
-
JSXAttribute(path) {
|
|
582
|
-
const { name, value } = path.node;
|
|
583
|
-
if (!t.isJSXIdentifier(name) || !TRANSLATABLE_ATTRS.has(name.name)) return;
|
|
584
|
-
if (!t.isStringLiteral(value)) return;
|
|
585
|
-
const opening = path.parent;
|
|
586
|
-
if (!t.isJSXIdentifier(opening.name) || !/^[a-z]/.test(opening.name.name)) return;
|
|
587
|
-
const text = value.value.trim();
|
|
588
|
-
if (!text || !isTranslatable(text)) return;
|
|
589
|
-
const component = enclosingComponent(path);
|
|
590
|
-
if (!component) return;
|
|
591
|
-
strings.add(text);
|
|
592
|
-
if (!rewrite) return;
|
|
593
|
-
path.node.value = t.jsxExpressionContainer(t.callExpression(t.identifier(HOOK_VAR), [t.stringLiteral(text)]));
|
|
594
|
-
hookTargets.add(component);
|
|
595
|
-
}
|
|
596
|
-
});
|
|
597
|
-
if (strings.size === 0) return null;
|
|
598
|
-
if (!rewrite) return {
|
|
599
|
-
code,
|
|
600
|
-
map: null,
|
|
601
|
-
strings: [...strings]
|
|
602
|
-
};
|
|
603
|
-
for (const fn of hookTargets) {
|
|
604
|
-
if (!t.isBlockStatement(fn.node.body)) fn.node.body = t.blockStatement([t.returnStatement(fn.node.body)]);
|
|
605
|
-
fn.node.body.body.unshift(t.variableDeclaration("const", [t.variableDeclarator(t.identifier(HOOK_VAR), t.callExpression(t.identifier(HOOK_IMPORT), []))]));
|
|
606
|
-
}
|
|
607
|
-
const specifiers = [];
|
|
608
|
-
if (usedT) specifiers.push(t.importSpecifier(t.identifier(T_IMPORT), t.identifier("__T")));
|
|
609
|
-
if (hookTargets.size) specifiers.push(t.importSpecifier(t.identifier(HOOK_IMPORT), t.identifier("__useT")));
|
|
610
|
-
ast.program.body.unshift(t.importDeclaration(specifiers, t.stringLiteral(RUNTIME)));
|
|
611
|
-
const out = generate(ast, {
|
|
612
|
-
sourceMaps: true,
|
|
613
|
-
sourceFileName: file
|
|
614
|
-
}, code);
|
|
615
|
-
return {
|
|
616
|
-
code: out.code,
|
|
617
|
-
map: out.map,
|
|
618
|
-
strings: [...strings]
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
/** Strings only, without rewriting (used by `react-autolocale sync`). */
|
|
622
|
-
function extractStrings(code, file) {
|
|
623
|
-
return transformJsx(code, file, { rewrite: false })?.strings ?? [];
|
|
624
|
-
}
|
|
625
|
-
//#endregion
|
|
626
|
-
//#region src/core/scan.ts
|
|
627
|
-
const EXTENSIONS = /* @__PURE__ */ new Set([
|
|
628
|
-
".tsx",
|
|
629
|
-
".jsx",
|
|
630
|
-
".ts",
|
|
631
|
-
".js"
|
|
632
|
-
]);
|
|
633
|
-
const IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
634
|
-
"node_modules",
|
|
635
|
-
"dist",
|
|
636
|
-
"build",
|
|
637
|
-
"coverage"
|
|
638
|
-
]);
|
|
639
|
-
function isSourceFile(file) {
|
|
640
|
-
return EXTENSIONS.has(path.extname(file)) && !file.endsWith(".d.ts") && !/\.(test|spec)\./.test(file);
|
|
641
|
-
}
|
|
642
|
-
function* walk(dir) {
|
|
643
|
-
if (!fs.existsSync(dir)) return;
|
|
644
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
645
|
-
if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) continue;
|
|
646
|
-
const full = path.join(dir, entry.name);
|
|
647
|
-
if (entry.isDirectory()) yield* walk(full);
|
|
648
|
-
else if (isSourceFile(full)) yield full;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
/** Every translatable string under the configured folders, in stable file order. */
|
|
652
|
-
function scanProject(root, include) {
|
|
653
|
-
const found = /* @__PURE__ */ new Set();
|
|
654
|
-
for (const dir of include) for (const file of walk(path.resolve(root, dir))) for (const s of extractStrings(fs.readFileSync(file, "utf8"), file)) found.add(s);
|
|
655
|
-
return [...found];
|
|
656
|
-
}
|
|
657
|
-
//#endregion
|
|
658
|
-
//#region src/core/head.ts
|
|
659
|
-
const META_SELECTORS = [
|
|
660
|
-
"meta[name=\"description\"]",
|
|
661
|
-
"meta[property=\"og:title\"]",
|
|
662
|
-
"meta[property=\"og:description\"]",
|
|
663
|
-
"meta[name=\"twitter:title\"]",
|
|
664
|
-
"meta[name=\"twitter:description\"]"
|
|
665
|
-
];
|
|
666
|
-
/** Title and description-like tags worth translating (only used when seo is on). */
|
|
667
|
-
function headStrings(html) {
|
|
668
|
-
const doc = new JSDOM(html).window.document;
|
|
669
|
-
const out = /* @__PURE__ */ new Set();
|
|
670
|
-
const title = doc.title.trim();
|
|
671
|
-
if (title && /\p{L}/u.test(title)) out.add(title);
|
|
672
|
-
for (const sel of META_SELECTORS) {
|
|
673
|
-
const v = doc.querySelector(sel)?.getAttribute("content")?.trim();
|
|
674
|
-
if (v && /\p{L}/u.test(v)) out.add(v);
|
|
675
|
-
}
|
|
676
|
-
return [...out];
|
|
677
|
-
}
|
|
678
|
-
function translateHead(doc, dict) {
|
|
679
|
-
const title = doc.title.trim();
|
|
680
|
-
if (title && dict[title]) doc.title = dict[title];
|
|
681
|
-
for (const sel of META_SELECTORS) {
|
|
682
|
-
const el = doc.querySelector(sel);
|
|
683
|
-
const v = el?.getAttribute("content")?.trim();
|
|
684
|
-
if (el && v && dict[v]) el.setAttribute("content", dict[v]);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
//#endregion
|
|
688
10
|
//#region src/core/routes.ts
|
|
689
|
-
const traverse
|
|
11
|
+
const traverse = _traverse.default ?? _traverse;
|
|
690
12
|
function str(node) {
|
|
691
13
|
if (t.isJSXExpressionContainer(node)) return str(node.expression);
|
|
692
14
|
if (t.isStringLiteral(node)) return node.value;
|
|
@@ -740,7 +62,7 @@ function routesInCode(code, file) {
|
|
|
740
62
|
}
|
|
741
63
|
const found = /* @__PURE__ */ new Set();
|
|
742
64
|
const add = (p) => isStatic(p) && found.add(p);
|
|
743
|
-
traverse
|
|
65
|
+
traverse(ast, {
|
|
744
66
|
JSXOpeningElement(p) {
|
|
745
67
|
const own = jsxRoutePath(p.node);
|
|
746
68
|
if (own === null) return;
|
|
@@ -775,62 +97,6 @@ function discoverRoutes(root, include) {
|
|
|
775
97
|
return [...all];
|
|
776
98
|
}
|
|
777
99
|
//#endregion
|
|
778
|
-
//#region src/core/site.ts
|
|
779
|
-
const traverse = _traverse.default ?? _traverse;
|
|
780
|
-
function literal(node) {
|
|
781
|
-
if (!node) return void 0;
|
|
782
|
-
if (t.isJSXExpressionContainer(node)) return literal(node.expression);
|
|
783
|
-
if (t.isStringLiteral(node)) return node.value;
|
|
784
|
-
if (t.isBooleanLiteral(node)) return node.value;
|
|
785
|
-
if (t.isTemplateLiteral(node) && node.expressions.length === 0) return node.quasis[0].value.cooked ?? void 0;
|
|
786
|
-
if (t.isArrayExpression(node)) {
|
|
787
|
-
const items = node.elements.map((e) => literal(e));
|
|
788
|
-
return items.every((i) => typeof i === "string") ? items : void 0;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
/** Reads the literal props of `<AutoScale ...>` from source code. Returns null if the file has none. */
|
|
792
|
-
function parseAutoScale(code, file) {
|
|
793
|
-
if (!code.includes("AutoScale")) return null;
|
|
794
|
-
let ast;
|
|
795
|
-
try {
|
|
796
|
-
ast = parse(code, {
|
|
797
|
-
sourceType: "module",
|
|
798
|
-
plugins: parserPlugins(file)
|
|
799
|
-
});
|
|
800
|
-
} catch {
|
|
801
|
-
return null;
|
|
802
|
-
}
|
|
803
|
-
let found = null;
|
|
804
|
-
traverse(ast, { JSXOpeningElement(path) {
|
|
805
|
-
const name = path.node.name;
|
|
806
|
-
if (found || !t.isJSXIdentifier(name) || name.name !== "AutoScale") return;
|
|
807
|
-
const props = {};
|
|
808
|
-
for (const attr of path.node.attributes) if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) props[attr.name.name] = attr.value === null ? true : literal(attr.value);
|
|
809
|
-
const { original, languages, exclude, seo, siteUrl } = props;
|
|
810
|
-
if (typeof original !== "string" || !Array.isArray(languages)) throw new Error(`${file}: <AutoScale> needs literal props, e.g. original="en" languages={["fr", "es"]}. Values must be written inline (not variables) so they can be read at build time.`);
|
|
811
|
-
found = {
|
|
812
|
-
original,
|
|
813
|
-
languages: languages.filter((l) => l !== original),
|
|
814
|
-
exclude: Array.isArray(exclude) ? exclude : [],
|
|
815
|
-
seo: seo === true || seo === "true",
|
|
816
|
-
siteUrl: typeof siteUrl === "string" ? siteUrl : void 0
|
|
817
|
-
};
|
|
818
|
-
} });
|
|
819
|
-
return found;
|
|
820
|
-
}
|
|
821
|
-
/** Finds the first <AutoScale> in the project's source folders. */
|
|
822
|
-
function findSite(root, include) {
|
|
823
|
-
for (const dir of include) for (const file of walk(path.resolve(root, dir))) {
|
|
824
|
-
const site = parseAutoScale(fs.readFileSync(file, "utf8"), file);
|
|
825
|
-
if (site) return site;
|
|
826
|
-
}
|
|
827
|
-
throw new Error(`No <AutoScale> found in ${include.join(", ")}. Wrap your app: <AutoScale original="en" languages={["fr", "es"]}><App /></AutoScale>`);
|
|
828
|
-
}
|
|
829
|
-
/** Folders scanned for JSX. */
|
|
830
|
-
function defaultInclude(root) {
|
|
831
|
-
return fs.existsSync(path.join(root, "src")) ? ["src"] : ["."];
|
|
832
|
-
}
|
|
833
|
-
//#endregion
|
|
834
100
|
//#region src/vite/seo.ts
|
|
835
101
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
836
102
|
const restOf = (p) => p.pathname.slice(p.lang.length + 1) || "/";
|
|
@@ -1101,7 +367,7 @@ async function prerender(opts) {
|
|
|
1101
367
|
if (site.seo) applySeo({
|
|
1102
368
|
outDir,
|
|
1103
369
|
site,
|
|
1104
|
-
dicts,
|
|
370
|
+
dicts: opts.headDicts ?? dicts,
|
|
1105
371
|
pages,
|
|
1106
372
|
log
|
|
1107
373
|
});
|
|
@@ -1113,69 +379,90 @@ async function prerender(opts) {
|
|
|
1113
379
|
//#region src/vite/index.ts
|
|
1114
380
|
const VIRTUAL_ID = "virtual:react-autolocale";
|
|
1115
381
|
const RESOLVED_ID = "\0" + VIRTUAL_ID;
|
|
382
|
+
/** One module per language, so each dictionary is fetched only when that language is used. */
|
|
383
|
+
const DICT_PREFIX = VIRTUAL_ID + "/dict/";
|
|
384
|
+
/**
|
|
385
|
+
* Reads the generated catalogs and serves them to the runtime. It never translates, downloads a model or calls
|
|
386
|
+
* a translation API: missing translations show the source text, and `react-autolocale sync` fills them.
|
|
387
|
+
*/
|
|
1116
388
|
function reactAutolocale(options = {}) {
|
|
1117
389
|
let config;
|
|
1118
390
|
let root = process.cwd();
|
|
1119
|
-
let include = [];
|
|
1120
391
|
let includeDirs = [];
|
|
1121
|
-
let
|
|
1122
|
-
let dicts = {
|
|
1123
|
-
|
|
392
|
+
let project;
|
|
393
|
+
let dicts = {
|
|
394
|
+
byId: {},
|
|
395
|
+
byText: {},
|
|
396
|
+
missing: {},
|
|
397
|
+
usedLegacy: false
|
|
398
|
+
};
|
|
399
|
+
let banner = {};
|
|
1124
400
|
let server;
|
|
1125
401
|
let rendered = false;
|
|
1126
402
|
const log = (msg) => console.log(msg);
|
|
1127
|
-
const
|
|
403
|
+
const warn = (msg) => config?.logger ? config.logger.warn(`[react-autolocale] ${msg}`) : log(`[react-autolocale] ${msg}`);
|
|
1128
404
|
async function refresh() {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
log,
|
|
1143
|
-
overrides: options.overrides
|
|
1144
|
-
});
|
|
405
|
+
project = await loadProject(root, { log });
|
|
406
|
+
if (options.include) project.include = options.include;
|
|
407
|
+
includeDirs = project.include.map((d) => path.resolve(root, d) + path.sep);
|
|
408
|
+
dicts = loadDicts(project, mergeOverrides(readOverrides(root), options.overrides));
|
|
409
|
+
banner = readBanner(project.output, root);
|
|
410
|
+
}
|
|
411
|
+
/** One summary per build or server start, never a translation job. */
|
|
412
|
+
function report() {
|
|
413
|
+
if (options.engine || options.glossary) warn("`engine` and `glossary` in vite.config are ignored: builds no longer translate. Move them to auto-locale.config.ts (provider, glossary).");
|
|
414
|
+
if (dicts.usedLegacy) warn(`Using translations from node_modules/.cache/react-autolocale (older version). Run \`npx react-autolocale generate\` to move them into ${project.outputRel}/.`);
|
|
415
|
+
const gaps = Object.entries(dicts.missing).filter(([, n]) => n > 0);
|
|
416
|
+
if (!Object.values(dicts.byId).some((d) => Object.keys(d).length) && project.site.languages.length) warn(`No translations in ${project.outputRel}/ yet; pages show the source text. Run \`npx react-autolocale generate\`.`);
|
|
417
|
+
else if (gaps.length) warn(`Untranslated strings (shown in ${project.site.original}): ${gaps.map(([l, n]) => `${l} ${n}`).join(", ")}. Run \`npx react-autolocale sync\`.`);
|
|
1145
418
|
}
|
|
1146
419
|
function virtualModule() {
|
|
420
|
+
const { site } = project;
|
|
1147
421
|
const meta = site.languages.map((code) => ({
|
|
1148
422
|
code,
|
|
1149
423
|
native: getLanguage(code)?.native ?? code,
|
|
1150
424
|
rtl: !!getLanguage(code)?.rtl
|
|
1151
425
|
}));
|
|
1152
|
-
const loaders = site.languages.
|
|
426
|
+
const loaders = site.languages.map((code) => `${JSON.stringify(code)}: () => import(${JSON.stringify(DICT_PREFIX + code)})`);
|
|
1153
427
|
return [
|
|
1154
428
|
`export const original = ${JSON.stringify(site.original)};`,
|
|
1155
429
|
`export const originalNative = ${JSON.stringify(getLanguage(site.original)?.native ?? site.original)};`,
|
|
1156
430
|
`export const seo = ${JSON.stringify(site.seo)};`,
|
|
431
|
+
`export const banner = ${JSON.stringify(banner)};`,
|
|
1157
432
|
`export const languages = ${JSON.stringify(meta)};`,
|
|
1158
433
|
`export const loaders = { ${loaders.join(", ")} };`
|
|
1159
434
|
].join("\n");
|
|
1160
435
|
}
|
|
1161
436
|
let timer;
|
|
1162
|
-
let busy = false;
|
|
1163
437
|
function scheduleRefresh() {
|
|
1164
438
|
clearTimeout(timer);
|
|
1165
439
|
timer = setTimeout(async () => {
|
|
1166
|
-
if (
|
|
1167
|
-
busy = true;
|
|
440
|
+
if (!server) return;
|
|
1168
441
|
try {
|
|
1169
442
|
await refresh();
|
|
1170
|
-
const mod
|
|
1171
|
-
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
443
|
+
for (const [id, mod] of server.moduleGraph.idToModuleMap) if (id.startsWith(RESOLVED_ID)) server.moduleGraph.invalidateModule(mod);
|
|
1172
444
|
server.ws.send({ type: "full-reload" });
|
|
1173
445
|
} catch (err) {
|
|
1174
|
-
|
|
1175
|
-
} finally {
|
|
1176
|
-
busy = false;
|
|
446
|
+
warn(err.message);
|
|
1177
447
|
}
|
|
1178
|
-
},
|
|
448
|
+
}, 300);
|
|
449
|
+
}
|
|
450
|
+
const untranslated = /* @__PURE__ */ new Set();
|
|
451
|
+
let warnTimer;
|
|
452
|
+
function noteUntranslated(strings) {
|
|
453
|
+
const { site } = project;
|
|
454
|
+
for (const s of strings) {
|
|
455
|
+
if (site.exclude.includes(s.trim())) continue;
|
|
456
|
+
const id = messageId(s);
|
|
457
|
+
if (site.languages.some((l) => !dicts.byId[l]?.[id])) untranslated.add(s);
|
|
458
|
+
}
|
|
459
|
+
if (!untranslated.size) return;
|
|
460
|
+
clearTimeout(warnTimer);
|
|
461
|
+
warnTimer = setTimeout(() => {
|
|
462
|
+
const list = [...untranslated];
|
|
463
|
+
warn(`${list.length} string${list.length === 1 ? " has" : "s have"} no translation yet (e.g. "${list[0]}"). Run \`npx react-autolocale sync\`; the page reloads when it finishes.`);
|
|
464
|
+
untranslated.clear();
|
|
465
|
+
}, 1500);
|
|
1179
466
|
}
|
|
1180
467
|
return {
|
|
1181
468
|
name: "react-autolocale",
|
|
@@ -1193,16 +480,30 @@ function reactAutolocale(options = {}) {
|
|
|
1193
480
|
},
|
|
1194
481
|
configureServer(s) {
|
|
1195
482
|
server = s;
|
|
483
|
+
s.watcher.add([path.resolve(root, OVERRIDES_FILE), path.resolve(root, ".auto-locale/manifest.json")]);
|
|
484
|
+
s.watcher.on("all", (_event, changed) => {
|
|
485
|
+
if (!project) return;
|
|
486
|
+
if (changed === path.resolve(root, "react-autolocale.overrides.json") || changed === project.configFile || changed.startsWith(project.output + path.sep) && changed.endsWith(".json") || changed === path.join(project.meta, "manifest.json")) scheduleRefresh();
|
|
487
|
+
});
|
|
1196
488
|
},
|
|
1197
489
|
async buildStart() {
|
|
1198
490
|
if (config.build.ssr) return;
|
|
1199
491
|
await refresh();
|
|
492
|
+
if (server) server.watcher.add(project.output);
|
|
493
|
+
report();
|
|
1200
494
|
},
|
|
1201
495
|
resolveId(id) {
|
|
1202
|
-
|
|
496
|
+
if (id === VIRTUAL_ID) return RESOLVED_ID;
|
|
497
|
+
if (id.startsWith(DICT_PREFIX)) return "\0" + id;
|
|
498
|
+
return null;
|
|
1203
499
|
},
|
|
1204
500
|
load(id) {
|
|
1205
|
-
|
|
501
|
+
if (id === RESOLVED_ID) return virtualModule();
|
|
502
|
+
if (id.startsWith("\0" + DICT_PREFIX)) {
|
|
503
|
+
const lang = id.slice(31);
|
|
504
|
+
return `export default ${JSON.stringify(dicts.byId[lang] ?? {})};`;
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
1206
507
|
},
|
|
1207
508
|
transform(code, id) {
|
|
1208
509
|
const file = id.split("?")[0];
|
|
@@ -1210,9 +511,7 @@ function reactAutolocale(options = {}) {
|
|
|
1210
511
|
if (!includeDirs.some((d) => file.startsWith(d))) return null;
|
|
1211
512
|
const result = transformJsx(code, file);
|
|
1212
513
|
if (!result) return null;
|
|
1213
|
-
if (config.command === "serve" &&
|
|
1214
|
-
if (!site.languages.every((l) => result.strings.every((s) => site.exclude.includes(s.trim()) || dicts[l]?.[s]))) scheduleRefresh();
|
|
1215
|
-
}
|
|
514
|
+
if (config.command === "serve" && project) noteUntranslated(result.strings);
|
|
1216
515
|
return {
|
|
1217
516
|
code: result.code,
|
|
1218
517
|
map: result.map
|
|
@@ -1224,14 +523,15 @@ function reactAutolocale(options = {}) {
|
|
|
1224
523
|
if (config.command !== "build" || config.build.ssr || rendered) return;
|
|
1225
524
|
rendered = true;
|
|
1226
525
|
const extra = typeof options.routes === "function" ? await options.routes() : options.routes ?? [];
|
|
1227
|
-
const routes = [.../* @__PURE__ */ new Set([...discoverRoutes(root, include), ...extra])];
|
|
526
|
+
const routes = [.../* @__PURE__ */ new Set([...discoverRoutes(root, project.include), ...extra])];
|
|
1228
527
|
await prerender({
|
|
1229
528
|
root,
|
|
1230
529
|
outDir: path.resolve(root, config.build.outDir),
|
|
1231
|
-
site,
|
|
1232
|
-
include,
|
|
530
|
+
site: project.site,
|
|
531
|
+
include: project.include,
|
|
1233
532
|
routes,
|
|
1234
|
-
dicts,
|
|
533
|
+
dicts: dicts.byId,
|
|
534
|
+
headDicts: dicts.byText,
|
|
1235
535
|
log
|
|
1236
536
|
});
|
|
1237
537
|
}
|
|
@@ -1239,4 +539,4 @@ function reactAutolocale(options = {}) {
|
|
|
1239
539
|
};
|
|
1240
540
|
}
|
|
1241
541
|
//#endregion
|
|
1242
|
-
export { reactAutolocale as default, reactAutolocale };
|
|
542
|
+
export { reactAutolocale as default, reactAutolocale, ollama };
|