react-autolocale 0.2.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,375 +1,14 @@
1
- import { a as redirectScript, t as BANNER_EN } from "./suggestion-UZQ8ngd0.js";
2
- import { a as cacheDir, c as createLocalTranslator, d as toTranslator, f as unmask, i as readOverrides, l as mask, o as cacheFile, p as getLanguage, r as mergeOverrides, s as ensureTranslations, t as OVERRIDES_FILE, u as ollama } from "./overrides-ZQ6meLck.js";
3
- 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";
4
3
  import path from "node:path";
4
+ import fs from "node:fs";
5
+ import { JSDOM, VirtualConsole } from "jsdom";
5
6
  import { parse } from "@babel/parser";
6
7
  import _traverse from "@babel/traverse";
7
- import _generate from "@babel/generator";
8
8
  import * as t from "@babel/types";
9
- import { JSDOM, VirtualConsole } from "jsdom";
10
9
  import { createServer } from "vite";
11
- //#region src/core/transform.ts
12
- const traverse$2 = _traverse.default ?? _traverse;
13
- const generate = _generate.default ?? _generate;
14
- /** Attributes whose string value is user-visible text. */
15
- const TRANSLATABLE_ATTRS = /* @__PURE__ */ new Set([
16
- "placeholder",
17
- "title",
18
- "alt",
19
- "aria-label",
20
- "aria-description"
21
- ]);
22
- /** Elements whose text content is code, not copy. */
23
- const SKIP_ELEMENTS = /* @__PURE__ */ new Set([
24
- "script",
25
- "style",
26
- "code",
27
- "pre",
28
- "kbd",
29
- "samp",
30
- "svg",
31
- "textarea"
32
- ]);
33
- const T_IMPORT = "__RLT";
34
- const TD_IMPORT = "__RLTD";
35
- const HOOK_IMPORT = "__rlUseT";
36
- const HOOK_VAR = "__rl";
37
- const RUNTIME = "react-autolocale";
38
- function parserPlugins(file) {
39
- if (/\.tsx$/.test(file)) return ["typescript", "jsx"];
40
- if (/\.ts$/.test(file)) return ["typescript"];
41
- return ["jsx"];
42
- }
43
- /** Collapses JSX text the way React does: lines are trimmed and joined with one space. */
44
- function cleanJsxText(raw) {
45
- const lines = raw.split(/\r\n|\n|\r/);
46
- let last = 0;
47
- for (let i = 0; i < lines.length; i++) if (/[^ \t]/.test(lines[i])) last = i;
48
- let out = "";
49
- for (let i = 0; i < lines.length; i++) {
50
- let line = lines[i].replace(/\t/g, " ");
51
- if (i !== 0) line = line.replace(/^[ ]+/, "");
52
- if (i !== lines.length - 1) line = line.replace(/[ ]+$/, "");
53
- if (line) {
54
- if (i !== last) line += " ";
55
- out += line;
56
- }
57
- }
58
- return out;
59
- }
60
- /** Text worth translating: must contain at least one letter in any script. */
61
- function isTranslatable(text) {
62
- return /\p{L}/u.test(text);
63
- }
64
- function isComponentName(name) {
65
- return !!name && /^[A-Z]/.test(name);
66
- }
67
- /** Finds the function that is a React component (PascalCase name, possibly wrapped in memo/forwardRef). */
68
- function enclosingComponent(path) {
69
- let p = path.parentPath;
70
- while (p) {
71
- if (p.isFunction()) {
72
- const fn = p;
73
- let name;
74
- if ((fn.node.type === "FunctionDeclaration" || fn.node.type === "FunctionExpression") && fn.node.id) name = fn.node.id.name;
75
- if (!name) {
76
- let up = fn.parentPath;
77
- while (up && up.isCallExpression()) up = up.parentPath;
78
- if (up?.isVariableDeclarator() && t.isIdentifier(up.node.id)) name = up.node.id.name;
79
- }
80
- if (isComponentName(name)) return fn;
81
- }
82
- p = p.parentPath;
83
- }
84
- return null;
85
- }
86
- /** `translate="no"` on an element (or any ancestor) keeps its text and attributes exactly as written. */
87
- function hasNoTranslate(opening) {
88
- return opening.attributes.some((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name: "translate" }) && (t.isStringLiteral(a.value, { value: "no" }) || t.isJSXExpressionContainer(a.value) && t.isStringLiteral(a.value.expression, { value: "no" })));
89
- }
90
- function insideNoTranslate(path) {
91
- for (let p = path; p; p = p.parentPath) if (p.isJSXElement() && hasNoTranslate(p.node.openingElement)) return true;
92
- return false;
93
- }
94
- /** A value we can safely hand to the runtime: `name`, `user.name`, `this.props.count`, `items?.[0]`. */
95
- function simpleValueName(node) {
96
- const root = (n) => t.isIdentifier(n) || t.isThisExpression(n) || (t.isMemberExpression(n) || t.isOptionalMemberExpression(n)) && root(n.object) && (n.computed ? t.isNumericLiteral(n.property) || t.isStringLiteral(n.property) : t.isIdentifier(n.property));
97
- if (!root(node)) return null;
98
- if (t.isIdentifier(node)) return node.name;
99
- if ((t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) && !node.computed && t.isIdentifier(node.property)) return node.property.name;
100
- return "value";
101
- }
102
- /**
103
- * "Hello {name}!" -> template "Hello {name}!" plus the values to fill in at runtime.
104
- * Only text mixed with plain variables qualifies: no nested elements, calls, conditions or JSX.
105
- */
106
- function buildTemplate(node) {
107
- let text = "";
108
- const values = [];
109
- const used = /* @__PURE__ */ new Set();
110
- for (const child of node.children) if (t.isJSXText(child)) text += cleanJsxText(child.value);
111
- else if (t.isJSXExpressionContainer(child)) {
112
- const e = child.expression;
113
- if (t.isJSXEmptyExpression(e)) continue;
114
- if (t.isStringLiteral(e)) {
115
- text += e.value;
116
- continue;
117
- }
118
- const base = simpleValueName(e);
119
- if (!base) return null;
120
- let name = base;
121
- for (let i = 2; used.has(name); i++) name = `${base}_${i}`;
122
- used.add(name);
123
- values.push({
124
- name,
125
- expr: e
126
- });
127
- text += `{${name}}`;
128
- } else return null;
129
- const letters = text.replace(/\{\w+\}/g, "");
130
- if (!values.length || !isTranslatable(letters)) return null;
131
- return {
132
- text: text.trim(),
133
- values,
134
- lead: /^\s/.test(text),
135
- trail: /\s$/.test(text)
136
- };
137
- }
138
- /** Parses a file, extracts translatable strings, and rewrites JSX to render translations. */
139
- function transformJsx(code, file, opts = {}) {
140
- const rewrite = opts.rewrite !== false;
141
- if (!code.includes("<")) return null;
142
- let ast;
143
- try {
144
- ast = parse(code, {
145
- sourceType: "module",
146
- sourceFilename: file,
147
- plugins: parserPlugins(file)
148
- });
149
- } catch {
150
- return null;
151
- }
152
- const strings = /* @__PURE__ */ new Set();
153
- /** What kind of element each string sits in ("button", "h1", "placeholder", ...), for context-aware engines. */
154
- const contexts = {};
155
- const record = (text, context) => {
156
- strings.add(text);
157
- contexts[text] ??= context;
158
- };
159
- const hookTargets = /* @__PURE__ */ new Set();
160
- let usedT = false;
161
- let usedTD = false;
162
- const space = () => t.jsxExpressionContainer(t.stringLiteral(" "));
163
- traverse$2(ast, {
164
- "JSXElement|JSXFragment"(path) {
165
- const node = path.node;
166
- const name = t.isJSXElement(node) && t.isJSXIdentifier(node.openingElement.name) ? node.openingElement.name.name : null;
167
- if (name && SKIP_ELEMENTS.has(name)) return;
168
- if (insideNoTranslate(path)) return;
169
- const context = name ?? "text";
170
- if (node.children.some((c) => t.isJSXExpressionContainer(c) && !t.isJSXEmptyExpression(c.expression) && !t.isStringLiteral(c.expression))) {
171
- const tpl = buildTemplate(node);
172
- if (!tpl) return;
173
- record(tpl.text, context);
174
- if (!rewrite) return;
175
- const valueObject = t.objectExpression(tpl.values.map((v) => t.objectProperty(t.identifier(v.name), t.cloneNode(v.expr))));
176
- node.children = [
177
- ...tpl.lead ? [space()] : [],
178
- t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier(TD_IMPORT), [t.jsxAttribute(t.jsxIdentifier("s"), t.stringLiteral(tpl.text)), t.jsxAttribute(t.jsxIdentifier("v"), t.jsxExpressionContainer(valueObject))], true), null, []),
179
- ...tpl.trail ? [space()] : []
180
- ];
181
- usedTD = true;
182
- return;
183
- }
184
- const children = [];
185
- let changed = false;
186
- for (const child of node.children) {
187
- const cleaned = t.isJSXText(child) ? cleanJsxText(child.value) : "";
188
- const text = cleaned.trim();
189
- if (!t.isJSXText(child) || !text || !isTranslatable(text)) {
190
- children.push(child);
191
- continue;
192
- }
193
- record(text, context);
194
- if (!rewrite) {
195
- children.push(child);
196
- continue;
197
- }
198
- if (/^\s/.test(cleaned)) children.push(space());
199
- children.push(t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier(T_IMPORT), [t.jsxAttribute(t.jsxIdentifier("s"), t.stringLiteral(text))], true), null, []));
200
- if (/\s$/.test(cleaned)) children.push(space());
201
- usedT = true;
202
- changed = true;
203
- }
204
- if (changed) node.children = children;
205
- },
206
- JSXAttribute(path) {
207
- const { name, value } = path.node;
208
- if (!t.isJSXIdentifier(name) || !TRANSLATABLE_ATTRS.has(name.name)) return;
209
- if (!t.isStringLiteral(value)) return;
210
- const opening = path.parent;
211
- if (!t.isJSXIdentifier(opening.name) || !/^[a-z]/.test(opening.name.name)) return;
212
- if (insideNoTranslate(path.parentPath.parentPath)) return;
213
- const text = value.value.trim();
214
- if (!text || !isTranslatable(text)) return;
215
- const component = enclosingComponent(path);
216
- if (!component) return;
217
- record(text, name.name);
218
- if (!rewrite) return;
219
- path.node.value = t.jsxExpressionContainer(t.callExpression(t.identifier(HOOK_VAR), [t.stringLiteral(text)]));
220
- hookTargets.add(component);
221
- }
222
- });
223
- if (strings.size === 0) return null;
224
- if (!rewrite) return {
225
- code,
226
- map: null,
227
- strings: [...strings],
228
- contexts
229
- };
230
- for (const fn of hookTargets) {
231
- if (!t.isBlockStatement(fn.node.body)) fn.node.body = t.blockStatement([t.returnStatement(fn.node.body)]);
232
- fn.node.body.body.unshift(t.variableDeclaration("const", [t.variableDeclarator(t.identifier(HOOK_VAR), t.callExpression(t.identifier(HOOK_IMPORT), []))]));
233
- }
234
- const specifiers = [];
235
- if (usedT) specifiers.push(t.importSpecifier(t.identifier(T_IMPORT), t.identifier("__T")));
236
- if (usedTD) specifiers.push(t.importSpecifier(t.identifier(TD_IMPORT), t.identifier("__TD")));
237
- if (hookTargets.size) specifiers.push(t.importSpecifier(t.identifier(HOOK_IMPORT), t.identifier("__useT")));
238
- ast.program.body.unshift(t.importDeclaration(specifiers, t.stringLiteral(RUNTIME)));
239
- const out = generate(ast, {
240
- sourceMaps: true,
241
- sourceFileName: file
242
- }, code);
243
- return {
244
- code: out.code,
245
- map: out.map,
246
- strings: [...strings],
247
- contexts
248
- };
249
- }
250
- /** Strings (and where they appear), without rewriting. */
251
- function extractItems(code, file) {
252
- const r = transformJsx(code, file, { rewrite: false });
253
- return {
254
- strings: r?.strings ?? [],
255
- contexts: r?.contexts ?? {}
256
- };
257
- }
258
- //#endregion
259
- //#region src/core/scan.ts
260
- const EXTENSIONS = /* @__PURE__ */ new Set([
261
- ".tsx",
262
- ".jsx",
263
- ".ts",
264
- ".js"
265
- ]);
266
- const IGNORED_DIRS = /* @__PURE__ */ new Set([
267
- "node_modules",
268
- "dist",
269
- "build",
270
- "coverage"
271
- ]);
272
- function isSourceFile(file) {
273
- return EXTENSIONS.has(path.extname(file)) && !file.endsWith(".d.ts") && !/\.(test|spec)\./.test(file);
274
- }
275
- function* walk(dir) {
276
- if (!fs.existsSync(dir)) return;
277
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
278
- if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) continue;
279
- const full = path.join(dir, entry.name);
280
- if (entry.isDirectory()) yield* walk(full);
281
- else if (isSourceFile(full)) yield full;
282
- }
283
- }
284
- /** Every translatable string under the configured folders, in stable file order, and where each appears. */
285
- function scanProjectDetailed(root, include) {
286
- const found = /* @__PURE__ */ new Set();
287
- const contexts = {};
288
- for (const dir of include) for (const file of walk(path.resolve(root, dir))) {
289
- const items = extractItems(fs.readFileSync(file, "utf8"), file);
290
- for (const s of items.strings) {
291
- found.add(s);
292
- contexts[s] ??= items.contexts[s] ?? "";
293
- }
294
- }
295
- return {
296
- strings: [...found],
297
- contexts
298
- };
299
- }
300
- //#endregion
301
- //#region src/core/head.ts
302
- const META_SELECTORS = [
303
- "meta[name=\"description\"]",
304
- "meta[property=\"og:title\"]",
305
- "meta[property=\"og:description\"]",
306
- "meta[name=\"twitter:title\"]",
307
- "meta[name=\"twitter:description\"]"
308
- ];
309
- /** Title and description-like tags worth translating (only used when seo is on). */
310
- function headStrings(html) {
311
- const doc = new JSDOM(html).window.document;
312
- const out = /* @__PURE__ */ new Set();
313
- const title = doc.title.trim();
314
- if (title && /\p{L}/u.test(title)) out.add(title);
315
- for (const sel of META_SELECTORS) {
316
- const v = doc.querySelector(sel)?.getAttribute("content")?.trim();
317
- if (v && /\p{L}/u.test(v)) out.add(v);
318
- }
319
- return [...out];
320
- }
321
- function translateHead(doc, dict) {
322
- const title = doc.title.trim();
323
- if (title && dict[title]) doc.title = dict[title];
324
- for (const sel of META_SELECTORS) {
325
- const el = doc.querySelector(sel);
326
- const v = el?.getAttribute("content")?.trim();
327
- if (el && v && dict[v]) el.setAttribute("content", dict[v]);
328
- }
329
- }
330
- //#endregion
331
- //#region src/core/banner.ts
332
- const FILE = (root) => path.join(cacheDir(root), "_banner.json");
333
- /** The banner's fixed phrases in every language, so a visitor sees them in their own language. */
334
- async function ensureBanner(opts) {
335
- const source = JSON.stringify(BANNER_EN);
336
- let cached = {};
337
- try {
338
- const file = JSON.parse(fs.readFileSync(FILE(opts.root), "utf8"));
339
- if (file._source === source) cached = file;
340
- } catch {}
341
- const out = { en: BANNER_EN };
342
- let changed = false;
343
- const keys = Object.keys(BANNER_EN);
344
- for (const lang of opts.langs) {
345
- if (lang === "en") continue;
346
- if (cached[lang]) {
347
- out[lang] = cached[lang];
348
- continue;
349
- }
350
- opts.log?.(`[react-autolocale] ${lang}: translating banner text...`);
351
- const jobs = keys.map((k) => mask(BANNER_EN[k], ["{language}"]));
352
- const translated = await opts.getTranslator().translate(jobs.map((j) => j.masked), "en", lang);
353
- const text = { ...BANNER_EN };
354
- keys.forEach((k, i) => {
355
- text[k] = unmask(translated[i] ?? "", jobs[i].phrases) ?? BANNER_EN[k];
356
- });
357
- out[lang] = text;
358
- cached[lang] = text;
359
- changed = true;
360
- }
361
- if (changed) {
362
- fs.mkdirSync(cacheDir(opts.root), { recursive: true });
363
- fs.writeFileSync(FILE(opts.root), JSON.stringify({
364
- _source: source,
365
- ...cached
366
- }, null, 2) + "\n");
367
- }
368
- return out;
369
- }
370
- //#endregion
371
10
  //#region src/core/routes.ts
372
- const traverse$1 = _traverse.default ?? _traverse;
11
+ const traverse = _traverse.default ?? _traverse;
373
12
  function str(node) {
374
13
  if (t.isJSXExpressionContainer(node)) return str(node.expression);
375
14
  if (t.isStringLiteral(node)) return node.value;
@@ -423,7 +62,7 @@ function routesInCode(code, file) {
423
62
  }
424
63
  const found = /* @__PURE__ */ new Set();
425
64
  const add = (p) => isStatic(p) && found.add(p);
426
- traverse$1(ast, {
65
+ traverse(ast, {
427
66
  JSXOpeningElement(p) {
428
67
  const own = jsxRoutePath(p.node);
429
68
  if (own === null) return;
@@ -458,62 +97,6 @@ function discoverRoutes(root, include) {
458
97
  return [...all];
459
98
  }
460
99
  //#endregion
461
- //#region src/core/site.ts
462
- const traverse = _traverse.default ?? _traverse;
463
- function literal(node) {
464
- if (!node) return void 0;
465
- if (t.isJSXExpressionContainer(node)) return literal(node.expression);
466
- if (t.isStringLiteral(node)) return node.value;
467
- if (t.isBooleanLiteral(node)) return node.value;
468
- if (t.isTemplateLiteral(node) && node.expressions.length === 0) return node.quasis[0].value.cooked ?? void 0;
469
- if (t.isArrayExpression(node)) {
470
- const items = node.elements.map((e) => literal(e));
471
- return items.every((i) => typeof i === "string") ? items : void 0;
472
- }
473
- }
474
- /** Reads the literal props of `<AutoScale ...>` from source code. Returns null if the file has none. */
475
- function parseAutoScale(code, file) {
476
- if (!code.includes("AutoScale")) return null;
477
- let ast;
478
- try {
479
- ast = parse(code, {
480
- sourceType: "module",
481
- plugins: parserPlugins(file)
482
- });
483
- } catch {
484
- return null;
485
- }
486
- let found = null;
487
- traverse(ast, { JSXOpeningElement(path) {
488
- const name = path.node.name;
489
- if (found || !t.isJSXIdentifier(name) || name.name !== "AutoScale") return;
490
- const props = {};
491
- 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);
492
- const { original, languages, exclude, seo, siteUrl } = props;
493
- 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.`);
494
- found = {
495
- original,
496
- languages: languages.filter((l) => l !== original),
497
- exclude: Array.isArray(exclude) ? exclude : [],
498
- seo: seo === true || seo === "true",
499
- siteUrl: typeof siteUrl === "string" ? siteUrl : void 0
500
- };
501
- } });
502
- return found;
503
- }
504
- /** Finds the first <AutoScale> in the project's source folders. */
505
- function findSite(root, include) {
506
- for (const dir of include) for (const file of walk(path.resolve(root, dir))) {
507
- const site = parseAutoScale(fs.readFileSync(file, "utf8"), file);
508
- if (site) return site;
509
- }
510
- throw new Error(`No <AutoScale> found in ${include.join(", ")}. Wrap your app: <AutoScale original="en" languages={["fr", "es"]}><App /></AutoScale>`);
511
- }
512
- /** Folders scanned for JSX. */
513
- function defaultInclude(root) {
514
- return fs.existsSync(path.join(root, "src")) ? ["src"] : ["."];
515
- }
516
- //#endregion
517
100
  //#region src/vite/seo.ts
518
101
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
519
102
  const restOf = (p) => p.pathname.slice(p.lang.length + 1) || "/";
@@ -784,7 +367,7 @@ async function prerender(opts) {
784
367
  if (site.seo) applySeo({
785
368
  outDir,
786
369
  site,
787
- dicts,
370
+ dicts: opts.headDicts ?? dicts,
788
371
  pages,
789
372
  log
790
373
  });
@@ -796,52 +379,51 @@ async function prerender(opts) {
796
379
  //#region src/vite/index.ts
797
380
  const VIRTUAL_ID = "virtual:react-autolocale";
798
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
+ */
799
388
  function reactAutolocale(options = {}) {
800
389
  let config;
801
390
  let root = process.cwd();
802
- let include = [];
803
391
  let includeDirs = [];
804
- let site;
805
- let dicts = {};
392
+ let project;
393
+ let dicts = {
394
+ byId: {},
395
+ byText: {},
396
+ missing: {},
397
+ usedLegacy: false
398
+ };
806
399
  let banner = {};
807
- let translator;
808
400
  let server;
809
401
  let rendered = false;
810
402
  const log = (msg) => console.log(msg);
811
- const getTranslator = () => translator ??= options.engine ? toTranslator(options.engine) : createLocalTranslator(log);
403
+ const warn = (msg) => config?.logger ? config.logger.warn(`[react-autolocale] ${msg}`) : log(`[react-autolocale] ${msg}`);
812
404
  async function refresh() {
813
- include = options.include ?? defaultInclude(root);
814
- includeDirs = include.map((d) => path.resolve(root, d) + path.sep);
815
- site = findSite(root, include);
816
- const { strings, contexts } = scanProjectDetailed(root, include);
817
- if (site.seo) {
818
- const html = path.join(root, "index.html");
819
- if (fs.existsSync(html)) strings.push(...headStrings(fs.readFileSync(html, "utf8")));
820
- }
821
- banner = await ensureBanner({
822
- root,
823
- langs: [site.original, ...site.languages],
824
- getTranslator,
825
- log
826
- });
827
- dicts = await ensureTranslations({
828
- root,
829
- site,
830
- strings,
831
- getTranslator,
832
- log,
833
- contexts,
834
- glossary: options.glossary,
835
- overrides: mergeOverrides(readOverrides(root), options.overrides)
836
- });
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\`.`);
837
418
  }
838
419
  function virtualModule() {
420
+ const { site } = project;
839
421
  const meta = site.languages.map((code) => ({
840
422
  code,
841
423
  native: getLanguage(code)?.native ?? code,
842
424
  rtl: !!getLanguage(code)?.rtl
843
425
  }));
844
- 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)})`);
845
427
  return [
846
428
  `export const original = ${JSON.stringify(site.original)};`,
847
429
  `export const originalNative = ${JSON.stringify(getLanguage(site.original)?.native ?? site.original)};`,
@@ -852,23 +434,35 @@ function reactAutolocale(options = {}) {
852
434
  ].join("\n");
853
435
  }
854
436
  let timer;
855
- let busy = false;
856
437
  function scheduleRefresh() {
857
438
  clearTimeout(timer);
858
439
  timer = setTimeout(async () => {
859
- if (busy || !server) return;
860
- busy = true;
440
+ if (!server) return;
861
441
  try {
862
442
  await refresh();
863
- const mod = server.moduleGraph.getModuleById(RESOLVED_ID);
864
- if (mod) server.moduleGraph.invalidateModule(mod);
443
+ for (const [id, mod] of server.moduleGraph.idToModuleMap) if (id.startsWith(RESOLVED_ID)) server.moduleGraph.invalidateModule(mod);
865
444
  server.ws.send({ type: "full-reload" });
866
445
  } catch (err) {
867
- log(`[react-autolocale] ${err.message}`);
868
- } finally {
869
- busy = false;
446
+ warn(err.message);
870
447
  }
871
- }, 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);
872
466
  }
873
467
  return {
874
468
  name: "react-autolocale",
@@ -886,19 +480,30 @@ function reactAutolocale(options = {}) {
886
480
  },
887
481
  configureServer(s) {
888
482
  server = s;
889
- const file = path.resolve(root, OVERRIDES_FILE);
890
- s.watcher.add(file);
891
- s.watcher.on("change", (changed) => changed === file && scheduleRefresh());
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
+ });
892
488
  },
893
489
  async buildStart() {
894
490
  if (config.build.ssr) return;
895
491
  await refresh();
492
+ if (server) server.watcher.add(project.output);
493
+ report();
896
494
  },
897
495
  resolveId(id) {
898
- 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;
899
499
  },
900
500
  load(id) {
901
- 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;
902
507
  },
903
508
  transform(code, id) {
904
509
  const file = id.split("?")[0];
@@ -906,9 +511,7 @@ function reactAutolocale(options = {}) {
906
511
  if (!includeDirs.some((d) => file.startsWith(d))) return null;
907
512
  const result = transformJsx(code, file);
908
513
  if (!result) return null;
909
- if (config.command === "serve" && site) {
910
- if (!site.languages.every((l) => result.strings.every((s) => site.exclude.includes(s.trim()) || dicts[l]?.[s]))) scheduleRefresh();
911
- }
514
+ if (config.command === "serve" && project) noteUntranslated(result.strings);
912
515
  return {
913
516
  code: result.code,
914
517
  map: result.map
@@ -920,14 +523,15 @@ function reactAutolocale(options = {}) {
920
523
  if (config.command !== "build" || config.build.ssr || rendered) return;
921
524
  rendered = true;
922
525
  const extra = typeof options.routes === "function" ? await options.routes() : options.routes ?? [];
923
- const routes = [.../* @__PURE__ */ new Set([...discoverRoutes(root, include), ...extra])];
526
+ const routes = [.../* @__PURE__ */ new Set([...discoverRoutes(root, project.include), ...extra])];
924
527
  await prerender({
925
528
  root,
926
529
  outDir: path.resolve(root, config.build.outDir),
927
- site,
928
- include,
530
+ site: project.site,
531
+ include: project.include,
929
532
  routes,
930
- dicts,
533
+ dicts: dicts.byId,
534
+ headDicts: dicts.byText,
931
535
  log
932
536
  });
933
537
  }