@menapo/vi18n 0.0.0-stage → 0.1.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/LICENSE +49 -0
- package/README.md +110 -2
- package/dist/apply-Bre-lY9J.js +228 -0
- package/dist/captured-Bj8bO6vd.js +1034 -0
- package/dist/check-D34P-S--.js +1 -0
- package/dist/check-DyrAbv73.js +201 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +41 -0
- package/dist/codec-DuvU2VQy.js +207 -0
- package/dist/collect-Cx4_SKOe.js +13 -0
- package/dist/collisions-CfpZU8e4.js +217 -0
- package/dist/compile-B5Mz6WJm.js +245 -0
- package/dist/compile-Cc97JaW2.js +2502 -0
- package/dist/concurrency-C9UUWuDC.js +96 -0
- package/dist/concurrency-DONL7t4V.js +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +39 -0
- package/dist/dir-zw6bGMKQ.js +290 -0
- package/dist/duplicates-yuRljQ6h.js +185 -0
- package/dist/engine-B7p40M__.js +531 -0
- package/dist/engine.d.ts +43 -0
- package/dist/engine.js +1241 -0
- package/dist/exclude-Du-Zd_qI.js +26 -0
- package/dist/export-DGsMbcTZ.js +103 -0
- package/dist/extract-Dujlsgnk.js +298 -0
- package/dist/formats-KPIg-08V.js +781 -0
- package/dist/i18next-CrUnKFfh.js +1596 -0
- package/dist/i18next-Rgo-v9l0.js +1 -0
- package/dist/import-Cwdhcywt.js +225 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +239 -0
- package/dist/init-BecCTvKi.js +792 -0
- package/dist/library-D5CWvTQs.js +1882 -0
- package/dist/lingui--PcOOYry.js +833 -0
- package/dist/lint.d.ts +105 -0
- package/dist/lint.js +971 -0
- package/dist/load-CQRz4PpM.js +2950 -0
- package/dist/load-gW-oJt1Q.js +747 -0
- package/dist/locales-DX4MOBFE.js +827 -0
- package/dist/lsp-Cq1Nuu-Q.js +31 -0
- package/dist/match-BGr8EXB3.js +89 -0
- package/dist/mcp-Cf0T7qPW.js +28 -0
- package/dist/merge-driver-CpiiSVIc.js +422 -0
- package/dist/messages-Cxa-5tR0.js +166 -0
- package/dist/migrate-CntqvrW4.js +1 -0
- package/dist/migrate-dPgq9uzy.js +1484 -0
- package/dist/openai-CTaMDcl-.js +346 -0
- package/dist/output-DsaHKHEt.js +164 -0
- package/dist/parse.d.ts +62 -0
- package/dist/parse.js +156 -0
- package/dist/pipeline-C7g11THL.js +2031 -0
- package/dist/pipeline-wxS0SApG.js +1 -0
- package/dist/project-Dwx7iPm6.js +381 -0
- package/dist/project-pXipT8iG.js +1046 -0
- package/dist/pseudo-DRv30iN5.js +145 -0
- package/dist/raw-C4Y6vyGY.js +4708 -0
- package/dist/react-intl-CaBVwJ9u.js +519 -0
- package/dist/restore-Btq66Tid.js +175 -0
- package/dist/rewrite-CiTRAICl.js +803 -0
- package/dist/rich-DCUKl5HM.js +97 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/run-DYD_VXKy.js +694 -0
- package/dist/runtime-DTMmliT1.js +64 -0
- package/dist/scan-worker.d.ts +62 -0
- package/dist/scan-worker.js +1 -0
- package/dist/scoped-BMYpSthj.js +44 -0
- package/dist/server-BDkykMjb.js +11823 -0
- package/dist/server-qv3mz70d.js +10485 -0
- package/dist/server.d.ts +169 -0
- package/dist/server.js +1234 -0
- package/dist/sitemap-DV3o8QD7.js +159 -0
- package/dist/sources-D19czDSA.js +2242 -0
- package/dist/status-DZArW5aE.js +149 -0
- package/dist/stdio-CVaJfxA7.js +29 -0
- package/dist/stdio-FPOKQAgu.js +95 -0
- package/dist/sync-CcVzger_.js +658 -0
- package/dist/t-DC-c9zE5.js +454 -0
- package/dist/test.d.ts +116 -0
- package/dist/test.js +319 -0
- package/dist/translate-DZR3MkSA.js +299 -0
- package/dist/ts-BaU5yOf0.js +4863 -0
- package/dist/types-CgUCkFU1.d.ts +707 -0
- package/dist/types-D3QGM8XO.js +146 -0
- package/dist/types-DsdWBG0b.d.ts +558 -0
- package/dist/types-DtvQ7UdD.js +1948 -0
- package/dist/types-wSh9fCBY.js +11 -0
- package/dist/units-CQ5Q99RC.js +150 -0
- package/dist/url-CRfCOhUI.js +172 -0
- package/dist/url-CsBkRaJy.js +225 -0
- package/dist/validate-BxpupdAf.js +2615 -0
- package/dist/validate-D6DJepZv.js +365 -0
- package/dist/version-BlyLGlOa.js +28 -0
- package/dist/vite-scan-B_YW4X_a.js +242 -0
- package/dist/vite.d.ts +34 -0
- package/dist/vite.js +7558 -0
- package/dist/vue-i18n-CEMnpv7k.js +651 -0
- package/dist/which-C6PFI1Dm.js +116 -0
- package/dist/worker-84U6uYB3.js +1656 -0
- package/package.json +97 -3
- 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 };
|