@menapo/vi18n 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +110 -2
  3. package/dist/apply-Bre-lY9J.js +228 -0
  4. package/dist/captured-Bj8bO6vd.js +1034 -0
  5. package/dist/check-BIKEuu6B.js +201 -0
  6. package/dist/check-HsmmpJZg.js +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +41 -0
  9. package/dist/codec-DuvU2VQy.js +207 -0
  10. package/dist/collect-Cx4_SKOe.js +13 -0
  11. package/dist/collisions-CfpZU8e4.js +217 -0
  12. package/dist/compile-BaDh0cHG.js +245 -0
  13. package/dist/compile-Cc97JaW2.js +2502 -0
  14. package/dist/concurrency-C9UUWuDC.js +96 -0
  15. package/dist/concurrency-DONL7t4V.js +1 -0
  16. package/dist/config.d.ts +21 -0
  17. package/dist/config.js +39 -0
  18. package/dist/dir-zw6bGMKQ.js +290 -0
  19. package/dist/duplicates-yuRljQ6h.js +185 -0
  20. package/dist/engine-C2ENBRJe.js +531 -0
  21. package/dist/engine.d.ts +43 -0
  22. package/dist/engine.js +1241 -0
  23. package/dist/exclude-Du-Zd_qI.js +26 -0
  24. package/dist/export-C9Sy6UWv.js +103 -0
  25. package/dist/extract-C3S8btx0.js +298 -0
  26. package/dist/formats-9unqj_fZ.js +781 -0
  27. package/dist/i18next-9HAoxwUI.js +1 -0
  28. package/dist/i18next-BEJgv_S8.js +1596 -0
  29. package/dist/import-nANvQutw.js +225 -0
  30. package/dist/index.d.ts +156 -0
  31. package/dist/index.js +239 -0
  32. package/dist/init-C2ScuW6a.js +792 -0
  33. package/dist/library-D5CWvTQs.js +1882 -0
  34. package/dist/lingui-zUiCl9Um.js +833 -0
  35. package/dist/lint.d.ts +105 -0
  36. package/dist/lint.js +971 -0
  37. package/dist/load-CQRz4PpM.js +2950 -0
  38. package/dist/load-gW-oJt1Q.js +747 -0
  39. package/dist/locales-DX4MOBFE.js +827 -0
  40. package/dist/lsp-RMMMc88P.js +31 -0
  41. package/dist/match-BGr8EXB3.js +89 -0
  42. package/dist/mcp-CHVwBoRP.js +28 -0
  43. package/dist/merge-driver-BTX87aVm.js +422 -0
  44. package/dist/messages-BfjIExyB.js +166 -0
  45. package/dist/migrate-IpLDMXmq.js +1484 -0
  46. package/dist/migrate-rCQuOGsf.js +1 -0
  47. package/dist/openai-CTaMDcl-.js +346 -0
  48. package/dist/output-DWE3SIUA.js +164 -0
  49. package/dist/parse.d.ts +62 -0
  50. package/dist/parse.js +156 -0
  51. package/dist/pipeline-C9Dbotkh.js +2031 -0
  52. package/dist/pipeline-DbX6rlo5.js +1 -0
  53. package/dist/project-Bw9UPIdx.js +1046 -0
  54. package/dist/project-C4GpaN-n.js +381 -0
  55. package/dist/pseudo-DRv30iN5.js +145 -0
  56. package/dist/raw-C4Y6vyGY.js +4708 -0
  57. package/dist/react-intl-BP0u-D5m.js +519 -0
  58. package/dist/restore-CiYv-jxS.js +175 -0
  59. package/dist/rewrite-EsIw_lsq.js +803 -0
  60. package/dist/rich-DCUKl5HM.js +97 -0
  61. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  62. package/dist/run-BEcnBbu-.js +694 -0
  63. package/dist/runtime-DTMmliT1.js +64 -0
  64. package/dist/scan-worker.d.ts +62 -0
  65. package/dist/scan-worker.js +1 -0
  66. package/dist/scoped-BMYpSthj.js +44 -0
  67. package/dist/server-Da1Ot9IN.js +10485 -0
  68. package/dist/server-deBOq2vq.js +11823 -0
  69. package/dist/server.d.ts +169 -0
  70. package/dist/server.js +1234 -0
  71. package/dist/sitemap-DJXzKoza.js +159 -0
  72. package/dist/sources-D19czDSA.js +2242 -0
  73. package/dist/status-zr4jY59Y.js +149 -0
  74. package/dist/stdio-CVaJfxA7.js +29 -0
  75. package/dist/stdio-FPOKQAgu.js +95 -0
  76. package/dist/sync-B9V2KFwb.js +658 -0
  77. package/dist/t-DC-c9zE5.js +454 -0
  78. package/dist/test.d.ts +116 -0
  79. package/dist/test.js +319 -0
  80. package/dist/translate-CQnJfLjO.js +299 -0
  81. package/dist/ts-BaU5yOf0.js +4863 -0
  82. package/dist/types-CgUCkFU1.d.ts +707 -0
  83. package/dist/types-D3QGM8XO.js +146 -0
  84. package/dist/types-DsdWBG0b.d.ts +558 -0
  85. package/dist/types-DtvQ7UdD.js +1948 -0
  86. package/dist/types-wSh9fCBY.js +11 -0
  87. package/dist/units-CQ5Q99RC.js +150 -0
  88. package/dist/url-CRfCOhUI.js +172 -0
  89. package/dist/url-CsBkRaJy.js +225 -0
  90. package/dist/validate-BxpupdAf.js +2615 -0
  91. package/dist/validate-D6DJepZv.js +365 -0
  92. package/dist/version-Byqi2guX.js +28 -0
  93. package/dist/vite-scan-CT_vsp4g.js +242 -0
  94. package/dist/vite.d.ts +34 -0
  95. package/dist/vite.js +7558 -0
  96. package/dist/vue-i18n-g0iGy6CZ.js +651 -0
  97. package/dist/which-BOemZ5Q8.js +116 -0
  98. package/dist/worker-84U6uYB3.js +1656 -0
  99. package/package.json +97 -3
  100. package/schemas/catalog.json +57 -0
package/dist/engine.js ADDED
@@ -0,0 +1,1241 @@
1
+ import { _ as DEV, c as page, d as resolved, f as setEngine, g as BUILD, i as created, n as S, o as nav, p as subs, r as changed, s as noop, t as textDirection, u as report, v as browser, x as warnOnce } from "./dir-zw6bGMKQ.js";
2
+ import { a as hostLocale, d as splitLocalePath, i as awayHref, s as localizePath, t as ROUTES } from "./url-CsBkRaJy.js";
3
+ import { a as strategyFor, i as readCookie, n as clientLocale, o as urlLocale, r as matchLocale, t as STORAGE_KEY } from "./match-BGr8EXB3.js";
4
+ //#region src/runtime/devtools-protocol.ts
5
+ /** Client → server: one translation to write. */
6
+ const EDIT_EVENT = "vi18n:edit";
7
+ /** Server → the client that asked: the outcome of one `EDIT_EVENT`. */
8
+ const EDIT_RESULT_EVENT = "vi18n:edit-result";
9
+ /**
10
+ * The page global through which the dock, a same-origin iframe, reaches the
11
+ * editor: it is a separate document with no module graph of its own.
12
+ */
13
+ const BRIDGE$1 = "__vi18nDevtools";
14
+ //#endregion
15
+ //#region src/runtime/devtools.ts
16
+ /**
17
+ * In-context editing (PLAN §4.13 P2; INTERNALS §2.18), dev only. With
18
+ * `devtools: true` the plugin's dev client boot calls `inContext`; nothing
19
+ * else imports this module, so builds and a default dev server never carry
20
+ * it (the engine entry exports it, and a side-effect import of the engine
21
+ * tree-shakes it away).
22
+ *
23
+ * No marker reaches the DOM. The page scope's `seen` hook records every id
24
+ * the page looks up, and a reverse index matches an element's text against
25
+ * patterns compiled from the scope's messages: literal text exactly, every
26
+ * argument as a wildcard, each select or plural case as an alternative.
27
+ * Alt+click on text opens a card with the message's source, notes, state
28
+ * and the locale's translation (from the dev server's `__vi18n/messages`);
29
+ * saving sends `vi18n:edit` over Vite's HMR channel, and the catalog HMR
30
+ * that follows the write re-renders the page. The Vite DevTools dock reaches
31
+ * the editor through the `BRIDGE` global.
32
+ */
33
+ const WILD = "[\\s\\S]*?";
34
+ /** Collapses whitespace the way rendered text is compared. */
35
+ const normalizeText = (text) => text.replace(/\s+/g, " ").trim();
36
+ function literal(text, weight) {
37
+ weight.n += text.replace(/\s+/g, "").length;
38
+ return text.split(/\s+/).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s*");
39
+ }
40
+ function partSource(part, weight) {
41
+ if (typeof part === "string") return literal(part, weight);
42
+ if (part === 0) return WILD;
43
+ const op = part[1];
44
+ if (op === 1 || op === 2 || op === 3) return `(?:${Object.values(part[2]).map((m) => messageSource(m, weight)).join("|")})`;
45
+ if (op === 7) return part.slice(2).map((p) => partSource(p, weight)).join("");
46
+ if (op === 8) return "";
47
+ return WILD;
48
+ }
49
+ function messageSource(message, weight) {
50
+ if (typeof message === "string") return literal(message, weight);
51
+ return message.map((part) => partSource(part, weight)).join("");
52
+ }
53
+ /**
54
+ * The pattern a message's rendered text matches, and how much literal text
55
+ * it pins down; null for a message that is only arguments, which any text
56
+ * would match.
57
+ */
58
+ function messagePattern(message) {
59
+ const weight = { n: 0 };
60
+ let source = messageSource(message, weight);
61
+ if (weight.n === 0) return null;
62
+ while (source.includes(WILD + WILD)) source = source.replaceAll(WILD + WILD, WILD);
63
+ return {
64
+ re: new RegExp(`^\\s*${source}\\s*$`),
65
+ weight: weight.n
66
+ };
67
+ }
68
+ /** A reverse index over one locale's messages, compiled on first use. */
69
+ function reverseIndex(messages) {
70
+ let patterns;
71
+ return { match(text, prefer) {
72
+ const target = normalizeText(text);
73
+ if (target === "") return [];
74
+ patterns ??= [...messages].flatMap(([id, message]) => {
75
+ const pattern = messagePattern(message);
76
+ return pattern === null ? [] : [{
77
+ id,
78
+ ...pattern
79
+ }];
80
+ });
81
+ const rank = (p) => (prefer?.has(p.id) ? 1e9 : 0) + p.weight;
82
+ return patterns.filter((p) => p.re.test(target)).sort((a, b) => rank(b) - rank(a)).map((p) => p.id);
83
+ } };
84
+ }
85
+ /** Text a user sees on an element besides its content. */
86
+ const ATTRIBUTES = [
87
+ "placeholder",
88
+ "title",
89
+ "aria-label",
90
+ "alt",
91
+ "value"
92
+ ];
93
+ /**
94
+ * The messages behind the text at `target`: its attributes, then the text
95
+ * of it and up to four ancestors, the innermost that matches winning (a
96
+ * click on the `<b>` of a rich message finds the message on its parent).
97
+ */
98
+ function locate(target, index, prefer) {
99
+ for (const name of ATTRIBUTES) {
100
+ const value = target.getAttribute(name);
101
+ const ids = value === null ? [] : index.match(value, prefer);
102
+ if (ids.length > 0) return {
103
+ element: target,
104
+ ids
105
+ };
106
+ }
107
+ let element = target;
108
+ for (let depth = 0; element !== null && depth < 5; depth++, element = element.parentElement) {
109
+ const text = element.textContent ?? "";
110
+ if (text.length > 4e3) break;
111
+ const ids = index.match(text, prefer);
112
+ if (ids.length > 0) return {
113
+ element,
114
+ ids
115
+ };
116
+ }
117
+ return null;
118
+ }
119
+ /** Space kept from the viewport's edges, and between the card and the text. */
120
+ const MARGIN = 8;
121
+ /** Below this, a side is too short for a card that scrolls inside. */
122
+ const MIN_HEIGHT = 120;
123
+ const clamp = (value, low, high) => Math.max(low, Math.min(value, high));
124
+ /**
125
+ * Places a `card`-sized box next to `anchor` (the clicked text) inside the
126
+ * viewport without covering it: below when the card fits there, else above,
127
+ * else on the roomier of the two with the card scrolling inside, else beside
128
+ * the text when it is too tall for either; only text taller and wider than
129
+ * the viewport allows leaves the card over part of it.
130
+ */
131
+ function placeCard(anchor, card, viewport) {
132
+ const below = viewport.height - MARGIN - (anchor.bottom + MARGIN);
133
+ const above = anchor.top - MARGIN - MARGIN;
134
+ const left = clamp(anchor.left, MARGIN, viewport.width - MARGIN - card.width);
135
+ const vertical = (side, room) => {
136
+ const height = Math.min(card.height, room);
137
+ const top = side === "below" ? anchor.bottom + MARGIN : anchor.top - MARGIN - height;
138
+ return {
139
+ top: clamp(top, MARGIN, viewport.height - MARGIN - height),
140
+ left,
141
+ maxHeight: card.height > room ? room : null
142
+ };
143
+ };
144
+ if (card.height <= below) return vertical("below", below);
145
+ if (card.height <= above) return vertical("above", above);
146
+ if (Math.max(below, above) >= MIN_HEIGHT) return below >= above ? vertical("below", below) : vertical("above", above);
147
+ const right = viewport.width - MARGIN - (anchor.right + MARGIN);
148
+ const before = anchor.left - MARGIN - MARGIN;
149
+ const room = viewport.height - 16;
150
+ const top = clamp(anchor.top, MARGIN, viewport.height - MARGIN - Math.min(card.height, room));
151
+ const maxHeight = card.height > room ? room : null;
152
+ if (right >= card.width) return {
153
+ top,
154
+ left: anchor.right + MARGIN,
155
+ maxHeight
156
+ };
157
+ if (before >= card.width) return {
158
+ top,
159
+ left: anchor.left - MARGIN - card.width,
160
+ maxHeight
161
+ };
162
+ return {
163
+ top: viewport.height - MARGIN - Math.min(card.height, MIN_HEIGHT),
164
+ left,
165
+ maxHeight: card.height > MIN_HEIGHT ? MIN_HEIGHT : null
166
+ };
167
+ }
168
+ const STYLE = `
169
+ :host { all: initial; }
170
+ .box { position: fixed; z-index: 2147483647; pointer-events: none; outline: 2px solid #7c3aed; border-radius: 2px; }
171
+ .card { position: fixed; z-index: 2147483647; width: min(380px, calc(100vw - 16px)); max-height: min(70vh, 560px); overflow: auto;
172
+ box-sizing: border-box; padding: 12px; border-radius: 8px; border: 1px solid #8884; box-shadow: 0 8px 24px #0004;
173
+ font: 13px/1.4 system-ui, sans-serif; color-scheme: light dark; background: Canvas; color: CanvasText; }
174
+ .row { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
175
+ .grow { flex: 1; }
176
+ code, pre, textarea { font: 12px/1.4 ui-monospace, monospace; }
177
+ pre { margin: 4px 0 0; padding: 6px; white-space: pre-wrap; word-break: break-word; border-radius: 4px; background: #8881; }
178
+ textarea { box-sizing: border-box; width: 100%; min-height: 64px; margin-top: 4px; padding: 6px; resize: vertical; color: inherit; background: transparent; border: 1px solid #8886; border-radius: 4px; }
179
+ .muted { opacity: .7; }
180
+ .badge { padding: 0 6px; border-radius: 9px; font-size: 11px; background: #8882; }
181
+ .missing { background: #f973; } .ai { background: #38f3; } .translated { background: #2a63; }
182
+ .problem { margin-top: 6px; color: #d33; } .problem.warning { color: #b80; }
183
+ button { font: inherit; padding: 2px 10px; border-radius: 4px; border: 1px solid #8886; color: inherit; background: #8881; cursor: pointer; }
184
+ button:disabled { opacity: .5; cursor: default; }
185
+ ul { margin: 6px 0 0; padding: 0; list-style: none; } li { padding: 2px 0; cursor: pointer; } li:hover { text-decoration: underline; }
186
+ `;
187
+ function h(tag, props = {}, ...children) {
188
+ const element = document.createElement(tag);
189
+ for (const [key, value] of Object.entries(props)) if (key.startsWith("on")) element.addEventListener(key.slice(2), value);
190
+ else element[key] = value;
191
+ for (const child of children) if (child !== null && child !== false) element.append(child);
192
+ return element;
193
+ }
194
+ /** Moves an installed editor to the `hot` of a boot module that ran again. */
195
+ let attach;
196
+ /**
197
+ * Installs the editor on the page: the `seen` recorder on the page scope,
198
+ * the Alt+click listener, the HMR reply listener and the dock's bridge.
199
+ * Called by the plugin's dev client boot (`devtools: true`) with its
200
+ * `import.meta.hot`; a no-op outside a document or without HMR.
201
+ */
202
+ function inContext(hot, endpoint) {
203
+ if (hot === void 0 || typeof document !== "object") return;
204
+ if (attach !== void 0) return attach(hot);
205
+ const page = S.resolve();
206
+ const seen = /* @__PURE__ */ new Set();
207
+ page.seen = (id) => {
208
+ seen.add(id);
209
+ };
210
+ let indexed;
211
+ let index = reverseIndex(/* @__PURE__ */ new Map());
212
+ const currentIndex = () => {
213
+ if (indexed !== page.messages) index = reverseIndex(indexed = page.messages);
214
+ return index;
215
+ };
216
+ let nonce = 0;
217
+ const waiting = /* @__PURE__ */ new Map();
218
+ const listeners = /* @__PURE__ */ new Set();
219
+ let channel = hot;
220
+ attach = (next) => {
221
+ channel = next;
222
+ next.on(EDIT_RESULT_EVENT, (result) => {
223
+ waiting.get(result.nonce)?.(result);
224
+ waiting.delete(result.nonce);
225
+ });
226
+ next.on("vi18n:catalog-update", () => {
227
+ for (const listener of listeners) listener();
228
+ });
229
+ };
230
+ attach(hot);
231
+ const save = (request) => new Promise((done) => {
232
+ waiting.set(++nonce, done);
233
+ channel.send(EDIT_EVENT, {
234
+ ...request,
235
+ nonce
236
+ });
237
+ });
238
+ const load = async (ids) => {
239
+ const query = new URLSearchParams({ locale: page.locale });
240
+ for (const id of ids) query.append("id", id);
241
+ const response = await fetch(`${endpoint}?${query}`, { cache: "no-store" });
242
+ if (!response.ok) throw new Error(`${endpoint} answered ${response.status}`);
243
+ return await response.json();
244
+ };
245
+ const host = document.createElement("div");
246
+ host.setAttribute("data-vi18n-devtools", "");
247
+ const root = host.attachShadow({ mode: "open" });
248
+ root.append(h("style", { textContent: STYLE }));
249
+ const box = h("div", { className: "box" });
250
+ const card = h("div", { className: "card" });
251
+ /** The text the card belongs to; null when the dock opened it. */
252
+ let anchor = null;
253
+ const close = () => {
254
+ anchor = null;
255
+ box.remove();
256
+ card.remove();
257
+ };
258
+ const place = () => {
259
+ if (anchor === null || !card.isConnected) return;
260
+ if (!anchor.isConnected) {
261
+ box.remove();
262
+ return;
263
+ }
264
+ const rect = anchor.getBoundingClientRect();
265
+ Object.assign(box.style, {
266
+ top: `${rect.top - 2}px`,
267
+ left: `${rect.left - 2}px`,
268
+ width: `${rect.width + 4}px`,
269
+ height: `${rect.height + 4}px`
270
+ });
271
+ card.style.maxHeight = "";
272
+ const at = placeCard(rect, card.getBoundingClientRect(), {
273
+ width: innerWidth,
274
+ height: innerHeight
275
+ });
276
+ Object.assign(card.style, {
277
+ top: `${at.top}px`,
278
+ left: `${at.left}px`,
279
+ maxHeight: at.maxHeight === null ? "" : `${at.maxHeight}px`
280
+ });
281
+ };
282
+ new ResizeObserver(place).observe(card);
283
+ const show = async (ids, at) => {
284
+ const id = ids[at];
285
+ let data;
286
+ try {
287
+ data = await load([id]);
288
+ } catch (error) {
289
+ card.replaceChildren(h("div", { className: "problem" }, String(error)));
290
+ return;
291
+ }
292
+ const message = data.messages[0];
293
+ if (message === void 0) {
294
+ card.replaceChildren(h("div", {}, h("code", {}, id), " is not a message the dev server knows."));
295
+ return;
296
+ }
297
+ const area = h("textarea", {
298
+ value: message.text,
299
+ readOnly: !data.editable,
300
+ placeholder: data.editable ? "Untranslated" : ""
301
+ });
302
+ const problems = h("div");
303
+ const button = h("button", {
304
+ textContent: "Save",
305
+ disabled: !data.editable,
306
+ onclick: async () => {
307
+ button.disabled = true;
308
+ const result = await save({
309
+ id,
310
+ locale: data.locale,
311
+ source: message.source,
312
+ expected: message.text,
313
+ translation: area.value
314
+ });
315
+ button.disabled = false;
316
+ problems.replaceChildren(...result.problems.map((p) => h("div", { className: `problem ${p.severity}` }, `${p.code}: ${p.message}`)));
317
+ if (result.ok && result.problems.length === 0) close();
318
+ }
319
+ });
320
+ area.addEventListener("keydown", (event) => {
321
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) button.click();
322
+ });
323
+ const list = h("ul", { hidden: true });
324
+ const listButton = h("button", {
325
+ textContent: `Page messages (${seen.size})`,
326
+ onclick: async () => {
327
+ list.hidden = !list.hidden;
328
+ if (list.hidden) return;
329
+ const all = await load([...seen]);
330
+ list.replaceChildren(...all.messages.map((m) => h("li", { onclick: () => void show([m.id], 0) }, h("span", { className: `badge ${m.state}` }, m.reason ?? m.state), " ", m.text || m.source)));
331
+ }
332
+ });
333
+ const step = (by) => h("button", {
334
+ textContent: by < 0 ? "‹" : "›",
335
+ disabled: at + by < 0 || at + by >= ids.length,
336
+ onclick: () => void show(ids, at + by)
337
+ });
338
+ card.replaceChildren(h("div", { className: "row" }, h("code", { className: "grow" }, id), h("span", { className: `badge ${message.state}` }, message.reason ?? message.state), h("span", { className: "muted" }, data.locale), h("button", {
339
+ textContent: "×",
340
+ title: "Close (Esc)",
341
+ onclick: close
342
+ })), h("div", { className: "muted" }, "Source"), h("pre", {}, message.source), message.contexts.length > 0 ? h("div", { className: "muted" }, message.contexts.join(" · ")) : "", h("div", { className: "muted" }, [message.at, message.file].filter(Boolean).join(" → "), message.maxLength === null ? "" : ` · max ${message.maxLength}`), data.editable ? area : h("div", { className: "muted" }, data.locale === data.sourceLocale ? "The source locale's text is the code's: edit the call." : `${data.locale} has no catalog of its own.`), problems, h("div", { className: "row" }, button, ids.length > 1 && step(-1), ids.length > 1 && `${at + 1}/${ids.length}`, ids.length > 1 && step(1), h("span", { className: "grow" }), listButton), list);
343
+ };
344
+ const open = (element, ids) => {
345
+ if (!host.isConnected) document.body.append(host);
346
+ anchor = element;
347
+ if (element === null) {
348
+ box.remove();
349
+ Object.assign(card.style, {
350
+ top: "8px",
351
+ left: `${Math.max(8, innerWidth - 396)}px`,
352
+ maxHeight: ""
353
+ });
354
+ root.append(card);
355
+ show(ids, 0);
356
+ return;
357
+ }
358
+ root.append(box, card);
359
+ place();
360
+ show(ids, 0);
361
+ };
362
+ document.addEventListener("click", (event) => {
363
+ if (!event.altKey || event.composedPath().includes(host)) return;
364
+ const target = event.target;
365
+ if (!(target instanceof Element)) return;
366
+ const found = locate(target, currentIndex(), seen);
367
+ if (found === null) return;
368
+ event.preventDefault();
369
+ event.stopPropagation();
370
+ open(found.element, found.ids);
371
+ }, true);
372
+ document.addEventListener("keydown", (event) => {
373
+ if (event.key === "Escape") close();
374
+ });
375
+ addEventListener("scroll", place, {
376
+ passive: true,
377
+ capture: true
378
+ });
379
+ addEventListener("resize", place, { passive: true });
380
+ globalThis[BRIDGE$1] = {
381
+ locale: () => page.locale,
382
+ seen: () => [...seen],
383
+ open: (id) => open(null, [id]),
384
+ subscribe(listener) {
385
+ listeners.add(listener);
386
+ return () => void listeners.delete(listener);
387
+ }
388
+ };
389
+ }
390
+ //#endregion
391
+ //#region src/runtime/fetch.ts
392
+ /**
393
+ * The same-origin `fetch` wrapper (PLAN §5.12): server functions, actions and
394
+ * API calls learn the tab's locale from `x-vi18n-locale`, per tab and without
395
+ * a shared cookie. It wraps once, when the runtime evaluates, because the boot
396
+ * import runs before framework runtimes that keep their own `fetch` reference.
397
+ */
398
+ /**
399
+ * `header()` returns the locale to send, or null to leave requests alone.
400
+ * Browser only (the engine calls it in a document or a module worker), where
401
+ * `fetch`, `Request` and `location` always exist.
402
+ */
403
+ function wrapFetch(header) {
404
+ const original = globalThis.fetch;
405
+ const wrapped = (input, init) => {
406
+ const locale = header();
407
+ if (locale) try {
408
+ if (new URL(input.url ?? input, BUILD || browser() ? document.baseURI : location.href).origin === location.origin) {
409
+ const headers = new Headers(init?.headers ?? input.headers);
410
+ if (!headers.has("x-vi18n-locale")) {
411
+ headers.set("x-vi18n-locale", locale);
412
+ init = {
413
+ ...init,
414
+ headers
415
+ };
416
+ }
417
+ }
418
+ } catch {}
419
+ return original(input, init);
420
+ };
421
+ try {
422
+ globalThis.fetch = wrapped;
423
+ } catch {
424
+ if (!Reflect.defineProperty(globalThis, "fetch", {
425
+ value: wrapped,
426
+ writable: true,
427
+ configurable: true
428
+ }) && DEV) warnOnce("fetch is read-only here, so requests carry no x-vi18n-locale");
429
+ }
430
+ return BUILD ? noop : () => {
431
+ if (globalThis.fetch === wrapped) globalThis.fetch = original;
432
+ };
433
+ }
434
+ //#endregion
435
+ //#region src/runtime/navigation.ts
436
+ /**
437
+ * Navigation watcher (INTERNALS §2.11.7): a client-router navigation to a URL
438
+ * in another locale (a plain `<a href="/de/pricing">`) switches the page in
439
+ * place, so the router renders the new route with the new strings loaded.
440
+ */
441
+ /**
442
+ * Calls `onChange` after every same-document navigation committed, when
443
+ * `location` is the new URL. Browser only (the engine calls it in a
444
+ * document), where `history` exists.
445
+ */
446
+ function watchNavigation(onChange) {
447
+ const nav = globalThis.navigation;
448
+ if (nav?.currentEntry) {
449
+ nav.addEventListener("currententrychange", onChange);
450
+ return;
451
+ }
452
+ for (const name of ["pushState", "replaceState"]) {
453
+ const original = history[name];
454
+ history[name] = function(...args) {
455
+ const r = original.apply(this, args);
456
+ onChange();
457
+ return r;
458
+ };
459
+ }
460
+ addEventListener("popstate", onChange);
461
+ }
462
+ //#endregion
463
+ //#region src/runtime/loader.ts
464
+ /**
465
+ * The client engine behind the global state (INTERNALS §2.11): registration,
466
+ * bundle loading, skew handling, locale switching, the navigation watcher,
467
+ * the `fetch` wrapper and persistence. Only `@menapo/vi18n/engine` imports
468
+ * it, and the generated client boot module imports that entry first; it
469
+ * hooks into the core's `register` and replaces the inert client methods
470
+ * `state.ts` installs, so neither the `.` entry nor `/server` carries it.
471
+ *
472
+ * Its data is private to the runtime copy that created the state; every
473
+ * public export reaches it through the state's methods, so a second copy never
474
+ * reads its own empty variables. Module-level variables rather than one
475
+ * object, and one-letter fields on `LocaleData`: the minifier renames
476
+ * variables but never properties, and this file is most of the engine's
477
+ * 3.5 KB budget (PLAN §6).
478
+ */
479
+ const own = Object.hasOwn;
480
+ /**
481
+ * `ssr.payload: "preload"` compiled in (the plugin's `__VI18N_PRELOAD__`):
482
+ * default builds fold its branch out (PLAN §6), code bundled without the
483
+ * plugin keeps it.
484
+ */
485
+ const PRELOAD = typeof __VI18N_PRELOAD__ === "undefined" || !!__VI18N_PRELOAD__;
486
+ /**
487
+ * `output.compactIds` (the plugin's `__VI18N_COMPACT__`): bundle files are
488
+ * arrays of messages, mapped to ids through the version's `ids.<v>.json`.
489
+ * Folded out of default builds; undefined is false, as only the plugin's
490
+ * boot module imports this engine.
491
+ */
492
+ const COMPACT = typeof __VI18N_COMPACT__ !== "undefined" && !!__VI18N_COMPACT__;
493
+ /**
494
+ * `output.perLocale` (the plugin's `__VI18N_INLINE__`): chunks add the
495
+ * messages they inline through `add`. Folded out of default builds.
496
+ */
497
+ const INLINE = typeof __VI18N_INLINE__ !== "undefined" && !!__VI18N_INLINE__;
498
+ const { setLocale: clientOnly } = S;
499
+ let reg;
500
+ const locs = /* @__PURE__ */ new Map();
501
+ /** Message-set version the page booted with. */
502
+ let version;
503
+ let inline;
504
+ /** `output.compactIds`: the version's key → ids, fetched once and shared by every locale. */
505
+ let order;
506
+ /** Ids the page looked up and missed: the merge that brings one re-renders (§6 D43). */
507
+ const missed = /* @__PURE__ */ new Set();
508
+ /** Loads `ready()` waits for. */
509
+ const inflight = /* @__PURE__ */ new Set();
510
+ /** Bumped per switch, so a slow earlier `setLocale` cannot flip after a later one. */
511
+ let switches = 0;
512
+ /**
513
+ * The locale a URL-driven switch is loading, set exactly while one is
514
+ * pending: the URL names it, or a clicked link is about to, so the new
515
+ * page's route guards load their strings in it. Requests carry it once the
516
+ * URL names it (§6 D108).
517
+ */
518
+ let heading;
519
+ /** The pending switch's loading: true once complete, false when a later switch took over. */
520
+ let loading;
521
+ /** The path of the last commit the watcher saw. */
522
+ let committed;
523
+ let watching;
524
+ /** Restores the original `fetch`: Vitest registrations must not see the wrapper. */
525
+ let unwrap = noop;
526
+ const loc = (l) => {
527
+ let d = locs.get(l);
528
+ if (!d) locs.set(l, d = {
529
+ m: /* @__PURE__ */ new Map(),
530
+ k: /* @__PURE__ */ new Set(),
531
+ p: /* @__PURE__ */ new Map()
532
+ });
533
+ return d;
534
+ };
535
+ const error = (text, extra) => Object.assign(/* @__PURE__ */ new Error("vi18n: " + text), extra);
536
+ const parse = (text) => {
537
+ try {
538
+ return JSON.parse(text);
539
+ } catch {
540
+ return null;
541
+ }
542
+ };
543
+ const setCookie = (name, value, maxAge) => {
544
+ document.cookie = name + "=" + encodeURIComponent(value) + "; Path=/; Max-Age=" + maxAge + "; SameSite=Lax";
545
+ };
546
+ const byId = (id) => BUILD || browser() ? parse(document.querySelector("script#" + id)?.textContent) : null;
547
+ const abs = (url) => BUILD || browser() ? new URL(url, document.baseURI).href : url;
548
+ const assets = () => reg?.assetsUrl ?? inline?.assetsUrl ?? "";
549
+ /** A bundle's URL, and with `key` = "manifest" a manifest slice's (§2.9.5). */
550
+ const bundleUrl = (l, key, hash) => abs(assets() + l + "/" + key + "." + hash + ".json");
551
+ const stableUrl = (l) => abs((reg?.manifestUrl ?? inline?.manifestUrl ?? "").replace("{locale}", l));
552
+ const STABLE = { cache: "no-cache" };
553
+ const request = globalThis.fetch;
554
+ /**
555
+ * JSON from a boot-stub fetch (each consumed once) or the network. Anything
556
+ * but an object is a 404: hosts with an SPA fallback (Netlify, Vercel or
557
+ * Cloudflare Pages rewrites, Firebase) answer a file a deploy removed with
558
+ * `200 index.html`, which must take the stable-manifest path like a 404,
559
+ * never become a bundle.
560
+ */
561
+ async function json(url, init) {
562
+ const fetches = S.boot?.fetches;
563
+ const pending = fetches?.[url];
564
+ let data;
565
+ let status = 404;
566
+ if (pending) {
567
+ delete fetches[url];
568
+ data = await pending;
569
+ } else {
570
+ const r = await request(url, init);
571
+ if (r.ok) data = await r.json().catch(noop);
572
+ else status = r.status;
573
+ }
574
+ if (!data || typeof data !== "object") throw error(status + " " + url, { status });
575
+ return data;
576
+ }
577
+ /**
578
+ * The one skew decision (PLAN §5.11): the first version read is the page's;
579
+ * a later manifest or payload of another version belongs to another deploy,
580
+ * and its bundles are never mixed with ours.
581
+ */
582
+ function checkVersion(v) {
583
+ if ((version ??= v) !== v) throw error("skew " + v, { skew: 1 });
584
+ }
585
+ /** Merges into a new Map; re-renders only when it repairs a recorded miss. */
586
+ function merge(l, data) {
587
+ const d = loc(l);
588
+ const isPage = page.locale === l;
589
+ const m = new Map(d.m);
590
+ let repaired = false;
591
+ for (const b of data) for (const id in b) if (own(b, id)) {
592
+ m.set(id, b[id]);
593
+ if (isPage && missed.delete(id)) repaired = true;
594
+ }
595
+ d.m = m;
596
+ if (isPage) {
597
+ page.messages = m;
598
+ if (repaired) changed(true);
599
+ }
600
+ }
601
+ /**
602
+ * Adopts a payload's bundles (marked loaded) and fallback-mode messages, and
603
+ * starts the bundles it preloads (`ssr.payload: "preload"`): the page's
604
+ * `<link rel="preload">` already fetches them, so this fetch takes those
605
+ * responses, and a bundle no guard asks for soon is still used.
606
+ */
607
+ function adopt(p) {
608
+ const d = loc(p.locale);
609
+ const data = [];
610
+ for (const k in p.bundles) if (own(p.bundles, k)) {
611
+ d.k.add(k);
612
+ data.push(p.bundles[k]);
613
+ }
614
+ data.push(p.messages);
615
+ merge(p.locale, data);
616
+ if (PRELOAD && p.preload) load(p.locale, p.preload).catch(noop);
617
+ }
618
+ /**
619
+ * The page payload's zone, `now` and manifest, so islands hydrate as the
620
+ * server rendered them (PLAN §5.13): the boot page's, then each one a
621
+ * document-swapping router (Astro's) brings in with the next page. Builds
622
+ * run it through `adoptLate`, with the payload's bundles; dev, which adopts
623
+ * no payload, from `boot`.
624
+ */
625
+ function hydrate(p) {
626
+ if (p.manifest) {
627
+ const d = loc(p.locale);
628
+ d.h = p.manifest;
629
+ d.r ??= resolved;
630
+ }
631
+ try {
632
+ Intl.DateTimeFormat(void 0, { timeZone: p.timeZone });
633
+ page.timeZone = p.timeZone;
634
+ } catch {}
635
+ page.now = p.now;
636
+ page.timeZoneSource = "client";
637
+ }
638
+ /**
639
+ * Payloads not read yet, each adopted once: the page payload (boot's, then
640
+ * that of each page a document-swapping router brings in) and late ones
641
+ * (ids recorded after `</head>` was flushed). Each element is marked read
642
+ * with an expando property: no attribute changes a hydrating framework
643
+ * could see, and fewer bytes than a `WeakSet`. The mark comes after the
644
+ * version check, so another deploy's payload fails every later load, as it
645
+ * failed the first, instead of leaving its keys unknown and its strings empty.
646
+ */
647
+ function adoptLate() {
648
+ if (!(BUILD || browser())) return;
649
+ for (const el of document.querySelectorAll("script#__vi18n,script[data-vi18n-late]")) {
650
+ const p = el.vi18n ? null : parse(el.textContent);
651
+ if (p) {
652
+ if (p.v != null) checkVersion(p.v);
653
+ if (el.id) hydrate(p);
654
+ adopt(p);
655
+ }
656
+ el.vi18n = 1;
657
+ }
658
+ }
659
+ function setDev(l, catalog) {
660
+ const d = loc(l);
661
+ d.m = new Map(Object.entries(catalog.messages ?? {}));
662
+ d.labels = new Map(Object.entries(catalog.labels ?? {}));
663
+ if (page.locale === l) {
664
+ page.messages = d.m;
665
+ page.labels = d.labels;
666
+ }
667
+ }
668
+ function devCatalog(l) {
669
+ const d = loc(l);
670
+ const catalogs = reg?.catalogs;
671
+ return d.r ??= catalogs && own(catalogs, l) ? catalogs[l]().then((m) => setDev(l, m.default), (e) => {
672
+ d.r = void 0;
673
+ if (DEV) console.error("vi18n: loading the " + l + " catalog failed", e);
674
+ }) : resolved;
675
+ }
676
+ /**
677
+ * Settles once the locale's `{ key: hash }` map is in `d.h`, acquired once
678
+ * from the first source that applies (§2.11.1); a page payload's map comes
679
+ * through `hydrate`.
680
+ */
681
+ function manifest(l) {
682
+ const d = loc(l);
683
+ return d.r ??= acquire(l).then((h) => d.h = h, (e) => {
684
+ d.r = void 0;
685
+ throw e;
686
+ });
687
+ }
688
+ async function acquire(l) {
689
+ const b = S.boot;
690
+ if (b?.locale === l && b.manifest) return b.manifest;
691
+ const entry = inline?.locales[l];
692
+ if (entry) return inline.kind === "full" ? entry : slice(l, bundleUrl(l, "manifest", entry.slice));
693
+ return slice(l, stableUrl(l), STABLE);
694
+ }
695
+ const pause = () => new Promise((r) => setTimeout(r, 250));
696
+ /**
697
+ * A manifest slice, retried once after 250 ms like bundles (a boot-stub
698
+ * entry is consumed by the first try, so the retry goes to the network). A
699
+ * 404 means a translation-only deploy replaced the hashed slice the HTML
700
+ * names: the stable slice is the current one, and `checkVersion` decides
701
+ * whether its bundles may still be mixed with ours.
702
+ */
703
+ async function slice(l, url, init) {
704
+ let s;
705
+ try {
706
+ s = await json(url, init);
707
+ } catch (e) {
708
+ await pause();
709
+ const gone = e.status === 404;
710
+ s = await json(gone ? stableUrl(l) : url, gone ? STABLE : init);
711
+ }
712
+ checkVersion(s.v);
713
+ if (s.c && (BUILD || browser())) for (const e of document.querySelectorAll("[rel=modulepreload]")) {
714
+ const keys = s.c[e.href.split("/").pop()];
715
+ if (keys) load(l, keys).catch(noop);
716
+ }
717
+ return s.bundles;
718
+ }
719
+ /**
720
+ * One bundle, retried once after 250 ms. A 404 means the hash moved (a
721
+ * translation-only deploy): the stable slice gives the new hashes while its
722
+ * version is still ours, and a skew error otherwise.
723
+ */
724
+ async function bundle(l, key, d) {
725
+ try {
726
+ return await json(bundleUrl(l, key, d.h[key]));
727
+ } catch (e) {
728
+ await pause();
729
+ if (e.status === 404) {
730
+ if (!own(d.h = await slice(l, stableUrl(l), STABLE), key)) return {};
731
+ }
732
+ return await json(bundleUrl(l, key, d.h[key]));
733
+ }
734
+ }
735
+ /**
736
+ * `ids.<v>.json`, fetched once per page, in parallel with the first array
737
+ * bundle, retried once like bundles; a failure is forgotten so the next load
738
+ * tries again. Every array bundle of this version maps through it: `v`
739
+ * covers each key's ids (INTERNALS §2.9.3).
740
+ */
741
+ function ids() {
742
+ const url = abs(assets() + "ids." + version + ".json");
743
+ return order ??= json(url).catch(() => pause().then(() => json(url))).catch((e) => {
744
+ order = void 0;
745
+ throw e;
746
+ });
747
+ }
748
+ /**
749
+ * An array bundle's messages under their ids; positions without a message
750
+ * (`null`) stay out. Null-prototype, so an id named `__proto__` is a key.
751
+ */
752
+ function zip(keys = [], values) {
753
+ const b = Object.create(null);
754
+ keys.forEach((id, i) => {
755
+ if (values[i] != null) b[id] = values[i];
756
+ });
757
+ return b;
758
+ }
759
+ async function fetchKeys(l, keys, d) {
760
+ await manifest(l);
761
+ adoptLate();
762
+ const todo = (k) => !d.k.has(k) && own(d.h, k);
763
+ if ((BUILD || browser()) && document.readyState === "loading" && keys.some(todo)) {
764
+ await new Promise((r) => document.addEventListener("DOMContentLoaded", r));
765
+ adoptLate();
766
+ }
767
+ await Promise.all(keys.filter(todo).map((k) => {
768
+ let p = d.p.get(k);
769
+ if (!p) d.p.set(k, p = (COMPACT ? Promise.all([bundle(l, k, d), ids()]).then(([b, o]) => Array.isArray(b) ? zip(o[k], b) : b) : bundle(l, k, d)).then((b) => {
770
+ d.k.add(k);
771
+ merge(l, [b]);
772
+ }).finally(() => d.p.delete(k)));
773
+ return p;
774
+ }));
775
+ }
776
+ /** Registers a load `ready()` waits for. */
777
+ function inFlight(p) {
778
+ inflight.add(p);
779
+ const done = () => inflight.delete(p);
780
+ p.then(done, done);
781
+ return p;
782
+ }
783
+ /** Loads `keys` of locale `l`; dev and server catalogs are complete and resolve at once. */
784
+ function load(l, keys) {
785
+ if (!reg || !BUILD && reg.consumer !== "client") return resolved;
786
+ return inFlight(!BUILD && reg.mode === "dev" ? devCatalog(l) : fetchKeys(l, keys, loc(l)));
787
+ }
788
+ /**
789
+ * Vite's reload contract for failed route loads: a cancelable
790
+ * `vite:preloadError` whose `payload` is the error, so apps' existing
791
+ * reload-on-deploy handlers cover vi18n's skew too.
792
+ */
793
+ function preloadError(e) {
794
+ if (BUILD || browser()) {
795
+ const ev = new Event("vite:preloadError", { cancelable: true });
796
+ ev.payload = e;
797
+ if (!dispatchEvent(ev)) return;
798
+ }
799
+ throw e;
800
+ }
801
+ /**
802
+ * Loads `keys` for the page the router is about to render. Under a URL-driven
803
+ * switch that is the switch's locale, and the guard also waits for the rest
804
+ * of it, so the route renders after the flip (routers that push first) or the
805
+ * URL commit flips at once (routers that load first); never in the old
806
+ * locale under the new URL. Until the URL names that locale, the page's
807
+ * locale loads them too: no event tells a clicked link the router follows
808
+ * from one a blocker stopped or the browser downloads, and the caller may be
809
+ * a lazy part of the page still showing (§2.11.7). A switch that was
810
+ * cancelled, or that flipped to another locale meanwhile, sends it round again.
811
+ */
812
+ async function want(keys) {
813
+ const l = heading ?? page.locale;
814
+ await Promise.all([load(l, keys), l === sure() || load(page.locale, keys)]);
815
+ if (l === heading ? !await loading : l !== page.locale) return want(keys);
816
+ }
817
+ const need = (keys) => keys.length ? want(keys).catch(preloadError) : resolved;
818
+ function flip(l) {
819
+ const d = loc(l);
820
+ page.locale = l;
821
+ page.messages = d.m;
822
+ page.labels = d.labels;
823
+ missed.clear();
824
+ if (BUILD || browser()) Object.assign(document.documentElement, {
825
+ lang: l,
826
+ dir: textDirection(l)
827
+ });
828
+ }
829
+ /**
830
+ * The engine's half of `register`: the core already recorded the
831
+ * registration, filled `locales` and set the page scope to the source locale.
832
+ */
833
+ function boot(r) {
834
+ reg = r;
835
+ if (!BUILD && r.test) {
836
+ unwrap();
837
+ return devCatalog(r.config.sourceLocale);
838
+ }
839
+ if (!BUILD && r.consumer !== "client") return resolved;
840
+ if (!BUILD && !browser() && globalThis.WorkerGlobalScope) return worker(r);
841
+ if (ROUTES && r.config.routing.domains && (BUILD || browser())) r.config.routing.home = hostLocale(location.host, r.config);
842
+ const p = byId("__vi18n");
843
+ const n = BUILD ? navigator : globalThis.navigator;
844
+ const l = [p?.locale, S.boot?.locale].find((x) => S.locales.includes(x)) ?? clientLocale(r.config, {
845
+ pathname: BUILD || browser() ? location.pathname : "/",
846
+ cookie: BUILD || browser() ? document.cookie : "",
847
+ storage: (k) => localStorage.getItem(k),
848
+ languages: BUILD ? navigator.languages ?? [navigator.language] : n?.languages ?? [n?.language]
849
+ }).locale;
850
+ if (p && !BUILD && r.mode === "dev") hydrate(p);
851
+ flip(l);
852
+ if (BUILD || browser()) {
853
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
854
+ if (tz && readCookie(document.cookie, "vi18n_tz") !== tz) setCookie("vi18n_tz", tz, 31536e3);
855
+ if (BUILD || !watching) {
856
+ watching = true;
857
+ committed = location.pathname;
858
+ watchNavigation(onNavigate);
859
+ addEventListener("click", onClick, true);
860
+ }
861
+ }
862
+ if (!BUILD && r.mode === "dev") return inFlight(devCatalog(l));
863
+ const m = r.manifest;
864
+ inline = m && typeof m === "object" ? m : byId("__vi18n_manifest");
865
+ version = p?.v ?? S.boot?.v ?? inline?.v;
866
+ try {
867
+ adoptLate();
868
+ } catch {}
869
+ inFlight(manifest(l)).catch(noop);
870
+ return resolved;
871
+ }
872
+ /** Whether `path`'s strategy chain lists `strategy`. */
873
+ const lists = (path, strategy) => strategyFor(reg.config, path).includes(strategy);
874
+ /** The locale `path` names where the URL decides the locale, else null. */
875
+ const named = (path) => lists(path, "url") ? urlLocale(reg.config, path) : null;
876
+ /**
877
+ * The locale the URL settles: `heading` once the URL names it, else the
878
+ * page's. A clicked link decides nothing before its commit: the router may
879
+ * still redirect, a blocker may stop it, or the browser downloads it.
880
+ */
881
+ const sure = () => heading && named(location.pathname) === heading ? heading : page.locale;
882
+ /** The current URL in `l` on the host serving it, when that is another listed host (§6 D135). */
883
+ const away = (l) => awayHref(splitLocalePath(location.pathname, reg.config).path, l, reg.config);
884
+ /**
885
+ * The current URL in locale `l`, path-absolute (what client routers accept),
886
+ * or absolute on another listed host.
887
+ */
888
+ const hrefIn = (l) => (ROUTES && away(l) || localizePath(location.pathname, l, reg.config)) + location.search + location.hash;
889
+ function persist(l) {
890
+ if (!(BUILD || browser())) return;
891
+ const { name, maxAge } = reg.config.routing.cookie;
892
+ setCookie(name, l, maxAge);
893
+ if (lists(location.pathname, "localStorage")) try {
894
+ localStorage.setItem(STORAGE_KEY, l);
895
+ } catch {}
896
+ }
897
+ /**
898
+ * Loads `href` as a new document. Assigning the current URL is a same-document
899
+ * fragment navigation when it has a `#hash` (no reload, so the locale would
900
+ * never change), hence a reload when only the fragment could differ.
901
+ *
902
+ * Nothing records that the page is leaving: the watcher sees commits only,
903
+ * so this navigation never reaches it, and a page that stays (restored from
904
+ * the back/forward cache, or a `beforeunload` prompt cancelled the unload)
905
+ * must keep switching on its next navigation. A switch started in the
906
+ * moments before the unload only loads what the next document loads again.
907
+ */
908
+ function go(href) {
909
+ const u = new URL(href, location.href);
910
+ u.hash = location.hash;
911
+ if (u.href === location.href) location.reload();
912
+ else location.assign(href);
913
+ }
914
+ /** A document navigation: nothing half-switched is ever shown (§6 D47). */
915
+ function leave(l) {
916
+ persist(l);
917
+ go(hrefIn(l));
918
+ }
919
+ /**
920
+ * Starts switch number `++switches`: loads `l` for every key the page uses.
921
+ * `url` is `l` for a URL-driven switch (it becomes `heading`), undefined for
922
+ * `setLocale`. Guards keep loading bundles while it waits (a link click
923
+ * pushes the URL, then the lazy route's guard runs): in the old locale under
924
+ * `setLocale`, in `l` under a URL-driven switch (`want`). Both are loaded
925
+ * until nothing is left, or the new locale would flip without that route's
926
+ * strings.
927
+ */
928
+ function prepare(l, url) {
929
+ const n = ++switches;
930
+ const from = loc(page.locale);
931
+ const to = loc(l);
932
+ heading = url;
933
+ return loading = (async () => {
934
+ let keys = [...from.k, ...from.p.keys()];
935
+ do {
936
+ await load(l, keys);
937
+ if (n !== switches) return false;
938
+ keys = [
939
+ ...from.k,
940
+ ...from.p.keys(),
941
+ ...to.p.keys()
942
+ ].filter((k) => !to.k.has(k) && own(to.h, k));
943
+ } while (keys.length);
944
+ return true;
945
+ })();
946
+ }
947
+ /** Flips everything at once, once `l` is loaded. */
948
+ function finish(l) {
949
+ heading = void 0;
950
+ flip(l);
951
+ persist(l);
952
+ changed(true);
953
+ for (const fn of subs) fn(l);
954
+ }
955
+ /**
956
+ * Cancels the pending switch: its loads complete, but it never flips.
957
+ * Guards that resolved in a URL-driven switch's locale let their routes
958
+ * render (a guard redirected back, the router went elsewhere): what that
959
+ * locale has and the page's lacks, which is what they loaded (a switch
960
+ * first loads every key the page has), loads in the page's locale, and the
961
+ * ids missed meanwhile re-render once it arrives.
962
+ */
963
+ function cancel() {
964
+ if (heading) {
965
+ const d = loc(heading);
966
+ load(page.locale, [...d.k, ...d.p.keys()]).catch(noop);
967
+ }
968
+ switches++;
969
+ heading = void 0;
970
+ }
971
+ /**
972
+ * `setLocale`: loads `l` for every key the page uses, flips, then puts `l`
973
+ * into the URL through the navigator.
974
+ */
975
+ async function switchTo(l) {
976
+ if (!S.locales.includes(l)) throw new RangeError("vi18n: unknown locale " + l);
977
+ const config = reg.config;
978
+ const url = (BUILD || browser()) && config.routing.url !== "no_prefix" && lists(location.pathname, "url");
979
+ if (config.routing.switch === "navigate" || url && !nav || ROUTES && url && away(l)) return leave(l);
980
+ const link = heading;
981
+ const kept = l === page.locale;
982
+ if (kept) {
983
+ cancel();
984
+ persist(l);
985
+ } else {
986
+ const p = prepare(l);
987
+ try {
988
+ if (!await p) return;
989
+ } catch (e) {
990
+ if (p !== loading) return;
991
+ if (e.skew) return leave(l);
992
+ throw e;
993
+ }
994
+ finish(l);
995
+ }
996
+ if (url && (link || (urlLocale(config, location.pathname) ?? (kept && l)) !== l)) nav(hrefIn(l));
997
+ }
998
+ /**
999
+ * The page is going to a URL in locale `l`: it committed (`commit`), or a
1000
+ * link to it was clicked. Routers that load the route before they push its
1001
+ * URL (vue-router, Solid's) run its guards in between, so the click already
1002
+ * starts loading, and the commit flips. Routers that push first render the
1003
+ * route after the commit: its guard waits for the switch (`want`). A route
1004
+ * whose strings were loaded before (or preloaded on hover, as Solid's
1005
+ * router does) runs no guard, and stays in the old locale under the new
1006
+ * URL until the switch flips (INTERNALS §2.11.7). The watcher and the click
1007
+ * listener start at a client registration, so `reg` is set.
1008
+ */
1009
+ function toward(l, commit) {
1010
+ if (l === null) return;
1011
+ if (l === page.locale) {
1012
+ if (heading) cancel();
1013
+ } else if (reg.config.routing.switch === "navigate") {
1014
+ if (commit) go(location.href);
1015
+ } else {
1016
+ if (heading !== l) prepare(l, l);
1017
+ loading.then((ok) => commit && ok && heading === l && finish(l), (e) => {
1018
+ if (heading === l) {
1019
+ cancel();
1020
+ if (e.skew && named(location.pathname) === l) leave(l);
1021
+ }
1022
+ if (DEV) warnOnce("switching to " + l + " failed: " + e);
1023
+ });
1024
+ }
1025
+ }
1026
+ /** `setLocale` where a server registration may reach this engine: it throws there. */
1027
+ function setLocale(l) {
1028
+ if (S.serverResolver || reg?.consumer === "server") return clientOnly(l);
1029
+ return switchTo(l);
1030
+ }
1031
+ /** A same-document navigation committed: `location` is the new URL. */
1032
+ function onNavigate() {
1033
+ if (BUILD || reg.mode === "build") try {
1034
+ adoptLate();
1035
+ } catch {}
1036
+ const p = location.pathname;
1037
+ if (p !== committed) toward(named(committed = p), true);
1038
+ }
1039
+ /**
1040
+ * A click on a same-origin link that a client router may follow (the
1041
+ * checks routers make), in the capture phase: before the router's handler,
1042
+ * whose route guards may start within the same task.
1043
+ */
1044
+ function onClick(e) {
1045
+ const a = e.composedPath().find((n) => n.href);
1046
+ if (a && !(e.button || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) && (!a.target || a.target === "_self") && !a.hasAttribute("download") && a.origin === location.origin) toward(named(a.pathname));
1047
+ }
1048
+ /** The bridge's message key: `{ __vi18n: 1 }` from a booted worker, `{ __vi18n: Told }` to it. */
1049
+ const BRIDGE = "__vi18n";
1050
+ /** While set, the worker holds its incoming messages; they replay in order once it settles. */
1051
+ let gate;
1052
+ const held = [];
1053
+ /** Settles the first answer of the creating realm (set while the worker waits for it). */
1054
+ let answer;
1055
+ const dataOf = (e) => e.data?.[BRIDGE];
1056
+ /**
1057
+ * Holds the worker's messages until `p` and every load started meanwhile
1058
+ * settled, so handlers run with the locale's strings, as a chunk guard holds
1059
+ * a route; also messages that came while a module's top-level `await` (a
1060
+ * guard) kept its `onmessage` unset, which browsers would otherwise drop.
1061
+ * A later hold takes over and replays everything held.
1062
+ */
1063
+ function hold(p) {
1064
+ const g = gate = p.then(noop, noop).then(async () => {
1065
+ while (inflight.size) await Promise.allSettled(inflight);
1066
+ });
1067
+ g.then(() => {
1068
+ if (gate !== g) return;
1069
+ gate = void 0;
1070
+ setTimeout(() => {
1071
+ if (gate) return;
1072
+ for (const e of held.splice(0)) {
1073
+ const copy = Object.assign(new MessageEvent("message", e), { vi18n: 1 });
1074
+ dispatchEvent(copy);
1075
+ }
1076
+ });
1077
+ });
1078
+ }
1079
+ /**
1080
+ * Switches the worker in place, what both the creating realm's flips and the
1081
+ * worker's own `setLocale` do: no URL, cookie or document to update.
1082
+ */
1083
+ function inPlace(l) {
1084
+ if (!S.locales.includes(l)) return Promise.reject(/* @__PURE__ */ new RangeError("vi18n: unknown locale " + l));
1085
+ if (l === page.locale) {
1086
+ cancel();
1087
+ return resolved;
1088
+ }
1089
+ const p = prepare(l).then((ok) => {
1090
+ if (ok) finish(l);
1091
+ });
1092
+ hold(p);
1093
+ return p;
1094
+ }
1095
+ /** A message from the creating realm: its locale now, with what it knows of it. */
1096
+ function told(d) {
1097
+ answer?.(d);
1098
+ answer = void 0;
1099
+ version ??= d.v;
1100
+ const x = loc(d.l);
1101
+ if (d.h && !x.r) {
1102
+ x.h = d.h;
1103
+ x.r = resolved;
1104
+ }
1105
+ inPlace(d.l).catch((e) => {
1106
+ if (DEV) warnOnce("switching the worker to " + d.l + " failed: " + e);
1107
+ });
1108
+ }
1109
+ /**
1110
+ * The engine's half of `register` in a module worker. A worker the bridge
1111
+ * created carries the creating realm's locale in its URL's fragment
1112
+ * (`#vi18n=<l>`), says hello once booted, and gets that realm's version and
1113
+ * manifest map back, then every switch. Without the fragment nothing created
1114
+ * it through the bridge, so nobody would answer, and it would hand an
1115
+ * unexpected message to the app's own handlers: it takes the browser's
1116
+ * languages and stays in that locale.
1117
+ */
1118
+ function worker(r) {
1119
+ const tag = /[#&]vi18n=([^&]*)/.exec(location.hash)?.[1];
1120
+ let l = tag === void 0 ? "" : decodeURIComponent(tag);
1121
+ const bridged = S.locales.includes(l);
1122
+ if (!bridged) l = matchLocale(globalThis.navigator?.languages ?? [], r.config) ?? r.config.sourceLocale;
1123
+ flip(l);
1124
+ unwrap = wrapFetch(localeHeader);
1125
+ S.setLocale = inPlace;
1126
+ addEventListener("message", (e) => {
1127
+ const d = dataOf(e);
1128
+ if (d && typeof d === "object") {
1129
+ e.stopImmediatePropagation();
1130
+ told(d);
1131
+ } else if (gate && !e.vi18n) {
1132
+ e.stopImmediatePropagation();
1133
+ held.push(e);
1134
+ }
1135
+ });
1136
+ let reply = Promise.resolve(void 0);
1137
+ if (bridged) {
1138
+ reply = new Promise((settle) => answer = settle);
1139
+ postMessage({ [BRIDGE]: 1 });
1140
+ }
1141
+ if (r.mode === "dev") {
1142
+ const p = inFlight(devCatalog(l));
1143
+ hold(Promise.all([p, reply]));
1144
+ return p;
1145
+ }
1146
+ const d = loc(l);
1147
+ const m = reply.then((t) => {
1148
+ version ??= t?.v;
1149
+ return t?.l === l && t.h ? t.h : acquire(l);
1150
+ }).then((h) => d.h = h, (e) => {
1151
+ d.r = void 0;
1152
+ throw e;
1153
+ });
1154
+ d.r = m;
1155
+ inFlight(m).catch(noop);
1156
+ hold(reply);
1157
+ return resolved;
1158
+ }
1159
+ /**
1160
+ * The creating side of the bridge: `virtual:vi18n/bridge` calls it in every
1161
+ * realm whose code constructs a worker (a page, or a worker that starts its
1162
+ * own). Each `new Worker(url)` gets the realm's locale as a URL fragment,
1163
+ * which leaves the request alone; a worker that answers with a hello is told
1164
+ * the version and map, then every locale this realm flips to. Workers that
1165
+ * never say hello (no vi18n in them, or `type: "classic"` scripts the
1166
+ * fragment does not reach) never hear from it.
1167
+ */
1168
+ function bridge() {
1169
+ const W = globalThis.Worker;
1170
+ if (!created || !W || W.vi18n) return;
1171
+ const workers = /* @__PURE__ */ new Set();
1172
+ const tell = (w) => {
1173
+ const told = {
1174
+ l: page.locale,
1175
+ v: version,
1176
+ h: locs.get(page.locale)?.h
1177
+ };
1178
+ w.postMessage({ [BRIDGE]: told });
1179
+ };
1180
+ globalThis.Worker = class extends W {
1181
+ static vi18n = 1;
1182
+ constructor(url, options) {
1183
+ const u = typeof url === "string" || url instanceof URL ? String(url) : null;
1184
+ super(u === null ? url : u + (u.includes("#") ? "&" : "#") + "vi18n=" + encodeURIComponent(page.locale), options);
1185
+ this.addEventListener("message", (e) => {
1186
+ if (dataOf(e) === 1) {
1187
+ e.stopImmediatePropagation();
1188
+ workers.add(this);
1189
+ tell(this);
1190
+ }
1191
+ });
1192
+ }
1193
+ terminate() {
1194
+ workers.delete(this);
1195
+ super.terminate();
1196
+ }
1197
+ };
1198
+ subs.add(() => {
1199
+ for (const w of workers) tell(w);
1200
+ });
1201
+ }
1202
+ /**
1203
+ * `x-vi18n-locale` for same-origin requests, per `routing.propagate`: the
1204
+ * switch's locale once the URL names it, so the router's requests for the
1205
+ * new page carry it before the flip, else the page's (`sure`, §6 D108).
1206
+ */
1207
+ const localeHeader = () => reg?.config.routing.propagate === "header" && (BUILD || !reg.test) ? sure() : null;
1208
+ /** Hooks into `register` and replaces the core's client stubs with this engine's methods. */
1209
+ function install() {
1210
+ if (BUILD || browser()) unwrap = wrapFetch(localeHeader);
1211
+ setEngine(boot);
1212
+ const methods = {
1213
+ need,
1214
+ lazy: (importer, keys) => Promise.all([importer(), need(keys)]).then((r) => r[0]),
1215
+ prefetch: (keys) => void want(keys).catch(noop),
1216
+ async ready() {
1217
+ if (BUILD || reg?.mode === "build") load(page.locale, ["base"]).catch(noop);
1218
+ while (inflight.size) await Promise.allSettled(inflight);
1219
+ },
1220
+ setLocale: BUILD ? switchTo : setLocale,
1221
+ missing(scope, key, reason) {
1222
+ if (scope === page) {
1223
+ missed.add(key);
1224
+ if (reason === "unknown-id" && inflight.size) reason = "not-loaded";
1225
+ }
1226
+ report(key, scope.locale, reason);
1227
+ }
1228
+ };
1229
+ if (INLINE) methods.add = (b) => merge(page.locale, [b]);
1230
+ if (DEV) methods.replace = (l, catalog) => {
1231
+ if (!S.locales.includes(l)) return;
1232
+ setDev(l, catalog);
1233
+ changed(false);
1234
+ };
1235
+ Object.assign(S, methods);
1236
+ if (!BUILD && S.registration) boot(S.registration);
1237
+ }
1238
+ if (created) install();
1239
+ else if (DEV && S.need === S.ready) warnOnce("another copy of @menapo/vi18n without the client engine created the page state first, so this boot module's \"@menapo/vi18n/engine\" cannot install and t() renders no translations; dedupe the package");
1240
+ //#endregion
1241
+ export { bridge, inContext };