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/dist/vite.js CHANGED
@@ -1,692 +1,14 @@
1
- import { n as redirectScript } from "./redirect-CKNY4ru5.js";
2
- import fs from "node:fs";
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 { spawnSync } from "node:child_process";
5
- import { createRequire } from "node:module";
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$1 = _traverse.default ?? _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$1(ast, {
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, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
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 site;
1122
- let dicts = {};
1123
- let translator;
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 getTranslator = () => translator ??= createLocalTranslator(log);
403
+ const warn = (msg) => config?.logger ? config.logger.warn(`[react-autolocale] ${msg}`) : log(`[react-autolocale] ${msg}`);
1128
404
  async function refresh() {
1129
- include = options.include ?? defaultInclude(root);
1130
- includeDirs = include.map((d) => path.resolve(root, d) + path.sep);
1131
- site = findSite(root, include);
1132
- const strings = scanProject(root, include);
1133
- if (site.seo) {
1134
- const html = path.join(root, "index.html");
1135
- if (fs.existsSync(html)) strings.push(...headStrings(fs.readFileSync(html, "utf8")));
1136
- }
1137
- dicts = await ensureTranslations({
1138
- root,
1139
- site,
1140
- strings,
1141
- getTranslator,
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.filter((code) => fs.existsSync(cacheFile(root, code))).map((code) => `${JSON.stringify(code)}: () => import(${JSON.stringify(cacheFile(root, code))})`);
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 (busy || !server) return;
1167
- busy = true;
440
+ if (!server) return;
1168
441
  try {
1169
442
  await refresh();
1170
- const mod = server.moduleGraph.getModuleById(RESOLVED_ID);
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
- log(`[react-autolocale] ${err.message}`);
1175
- } finally {
1176
- busy = false;
446
+ warn(err.message);
1177
447
  }
1178
- }, 400);
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
- return id === VIRTUAL_ID ? RESOLVED_ID : null;
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
- return id === RESOLVED_ID ? virtualModule() : null;
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" && site) {
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 };