@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/server.js ADDED
@@ -0,0 +1,1234 @@
1
+ import { a as getState, h as createScope, t as textDirection } from "./dir-zw6bGMKQ.js";
2
+ import { a as hostLocale, c as localizedPathsFor, d as splitLocalePath, i as awayHref, l as matchRoute, n as atHome, o as localeOrigin, r as atHost, s as localizePath } from "./url-CsBkRaJy.js";
3
+ import { r as matchLocale } from "./match-BGr8EXB3.js";
4
+ import { t as createScopedApi } from "./scoped-BMYpSthj.js";
5
+ import { AsyncLocalStorage } from "node:async_hooks";
6
+ //#region src/server/catalogs.ts
7
+ /**
8
+ * Server registration and catalogs (INTERNALS §3.2 "Server registration").
9
+ * The boot module (or `registerCatalogs`) registers once; from then on this
10
+ * module owns what `/server` loads for it: the per-locale catalogs, the
11
+ * `sources` map and the client plan. The source catalog and `sources` start
12
+ * loading at registration, so `State.serverFallback` exists before the first
13
+ * request and untransformed code resolves source literals from the start.
14
+ *
15
+ * Module-level variables rather than one state object: the minifier renames
16
+ * them, which keeps `/server` inside its PLAN §6 budget.
17
+ */
18
+ const S = getState();
19
+ /**
20
+ * Dev mode from the plugin's `define`, read the way the runtime reads it:
21
+ * code bundled without the plugin behaves as production.
22
+ */
23
+ const DEV = typeof __VI18N_DEV__ !== "undefined" && !!__VI18N_DEV__;
24
+ /**
25
+ * `ROUTES` of the URL codec, read here again: the packed `/server` imports
26
+ * the runtime chunk, where a constant cannot fold this entry's branches out
27
+ * (§6 D134).
28
+ */
29
+ const ROUTES = typeof __VI18N_ROUTES__ === "undefined" || !!__VI18N_ROUTES__;
30
+ const own = Object.hasOwn;
31
+ const noop = () => {};
32
+ const error = (text) => /* @__PURE__ */ new Error("vi18n: " + text);
33
+ const warned = /* @__PURE__ */ new Set();
34
+ function warnOnce(text) {
35
+ if (warned.has(text)) return;
36
+ if (warned.size > 499) warned.clear();
37
+ warned.add(text);
38
+ console.warn("vi18n: " + text);
39
+ }
40
+ const zones = /* @__PURE__ */ new Set();
41
+ /**
42
+ * True for an IANA zone name this engine's Intl accepts (INTERNALS §3.2).
43
+ * Names start with a letter. UTC offsets (`+01:00`, `-0530`, `+01`), which
44
+ * Intl accepts too, are thousands of spellings: a client cycling them
45
+ * through `vi18n_tz` would add a formatter per spelling to every cache
46
+ * keyed by zone.
47
+ */
48
+ function isTimeZone(zone) {
49
+ if (typeof zone !== "string" || zone.length > 64 || !/^[a-z]/i.test(zone)) return false;
50
+ if (zones.has(zone)) return true;
51
+ try {
52
+ new Intl.DateTimeFormat("en", { timeZone: zone });
53
+ } catch {
54
+ return false;
55
+ }
56
+ if (zones.size < 1024) zones.add(zone);
57
+ return true;
58
+ }
59
+ /** The registration served; replaced when the state registers again. */
60
+ let reg = null;
61
+ let config;
62
+ let loaders;
63
+ /** Dev catalogs (`registration.catalogs`) are re-imported per scope, so HMR invalidation shows. */
64
+ let dev = false;
65
+ /**
66
+ * Build catalogs, settled or in flight (concurrent scopes share one load; a
67
+ * failed load is dropped so the next request retries), least recently used
68
+ * first.
69
+ */
70
+ let loaded = /* @__PURE__ */ new Map();
71
+ /** `sources()` as a Map, once settled; every scope `/server` opens carries it. */
72
+ let sources;
73
+ /** Source catalog and `sources` both settled: scopes may open without waiting. */
74
+ let settled = false;
75
+ let readying;
76
+ /** The client plan: undefined until loaded, null without one (sentinel, dev, `registerCatalogs`). */
77
+ let plan;
78
+ let planning;
79
+ /**
80
+ * The config of the current registration, (re)starting this module when the
81
+ * state registered again. Throws before any registration: there is nothing
82
+ * to negotiate with, and a guess would render the wrong locale.
83
+ */
84
+ function current() {
85
+ const r = S.registration;
86
+ if (!r) throw error(DEV ? "no registration yet: the Vite plugin's boot module registers on import, and servers built without it call registerCatalogs() first" : "no registration (registerCatalogs)");
87
+ if (r !== reg) {
88
+ reg = r;
89
+ config = r.config;
90
+ loaders = r.serverCatalogs ?? r.catalogs;
91
+ dev = DEV && !r.serverCatalogs && !!r.catalogs;
92
+ loaded = /* @__PURE__ */ new Map();
93
+ sources = void 0;
94
+ settled = false;
95
+ readying = void 0;
96
+ plan = r.clientPlan ? void 0 : null;
97
+ planning = void 0;
98
+ ready()?.catch(noop);
99
+ }
100
+ return config;
101
+ }
102
+ /** Resolves once the source catalog and `sources` are loaded; undefined when they already are. */
103
+ function ready() {
104
+ if (settled) return void 0;
105
+ const r = reg;
106
+ return readying ??= Promise.all([catalog(config.sourceLocale), loadSources(r)]).then(([c, map]) => {
107
+ if (r !== reg) return;
108
+ sources = map;
109
+ settled = true;
110
+ S.serverFallback = createScope("request", config.sourceLocale, c.messages, {
111
+ timeZone: config.ssr.timeZone,
112
+ timeZoneSource: "config",
113
+ sources: map
114
+ });
115
+ }, (e) => {
116
+ if (r === reg) readying = void 0;
117
+ throw e;
118
+ });
119
+ }
120
+ const isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
121
+ /** A module namespace's default export, or the object itself (both are accepted everywhere). */
122
+ function unwrap(m) {
123
+ return isObject(m) && own(m, "default") && isObject(m.default) ? m.default : m;
124
+ }
125
+ async function loadSources(r) {
126
+ if (!r.sources) return void 0;
127
+ const data = unwrap(await r.sources());
128
+ if (!isObject(data)) throw error("sources() did not return an object");
129
+ const map = /* @__PURE__ */ new Map();
130
+ for (const k in data) if (own(data, k) && typeof data[k] === "string") map.set(k, data[k]);
131
+ return map;
132
+ }
133
+ const entries = (v) => new Map(Object.entries(isObject(v) ? v : {}));
134
+ function toCatalog(locale, m) {
135
+ const data = unwrap(m);
136
+ if (!isObject(data)) throw error("the " + locale + " catalog is not an object");
137
+ if (!(DEV && dev)) return { messages: entries(data) };
138
+ const d = data;
139
+ return {
140
+ messages: entries(d.messages),
141
+ labels: entries(d.labels)
142
+ };
143
+ }
144
+ const devMaps = /* @__PURE__ */ new WeakMap();
145
+ /** The catalog of a configured locale: a value when loaded (build), else a promise. */
146
+ function catalog(locale) {
147
+ const into = loaded;
148
+ let c = into.get(locale);
149
+ if (!c) {
150
+ if (!loaders || !own(loaders, locale)) return Promise.reject(error("no server catalog for locale " + JSON.stringify(locale)));
151
+ if (DEV && dev) return loaders[locale]().then((m) => {
152
+ if (!isObject(m)) return toCatalog(locale, m);
153
+ let d = devMaps.get(m);
154
+ if (!d) devMaps.set(m, d = toCatalog(locale, m));
155
+ return d;
156
+ });
157
+ c = loaders[locale]().then((m) => {
158
+ const k = toCatalog(locale, m);
159
+ into.set(locale, k);
160
+ return k;
161
+ }, (e) => {
162
+ into.delete(locale);
163
+ throw e;
164
+ });
165
+ }
166
+ into.delete(locale);
167
+ into.set(locale, c);
168
+ for (const l of into.keys()) if (into.size > config.ssr.maxLoadedLocales && l !== config.sourceLocale) into.delete(l);
169
+ return c;
170
+ }
171
+ /** Loads the client plan once; the sentinel string (or anything malformed) means fallback mode. */
172
+ function loadPlan() {
173
+ if (plan !== void 0) return void 0;
174
+ const r = reg;
175
+ return planning ??= r.clientPlan().then((m) => {
176
+ if (r !== reg) return;
177
+ const p = m.default;
178
+ plan = typeof p === "object" && p ? p : null;
179
+ }, (e) => {
180
+ if (r === reg) planning = void 0;
181
+ throw e;
182
+ });
183
+ }
184
+ const STRATEGIES = [
185
+ "url",
186
+ "cookie",
187
+ "header",
188
+ "localStorage",
189
+ "default"
190
+ ];
191
+ const RULES = [
192
+ ["locales", []],
193
+ ["pseudoLocales", []],
194
+ ["fallbacks", { values: [] }],
195
+ ["routing", {}],
196
+ ["routing.strategy", [STRATEGIES]],
197
+ ["routing.routeStrategies", { values: [STRATEGIES] }],
198
+ ["routing.url", [
199
+ "prefix_except_default",
200
+ "prefix",
201
+ "prefix_and_default",
202
+ "no_prefix"
203
+ ]],
204
+ ["routing.redirectRoot", "boolean"],
205
+ ["routing.propagate", ["header", false]],
206
+ ["routing.switch", ["in-place", "navigate"]],
207
+ ["routing.cookie", {}],
208
+ ["routing.cookie.name", "string"],
209
+ ["routing.cookie.maxAge", "number"],
210
+ ["routing.base", "string"],
211
+ ["ssr", {}],
212
+ ["ssr.timeZone", "string"],
213
+ ["ssr.timeZoneFrom", { values: ["config", "request"] }],
214
+ ["ssr.normalizeSpaces", "boolean"]
215
+ ];
216
+ function matches(v, rule) {
217
+ if (typeof rule === "string") return typeof v === rule;
218
+ if (Array.isArray(rule)) {
219
+ if (!rule.length || Array.isArray(rule[0])) {
220
+ const list = rule[0];
221
+ return Array.isArray(v) && v.every((x) => list ? list.includes(x) : typeof x === "string");
222
+ }
223
+ return rule.includes(v);
224
+ }
225
+ return isObject(v) && (!rule.values || Object.values(v).every((x) => matches(x, rule.values)));
226
+ }
227
+ /**
228
+ * The last registration `registerCatalogs()` made: a server built without
229
+ * the Vite plugin, so no client plan exists and no client reads a payload.
230
+ */
231
+ let bare;
232
+ /**
233
+ * `config.json` from `vi18n compile --out` arrives as untyped JSON: each
234
+ * field the server reads is checked, so a stale or hand-edited file fails at
235
+ * startup with the field's name instead of mid-request.
236
+ */
237
+ function validateConfig(input) {
238
+ const c = unwrap(input);
239
+ const fail = (path) => {
240
+ throw new TypeError("vi18n: registerCatalogs: invalid config: " + path);
241
+ };
242
+ if (!isObject(c)) fail("not an object");
243
+ for (const [path, rule] of RULES) if (!matches(path.split(".").reduce((o, k) => o[k], c), rule)) fail(path);
244
+ if (!c.locales.length) fail("locales");
245
+ if (!c.locales.includes(c.sourceLocale)) fail("sourceLocale");
246
+ if (ROUTES) {
247
+ const { paths, domains } = c.routing;
248
+ if (paths !== void 0 && !matches(paths, { values: { values: "string" } })) fail("routing.paths");
249
+ if (domains !== void 0 && !matches(domains, { values: "string" })) fail("routing.domains");
250
+ }
251
+ if (!isTimeZone(c.ssr.timeZone)) fail("ssr.timeZone");
252
+ return c;
253
+ }
254
+ function registerCatalogs(loaders, options) {
255
+ const config = validateConfig(options.config);
256
+ if (!isObject(loaders)) throw new TypeError("vi18n: registerCatalogs: loaders must be an object");
257
+ S.register(bare = {
258
+ mode: "build",
259
+ consumer: "server",
260
+ config,
261
+ serverCatalogs: loaders,
262
+ sources: options.sources
263
+ });
264
+ current();
265
+ }
266
+ const registerBefore = S.register;
267
+ S.register = (r) => {
268
+ const done = registerBefore(r);
269
+ if (r.consumer === "server" && S.registration === r) current();
270
+ return done;
271
+ };
272
+ //#endregion
273
+ //#region src/server/als.ts
274
+ /**
275
+ * Request isolation (PLAN §5.13 "Isolation"). Scopes travel in an
276
+ * AsyncLocalStorage; there is deliberately no synchronous-slot fallback,
277
+ * because a slot still readable after an `await` would serve one request's
278
+ * locale to another.
279
+ */
280
+ const als = S.serverScopes ??= new AsyncLocalStorage();
281
+ /** Framework request stores (SvelteKit `getRequestEvent()`) supply the scope; null removes it. */
282
+ function setScopeResolver(resolve) {
283
+ als.resolver = resolve;
284
+ }
285
+ /**
286
+ * The current scope. A `withLocale`/`createTranslator` scope entered inside
287
+ * a request outranks the framework's request store: the store would still
288
+ * name the request, and an e-mail rendered for another user inside it must
289
+ * use that user's locale. Otherwise the resolver wins over the ALS, as
290
+ * frameworks may run render code outside the async context `handle` opened.
291
+ */
292
+ function getScope() {
293
+ const s = als.getStore();
294
+ const user = als.resolver;
295
+ if (s?.kind === "translator" || !user) return s;
296
+ let u;
297
+ try {
298
+ u = user();
299
+ } catch {}
300
+ return u ?? s;
301
+ }
302
+ S.serverResolver = getScope;
303
+ /**
304
+ * Some ALS implementations resolve but do not propagate (polyfills that hold
305
+ * the store only synchronously): reading another request's locale later
306
+ * would be silent, so the first scope opener checks that a store survives
307
+ * an `await`, and fails loudly otherwise.
308
+ */
309
+ async function assertPropagation(store) {
310
+ const k = {};
311
+ if (!await store.run(k, async () => {
312
+ await Promise.resolve();
313
+ return store.getStore() === k;
314
+ })) throw error("AsyncLocalStorage loses its store across await; enable nodejs_als or nodejs_compat (compatibility date 2026-08-04+)");
315
+ }
316
+ let probed;
317
+ let ok = false;
318
+ /** Runs the probe once per process; a failure is remembered, so every request reports it. */
319
+ function propagation() {
320
+ if (ok) return void 0;
321
+ return probed ??= assertPropagation(als).then(() => {
322
+ ok = true;
323
+ });
324
+ }
325
+ //#endregion
326
+ //#region src/server/inject.ts
327
+ /**
328
+ * The streaming HTML transform of `handle()` (PLAN §5.13 step 5): `lang` and
329
+ * `dir` on `<html>` unless the app set them, the payload before `</head>`,
330
+ * a late payload before `</body>` for ids recorded after the head was
331
+ * flushed. It works on bytes, not text: the needles are ASCII, so chunks
332
+ * without a match pass through as the same buffers, with no decode/encode
333
+ * round trip. The inserted markup is UTF-8 (payload text included), so pages
334
+ * must be UTF-8 too; `handle()` warns in dev about any other declared charset.
335
+ */
336
+ const enc = new TextEncoder();
337
+ /**
338
+ * Tag names; `tagAt` returns their index (literals below, which the minifier
339
+ * keeps short). 4–5 start raw text and 6 a comment, skipped whole: a
340
+ * `</head>` inside an inline script's string literal does not end the head.
341
+ * `<title>` and `<textarea>` are left out: their text is escaped (`&lt;`)
342
+ * by every renderer, unlike script and style source.
343
+ */
344
+ const TAGS = [
345
+ "<html",
346
+ "</head",
347
+ "<body",
348
+ "</body",
349
+ "<script",
350
+ "<style",
351
+ "<!--"
352
+ ];
353
+ /** A `<html` start tag longer than this is not one we can edit; it passes through. */
354
+ const MAX_TAG = 8192;
355
+ /**
356
+ * Which of `names` starts at `at`, -1 for none, -2 when `buf` ends first. A
357
+ * tag name must be followed by a delimiter; `<!--` and `-->` need none.
358
+ */
359
+ function tagAt(buf, at, names) {
360
+ let undecided = false;
361
+ for (let t = 0; t < names.length; t++) {
362
+ const name = names[t];
363
+ let k = 0;
364
+ for (; k < name.length && at + k < buf.length; k++) {
365
+ const b = buf[at + k];
366
+ if ((b > 64 && b < 91 ? b | 32 : b) !== name.charCodeAt(k)) break;
367
+ }
368
+ const d = buf[at + k];
369
+ if (k === name.length && (name.length < 5 || d <= 32 || d === 62 || d === 47)) return t;
370
+ if (d === void 0) undecided = true;
371
+ }
372
+ return undecided ? -2 : -1;
373
+ }
374
+ /** Index of the `>` closing the tag that starts at `from`, honouring quoted values; -1 if not in `buf`. */
375
+ function tagEnd(buf, from) {
376
+ let quote = 0;
377
+ for (let i = from; i < buf.length; i++) {
378
+ const b = buf[i];
379
+ if (quote) {
380
+ if (b === quote) quote = 0;
381
+ } else if (b === 34 || b === 39) quote = b;
382
+ else if (b === 62) return i;
383
+ }
384
+ return -1;
385
+ }
386
+ /** The attributes to add to an `<html …>` tag (its text without `<html` and `>`). */
387
+ function missingAttributes(tag, lang, dir) {
388
+ const names = (" " + tag.replace(/"[^"]*"|'[^']*'/g, "")).toLowerCase();
389
+ return (/[\s/]lang(?![^\s=/>])/.test(names) ? "" : ` lang="${lang}"`) + (/[\s/]dir(?![^\s=/>])/.test(names) ? "" : ` dir="${dir}"`);
390
+ }
391
+ /**
392
+ * The transform. Phases: 0 looks for `<html`, 1 for `</head` (`<body` ends
393
+ * both), 2 for `</body`, 3 passes everything through. Before phase 3,
394
+ * comments and raw-text elements are skipped whole, as a parser would. A
395
+ * chunk that ends inside a candidate tag keeps that tail back (at most
396
+ * `MAX_TAG` bytes) and joins it to the next chunk.
397
+ */
398
+ function injector(hooks) {
399
+ let phase = 0;
400
+ let carry = null;
401
+ /** The end tag (`<\/script`…) or `-->` ending the raw text being skipped; "" outside it. */
402
+ let raw = "";
403
+ return {
404
+ transform(chunk, controller) {
405
+ let buf = chunk;
406
+ if (carry) {
407
+ buf = new Uint8Array(carry.length + chunk.length);
408
+ buf.set(carry);
409
+ buf.set(chunk, carry.length);
410
+ carry = null;
411
+ }
412
+ let pos = 0;
413
+ const emit = (end) => {
414
+ if (end > pos) controller.enqueue(buf.subarray(pos, end));
415
+ pos = end;
416
+ };
417
+ const hold = (at) => {
418
+ emit(at);
419
+ carry = buf.slice(at);
420
+ };
421
+ const insert = (text) => {
422
+ if (text) controller.enqueue(enc.encode(text));
423
+ };
424
+ let i = 0;
425
+ while (phase < 3) {
426
+ const lt = buf.indexOf(raw ? raw.charCodeAt(0) : 60, i);
427
+ if (lt < 0) break;
428
+ i = lt + 1;
429
+ const t = tagAt(buf, lt, raw ? [raw] : TAGS);
430
+ if (t === -2) return hold(lt);
431
+ if (raw) {
432
+ if (!t) raw = "";
433
+ } else if (t > 3) raw = t > 5 ? "-->" : "</" + TAGS[t].slice(1);
434
+ else if (t === 0 && !phase) {
435
+ const end = tagEnd(buf, lt + 5);
436
+ if (end >= 0) {
437
+ const { lang, dir } = hooks.attributes();
438
+ emit(buf[end - 1] === 47 ? end - 1 : end);
439
+ insert(missingAttributes(new TextDecoder().decode(buf.subarray(lt + 5, end)), lang, dir));
440
+ i = end + 1;
441
+ } else if (buf.length - lt <= MAX_TAG) return hold(lt);
442
+ phase = 1;
443
+ } else if (t === 1 && phase < 2) {
444
+ emit(lt);
445
+ insert(hooks.head());
446
+ phase = 2;
447
+ } else if (t === 2) {
448
+ if (phase < 2) phase = 2;
449
+ } else if (t === 3) {
450
+ emit(lt);
451
+ insert(hooks.tail());
452
+ phase = 3;
453
+ }
454
+ }
455
+ emit(buf.length);
456
+ },
457
+ flush(controller) {
458
+ if (carry) controller.enqueue(carry);
459
+ }
460
+ };
461
+ }
462
+ //#endregion
463
+ //#region src/server/types.ts
464
+ /** Header the client runtime adds to same-origin `fetch` calls (`routing.propagate`). */
465
+ const LOCALE_HEADER = "x-vi18n-locale";
466
+ /** Cookie the client runtime writes with the browser's IANA zone. */
467
+ const TIME_ZONE_COOKIE = "vi18n_tz";
468
+ //#endregion
469
+ //#region src/server/negotiate.ts
470
+ /**
471
+ * Server locale resolution (INTERNALS §2.11.9, PLAN §5.14). The SPA boot
472
+ * script and the client runtime implement the same algorithm, and
473
+ * `tests/fixtures/resolution/cases.json` keeps the three equal. Every input
474
+ * (URL segment, header, cookie, Referer) is matched against `locales` and
475
+ * nothing else, so a request can only ever select a configured locale.
476
+ */
477
+ /** The base as a path starting and ending with `/`, as the URL codec reads it. */
478
+ function baseOf(config) {
479
+ const b = config.routing.base;
480
+ return b && b[0] === "/" ? b.endsWith("/") ? b : b + "/" : "/";
481
+ }
482
+ /**
483
+ * The pathname as route globs see it (INTERNALS §2.7.3): delocalized,
484
+ * without the base and without a trailing slash; the root is `/`.
485
+ */
486
+ function routeOf(pathname, config) {
487
+ const p = splitLocalePath(pathname, config).path;
488
+ const base = baseOf(config);
489
+ const s = p.startsWith(base) ? "/" + p.slice(base.length) : p + "/" === base ? "/" : p;
490
+ return s.length > 1 && s.endsWith("/") ? s.slice(0, -1) : s;
491
+ }
492
+ const keyCache = /* @__PURE__ */ new WeakMap();
493
+ function keysOf(o) {
494
+ let k = keyCache.get(o);
495
+ if (!k) keyCache.set(o, k = Object.keys(o));
496
+ return k;
497
+ }
498
+ /**
499
+ * The `url` step: an unprefixed path is the source locale's canonical page
500
+ * (on a listed host, the host's locale's) unless every locale is prefixed.
501
+ */
502
+ function urlStep(config, pathname) {
503
+ const url = config.routing.url;
504
+ if (url === "no_prefix") return null;
505
+ return splitLocalePath(pathname, config).locale ?? (url === "prefix" ? null : ROUTES && config.routing.home || config.sourceLocale);
506
+ }
507
+ /**
508
+ * The canonical URL of a page the URL names, where localized paths or
509
+ * domains are configured (§6 D133), or null when `pathname` is it: the
510
+ * locale's own path for the route (`/de/about` → `/de/ueber-uns`), without
511
+ * the prefix of the unprefixed locale, on the host serving the locale (an
512
+ * absolute URL, from the config only). Under `prefix_and_default` the
513
+ * requested prefix form stays. A target that would not read back as the
514
+ * same route is never answered, so no config can loop.
515
+ */
516
+ function canonical(pathname, config) {
517
+ const url = config.routing.url;
518
+ if (url === "no_prefix") return null;
519
+ const split = splitLocalePath(pathname, config);
520
+ const l = split.locale ?? (url === "prefix" ? null : config.routing.home ?? config.sourceLocale);
521
+ if (l === null) return null;
522
+ const away = awayHref(split.path, l, config);
523
+ if (away !== null) return away;
524
+ const c = url === "prefix_and_default" && split.locale !== null ? {
525
+ ...config,
526
+ routing: {
527
+ ...config.routing,
528
+ url: "prefix"
529
+ }
530
+ } : config;
531
+ const target = localizePath(split.path, l, c);
532
+ return target !== pathname && splitLocalePath(target, c).path === split.path ? target : null;
533
+ }
534
+ /** One cookie's decoded value from a `Cookie` header, or null. */
535
+ function readCookie(header, name) {
536
+ for (const part of header ? header.split(";") : []) {
537
+ const eq = part.indexOf("=");
538
+ if (eq > 0 && part.slice(0, eq).trim() === name) try {
539
+ return decodeURIComponent(part.slice(eq + 1).trim());
540
+ } catch {
541
+ return null;
542
+ }
543
+ }
544
+ return null;
545
+ }
546
+ const MAX_HEADER = 1024;
547
+ const MAX_ENTRIES = 20;
548
+ const QVALUE = /^\s*q\s*=\s*(0(?:\.\d{0,3})?|1(?:\.0{0,3})?)\s*$/i;
549
+ /**
550
+ * Accept-Language as language tags by preference. Hardened for hostile
551
+ * input: at most 1024 characters and 20 entries are read (an entry cut by
552
+ * the length cap is dropped), `q=0` means refused, a missing q is 1, a
553
+ * malformed q drops its entry, and equal q keeps header order (stable sort).
554
+ * Tags are not validated here; `matchLocale` canonicalises them in try/catch.
555
+ */
556
+ function parseAcceptLanguage(header) {
557
+ if (!header) return [];
558
+ const cut = header.length > MAX_HEADER;
559
+ const parts = (cut ? header.slice(0, MAX_HEADER) : header).split(",", 21);
560
+ if (cut && parts.length <= MAX_ENTRIES) parts.pop();
561
+ const entries = [];
562
+ for (let i = 0; i < parts.length && i < MAX_ENTRIES; i++) {
563
+ const part = parts[i];
564
+ const semi = part.indexOf(";");
565
+ const tag = (semi < 0 ? part : part.slice(0, semi)).trim();
566
+ if (!tag || tag === "*") continue;
567
+ let q = 1;
568
+ if (semi >= 0) {
569
+ const m = QVALUE.exec(part.slice(semi + 1));
570
+ if (!m) continue;
571
+ q = Number(m[1]);
572
+ }
573
+ if (q > 0) entries.push({
574
+ tag,
575
+ q
576
+ });
577
+ }
578
+ entries.sort((a, b) => b.q - a.q);
579
+ return entries.map((e) => e.tag);
580
+ }
581
+ const ISLANDS = "/_server-islands/";
582
+ /** §2.11.9 for one request, with the canonical and root redirects. */
583
+ function negotiateFor(request, url, config) {
584
+ if (ROUTES) config = atHost(config, url.host);
585
+ const { locales, sourceLocale, routing } = config;
586
+ const pathname = url.pathname;
587
+ const { routeStrategies, strategy } = routing;
588
+ const keys = keysOf(routeStrategies);
589
+ const route = keys.length ? matchRoute(keys, routeOf(pathname, config)) : null;
590
+ const chain = route === null ? strategy : routeStrategies[route];
591
+ let locale = null;
592
+ let source = "default";
593
+ let vary;
594
+ let redirect;
595
+ let status;
596
+ const headers = request.headers;
597
+ if (headers.get("sec-fetch-mode") !== "navigate" && !(urlStep(config, pathname) && chain.includes("url"))) {
598
+ const x = headers.get(LOCALE_HEADER);
599
+ if (x !== null && locales.includes(x)) {
600
+ locale = x;
601
+ source = "header-x";
602
+ vary = "x-vi18n-locale";
603
+ }
604
+ }
605
+ if (locale === null && routeOf(pathname, config).startsWith(ISLANDS)) {
606
+ let ref = null;
607
+ try {
608
+ ref = new URL(headers.get("referer")).pathname;
609
+ } catch {}
610
+ const l = ref === null || ref === "/" ? null : urlStep(config, ref);
611
+ if (l !== null) {
612
+ locale = l;
613
+ source = "referer";
614
+ vary = "Referer";
615
+ }
616
+ }
617
+ if (locale === null) {
618
+ const base = baseOf(config);
619
+ const atRoot = pathname === base || pathname + "/" === base;
620
+ const redirecting = atRoot && routing.redirectRoot;
621
+ let asked = false;
622
+ for (const step of chain) {
623
+ let l = null;
624
+ if (step === "url") {
625
+ if (!redirecting) l = urlStep(config, pathname);
626
+ } else if (step === "cookie") {
627
+ asked = true;
628
+ l = readCookie(headers.get("cookie"), routing.cookie.name);
629
+ } else if (step === "header") {
630
+ asked = true;
631
+ l = matchLocale(parseAcceptLanguage(headers.get("accept-language")), config);
632
+ } else if (step === "default") l = sourceLocale;
633
+ if (l !== null && locales.includes(l)) {
634
+ locale = l;
635
+ source = step;
636
+ break;
637
+ }
638
+ }
639
+ locale ??= sourceLocale;
640
+ if (asked || atRoot && source !== "url") vary = "Accept-Language";
641
+ const target = ROUTES && awayHref(base, locale, config) || localizePath(base, locale, config);
642
+ if (redirecting && locale !== urlStep(config, pathname) && target !== pathname) {
643
+ redirect = new URL(target + url.search, url);
644
+ status = 307;
645
+ }
646
+ }
647
+ if (ROUTES && (routing.paths || routing.domains)) {
648
+ const target = !redirect && (request.method === "GET" || request.method === "HEAD") ? canonical(pathname, config) : null;
649
+ if (target !== null) {
650
+ redirect = new URL(target + url.search, url);
651
+ status = 301;
652
+ }
653
+ } else if (!redirect && routing.url === "prefix_except_default" && (request.method === "GET" || request.method === "HEAD")) {
654
+ const split = splitLocalePath(pathname, config);
655
+ if (split.locale === sourceLocale) {
656
+ redirect = new URL(split.path + url.search, url);
657
+ status = 301;
658
+ }
659
+ }
660
+ const n = {
661
+ locale,
662
+ source
663
+ };
664
+ if (vary) n.vary = vary;
665
+ if (redirect) {
666
+ n.redirect = redirect;
667
+ n.status = status;
668
+ }
669
+ return n;
670
+ }
671
+ /**
672
+ * The locale `request` would render in, the redirect `handle()` would
673
+ * answer, and the header its response varies on.
674
+ */
675
+ function negotiate(request) {
676
+ return negotiateFor(request, new URL(request.url), current());
677
+ }
678
+ //#endregion
679
+ //#region src/server/payload.ts
680
+ /**
681
+ * The SSR payload (INTERNALS §2.10, PLAN §5.13 "Payload contents"): what a
682
+ * page inlines so hydration renders with zero catalog requests. Ids alone
683
+ * select the client bundles: an id used by exactly one root (`pins`) brings
684
+ * that root's whole key set, other ids bring only their non-`r.*` bundles,
685
+ * and server-only modules record nothing, so island-free pages get nothing.
686
+ *
687
+ * Bundle JSON is serialised (and escaped) once per catalog and key, and
688
+ * spliced into each request's payload: a page's payload costs string
689
+ * concatenation, not a `JSON.stringify` of every message it carries.
690
+ *
691
+ * With `ssr.payload: "preload"` the same selection is preloaded instead:
692
+ * one `<link rel="preload" as="fetch" crossorigin>` per bundle ahead of the
693
+ * payload, which names the keys for the client to load at once.
694
+ */
695
+ const sent = /* @__PURE__ */ new WeakMap();
696
+ /**
697
+ * `ssr.payload: "preload"` compiled in: the plugin defines it false for the
698
+ * default config, folding the branch out (PLAN §6); copies built without the
699
+ * plugin (Nitro's) keep it and follow the registration's config.
700
+ */
701
+ const PRELOAD = typeof __VI18N_PRELOAD__ === "undefined" || !!__VI18N_PRELOAD__;
702
+ const escapeJson = (json) => json.replace(/[<\u2028\u2029]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
703
+ const attr = (v) => v.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
704
+ /** Id → keys of the bundles holding it, per plan. */
705
+ const indexes = /* @__PURE__ */ new WeakMap();
706
+ function indexOf(plan) {
707
+ let idx = indexes.get(plan);
708
+ if (!idx) {
709
+ idx = /* @__PURE__ */ new Map();
710
+ for (const key in plan.bundles) {
711
+ if (!own(plan.bundles, key)) continue;
712
+ for (const id of plan.bundles[key]) {
713
+ const ks = idx.get(id);
714
+ if (ks) ks.push(key);
715
+ else idx.set(id, [key]);
716
+ }
717
+ }
718
+ indexes.set(plan, idx);
719
+ }
720
+ return idx;
721
+ }
722
+ /** Escaped bundle JSON per catalog and key (for one plan); null when the catalog lacks one of its ids. */
723
+ const bundleCache = /* @__PURE__ */ new WeakMap();
724
+ function bundleJson(plan, messages, key) {
725
+ let c = bundleCache.get(messages);
726
+ if (c?.plan !== plan) bundleCache.set(messages, c = {
727
+ plan,
728
+ json: /* @__PURE__ */ new Map()
729
+ });
730
+ let json = c.json.get(key);
731
+ if (json === void 0) {
732
+ json = "{";
733
+ for (const id of own(plan.bundles, key) && plan.bundles[key] || []) {
734
+ const m = messages.get(id);
735
+ if (m === void 0) {
736
+ json = null;
737
+ break;
738
+ }
739
+ json += (json.length > 1 ? "," : "") + JSON.stringify(id) + ":" + JSON.stringify(m);
740
+ }
741
+ if (json !== null) json = escapeJson(json + "}");
742
+ c.json.set(key, json);
743
+ }
744
+ return json;
745
+ }
746
+ /**
747
+ * Picks the bundles for `fresh` ids (§2.10 selection), recording them in
748
+ * `st.keys`. Returns null when none of the ids is a client id. Outside
749
+ * `dupBelow` duplicates an id lives in exactly one bundle, so "bundles not
750
+ * yet selected" needs no check of its own.
751
+ */
752
+ function select(plan, fresh, st) {
753
+ const idx = indexOf(plan);
754
+ const out = [];
755
+ const add = (k) => {
756
+ if (!st.keys.has(k)) {
757
+ st.keys.add(k);
758
+ out.push(k);
759
+ }
760
+ };
761
+ let client = false;
762
+ for (const id of fresh) {
763
+ const keys = idx.get(id);
764
+ if (!keys) continue;
765
+ client = true;
766
+ const root = own(plan.pins, id) ? plan.pins[id] : null;
767
+ if (root !== null) {
768
+ if (own(plan.roots, root)) for (const k of plan.roots[root]) add(k);
769
+ } else for (const k of keys) if (!k.startsWith("r.")) add(k);
770
+ }
771
+ if (!client) return null;
772
+ if (plan.base !== null) add(plan.base);
773
+ return out.sort();
774
+ }
775
+ /**
776
+ * The `<script>` to insert for `scope` now: the payload when none was
777
+ * emitted yet, otherwise a late payload of what is new, after the preload
778
+ * links of its bundles under `ssr.payload: "preload"`. "" when there is
779
+ * nothing to send (no client ids, nothing new).
780
+ */
781
+ function scriptFor(scope, nonce) {
782
+ const used = scope.used;
783
+ if (!used || !used.size) return "";
784
+ let st = sent.get(scope);
785
+ if (!st) sent.set(scope, st = {
786
+ head: false,
787
+ ids: /* @__PURE__ */ new Set(),
788
+ keys: /* @__PURE__ */ new Set()
789
+ });
790
+ const fresh = [];
791
+ for (const id of used) if (!st.ids.has(id)) {
792
+ st.ids.add(id);
793
+ fresh.push(id);
794
+ }
795
+ if (!fresh.length) return "";
796
+ const late = st.head;
797
+ const config = current();
798
+ const plan$1 = plan ?? null;
799
+ const messages = scope.messages;
800
+ const locale = scope.locale;
801
+ let bundles = "";
802
+ let extra = "";
803
+ let links = "";
804
+ if (plan$1) {
805
+ const keys = select(plan$1, fresh, st);
806
+ if (!keys) return "";
807
+ if (PRELOAD && config.ssr.payload === "preload") {
808
+ const hashes = own(plan$1.hashes, locale) && plan$1.hashes[locale] || {};
809
+ const named = [];
810
+ for (const k of keys) {
811
+ if (!own(hashes, k)) continue;
812
+ named.push(k);
813
+ links += "<link rel=\"preload\" as=\"fetch\" crossorigin href=\"" + attr(plan$1.assetsUrl + locale + "/" + k + "." + hashes[k] + ".json") + "\">";
814
+ }
815
+ if (late && !named.length) return "";
816
+ extra = ",\"preload\":" + escapeJson(JSON.stringify(named));
817
+ } else {
818
+ for (const k of keys) {
819
+ const json = bundleJson(plan$1, messages, k);
820
+ if (json !== null) bundles += (bundles ? "," : "") + JSON.stringify(k) + ":" + json;
821
+ }
822
+ if (late && !bundles) return "";
823
+ }
824
+ } else {
825
+ fresh.sort();
826
+ let m = "";
827
+ for (const id of fresh) {
828
+ const msg = messages.get(id);
829
+ if (msg !== void 0) m += (m ? "," : "") + JSON.stringify(id) + ":" + JSON.stringify(msg);
830
+ }
831
+ if (late && !m) return "";
832
+ extra = ",\"messages\":" + escapeJson("{" + m + "}");
833
+ }
834
+ const v = plan$1 ? plan$1.v : reg.mode === "dev" ? "dev" : null;
835
+ if (!late) st.head = true;
836
+ const json = escapeJson(JSON.stringify(late ? {
837
+ v,
838
+ locale
839
+ } : {
840
+ v,
841
+ locale,
842
+ timeZone: scope.timeZone ?? config.ssr.timeZone,
843
+ now: scope.now ?? Date.now(),
844
+ manifest: plan$1 && own(plan$1.hashes, locale) && plan$1.hashes[locale] || null
845
+ }).slice(0, -1)) + ",\"bundles\":{" + bundles + "}" + extra + "}";
846
+ return links + "<script type=\"application/json\" " + (late ? "data-vi18n-late" : "id=\"__vi18n\"") + (nonce ? " nonce=\"" + attr(nonce) + "\"" : "") + ">" + json + "<\/script>";
847
+ }
848
+ /**
849
+ * The payload element for HTML-string hooks (Nuxt `render:html`), with its
850
+ * preload links under `ssr.payload: "preload"`: the ids rendered so far in
851
+ * the current request scope, or "" when there are none.
852
+ * After it, `handle()` only adds late payloads for ids recorded later.
853
+ */
854
+ function payload(options) {
855
+ const scope = getScope();
856
+ return scope ? scriptFor(scope, options?.nonce) : "";
857
+ }
858
+ /** `<html lang dir>` for the current scope. */
859
+ function htmlAttributes() {
860
+ const locale = S.resolve().locale;
861
+ return {
862
+ lang: locale,
863
+ dir: textDirection(locale)
864
+ };
865
+ }
866
+ //#endregion
867
+ //#region src/server/translator.ts
868
+ /**
869
+ * Scope opening shared by every `/server` entry point, plus `withLocale` and
870
+ * `createTranslator` (PLAN §5.13 "E-mail, jobs and webhooks"). Every scope
871
+ * waits for the source catalog and `sources` (so untransformed code resolves
872
+ * source literals from the first request), its locale's catalog and, when the
873
+ * build has one, the client plan.
874
+ */
875
+ /**
876
+ * The configured locale `input` reaches: itself, else the closest match
877
+ * (`de-AT` reaches `de`), else null.
878
+ */
879
+ function matched(config, input) {
880
+ if (input === void 0) return null;
881
+ return config.locales.includes(input) ? input : matchLocale([input], config);
882
+ }
883
+ /**
884
+ * A configured locale for `input`, the source locale when nothing matches.
885
+ * Inputs here are user data (a stored profile locale), so a miss renders the
886
+ * source locale rather than throwing.
887
+ */
888
+ function pickLocale(config, input) {
889
+ const m = matched(config, input);
890
+ if (m === null && DEV) warnOnce(`${JSON.stringify(input)} is not a configured locale; using ${config.sourceLocale}`);
891
+ return m ?? config.sourceLocale;
892
+ }
893
+ /** An app-given zone: wrong zones are programming errors, unlike a visitor's cookie. */
894
+ function explicitZone(zone) {
895
+ if (!isTimeZone(zone)) throw new RangeError("vi18n: invalid time zone " + JSON.stringify(zone));
896
+ return zone;
897
+ }
898
+ /**
899
+ * Waits for whatever this scope needs that is not loaded or checked yet
900
+ * (the ALS probe runs before the first scope opens); resolves to the
901
+ * locale's catalog.
902
+ */
903
+ async function prepare(locale) {
904
+ const p = propagation();
905
+ const r = ready();
906
+ const plan = loadPlan();
907
+ const c = catalog(locale);
908
+ if (!p && !r && !plan && !(c instanceof Promise)) return c;
909
+ return (await Promise.all([
910
+ p,
911
+ r,
912
+ plan,
913
+ c
914
+ ]))[3];
915
+ }
916
+ /** A scope once its catalogs are loaded, carrying the registration's `sources` and dev labels. */
917
+ async function openScope(kind, locale, timeZone, timeZoneSource, now, trackUsed) {
918
+ const cat = await prepare(locale);
919
+ const scope = createScope(kind, locale, cat.messages, {
920
+ timeZone,
921
+ now,
922
+ timeZoneSource,
923
+ trackUsed,
924
+ sources
925
+ });
926
+ if (DEV && cat.labels) scope.labels = cat.labels;
927
+ return scope;
928
+ }
929
+ function translatorScope(locale, options) {
930
+ const config = current();
931
+ const zone = options?.timeZone;
932
+ return openScope("translator", pickLocale(config, locale), zone === void 0 ? config.ssr.timeZone : explicitZone(zone), zone === void 0 ? "config" : "explicit", options?.now, false);
933
+ }
934
+ /**
935
+ * Runs `fn` with `t` and `fmt` bound to `locale` (and the zone in `options`),
936
+ * also inside a request: an e-mail for another user renders in theirs.
937
+ */
938
+ async function withLocale(locale, fn, options) {
939
+ const scope = await translatorScope(locale, options);
940
+ return await als.run(scope, fn);
941
+ }
942
+ /** `t`, `rich*` and `fmt` bound to one locale and zone, for code that cannot run inside a scope. */
943
+ async function createTranslator(locale, options) {
944
+ return createScopedApi(await translatorScope(locale, options));
945
+ }
946
+ //#endregion
947
+ //#region src/server/handle.ts
948
+ /**
949
+ * Request scopes (PLAN §5.13 `handle(request, next)` steps 1–5, INTERNALS
950
+ * §3.2): negotiate, maybe redirect, open the scope with its zone and `now`,
951
+ * render inside it, then fix up the response.
952
+ */
953
+ /**
954
+ * The scope's zone, chosen before rendering (INTERNALS §6 D77): explicit →
955
+ * the route's `timeZoneFrom: config` → the `vi18n_tz` cookie → `cf.timezone`
956
+ * → `ssr.timeZone`. Invalid zones from the client are ignored.
957
+ */
958
+ function zoneFor(request, url, config, explicit) {
959
+ const ssr = config.ssr;
960
+ if (explicit !== void 0) return [explicitZone(explicit), "explicit"];
961
+ const from = ssr.timeZoneFrom;
962
+ const keys = keysOf(from);
963
+ const p = keys.length ? matchRoute(keys, routeOf(url.pathname, config)) : null;
964
+ if (p === null || from[p] !== "config") {
965
+ const c = readCookie(request.headers.get("cookie"), TIME_ZONE_COOKIE);
966
+ if (c !== null && isTimeZone(c)) return [c, "cookie"];
967
+ const cf = request.cf?.timezone;
968
+ if (isTimeZone(cf)) return [cf, "cf"];
969
+ }
970
+ return [ssr.timeZone, "config"];
971
+ }
972
+ /** Opens the request scope once its catalogs are loaded. */
973
+ function open(config, request, url, locale, options) {
974
+ const [zone, source] = zoneFor(request, url, config, options?.timeZone);
975
+ const now = options?.now;
976
+ return openScope("request", locale, zone, source, Number.isFinite(now) ? now : Date.now(), true);
977
+ }
978
+ const PUBLIC = /(?:^|[\s,])(?:public|s-maxage\s*=)/i;
979
+ const HTML = /^\s*text\/html\s*(?:;|$)/i;
980
+ /**
981
+ * `body`, read inside `scope` whoever reads it, through `inject` when
982
+ * given. Servers read a response after handle() returned, and a lazy body
983
+ * (`ReadableStream.from` over a generator, a pull source) renders as it is
984
+ * pulled. A pipe set up inside the scope is not enough: which context a
985
+ * pipe pulls in is up to the platform's stream scheduling (Node 24 resumes
986
+ * a parked pipe from the call that unblocked it), so every read and the
987
+ * cancel enter the scope themselves.
988
+ */
989
+ function inScope(body, scope, inject) {
990
+ const reader = body.getReader();
991
+ return new ReadableStream({
992
+ pull: (c) => als.run(scope, () => reader.read()).then((r) => r.done ? (inject?.flush(c), c.close()) : inject ? inject.transform(r.value, c) : c.enqueue(r.value)),
993
+ cancel: (reason) => als.run(scope, () => reader.cancel(reason))
994
+ });
995
+ }
996
+ /**
997
+ * A copy of `response` with the scope's headers and its body read inside
998
+ * the scope: injected for HTML, unchanged otherwise.
999
+ */
1000
+ function finish(request, response, scope, n, url, config, options) {
1001
+ if (response.status < 200) return response;
1002
+ const type = response.headers.get("content-type");
1003
+ const html = HTML.test(type);
1004
+ const zoned = scope.timeZoneSource === "cookie" || scope.timeZoneSource === "cf";
1005
+ const perUser = zoned && (scope.formattedTime === true || html);
1006
+ const vary = n?.vary;
1007
+ const body = response.body;
1008
+ if (!html && !vary && !perUser && !body) return response;
1009
+ const encoding = response.headers.get("content-encoding");
1010
+ const plain = html && !(encoding && encoding.toLowerCase() !== "identity");
1011
+ const publicly = () => {
1012
+ const cc = headers.get("cache-control");
1013
+ if (DEV && scope.formattedTime && zoned && cc && PUBLIC.test(cc)) warnOnce(`${routeOf(url.pathname, config)} formats times in a per-user zone but is publicly cacheable; set ssr.timeZoneFrom: config`);
1014
+ };
1015
+ const script = () => options?.payload ?? reg !== bare ? scriptFor(scope, options?.nonce) : "";
1016
+ const res = new Response(body && inScope(body, scope, plain && request.method !== "HEAD" ? injector({
1017
+ attributes: () => ({
1018
+ lang: scope.locale,
1019
+ dir: textDirection(scope.locale)
1020
+ }),
1021
+ head: script,
1022
+ tail: DEV ? () => (publicly(), script()) : script
1023
+ }) : void 0), response);
1024
+ const headers = res.headers;
1025
+ const lower = vary?.toLowerCase();
1026
+ if (vary && !(headers.get("vary") ?? "").split(",").some((t) => (t = t.trim().toLowerCase()) === "*" || t === lower)) headers.append("vary", vary);
1027
+ const cc = headers.get("cache-control");
1028
+ if (vary === "Accept-Language" ? !/private|no-store/i.test(cc) : perUser && !cc) headers.set("cache-control", cc ? "private, " + cc : "private");
1029
+ if (DEV) publicly();
1030
+ if (html) {
1031
+ if (!headers.has("content-language")) headers.set("content-language", scope.locale);
1032
+ if (!plain) {
1033
+ if (DEV) warnOnce(`${url.pathname}: ${encoding}-encoded HTML is not injected; let the server compress after handle()`);
1034
+ } else {
1035
+ headers.delete("content-length");
1036
+ const etag = headers.get("etag");
1037
+ if (etag && !etag.startsWith("W/")) headers.set("etag", "W/" + etag);
1038
+ if (DEV && /charset=(?!"?utf-?8)/i.test(type)) warnOnce(`${url.pathname}: the payload is UTF-8; serve HTML as UTF-8 (${type})`);
1039
+ }
1040
+ }
1041
+ return res;
1042
+ }
1043
+ /**
1044
+ * Negotiates the locale, answers the canonical or root redirect, renders
1045
+ * `next(request)` (the original request: rewriting stays with the router)
1046
+ * inside the request scope, and streams HTML through the injection.
1047
+ */
1048
+ async function handle(request, next, options) {
1049
+ const config = current();
1050
+ const url = new URL(request.url);
1051
+ const f = matched(config, options?.locale);
1052
+ const n = f === null ? negotiateFor(request, url, config) : null;
1053
+ if (n?.redirect) {
1054
+ const headers = new Headers({ location: ROUTES && n.redirect.host !== url.host ? n.redirect.href : n.redirect.pathname + n.redirect.search });
1055
+ if (n.status === 307) {
1056
+ headers.set("cache-control", "private");
1057
+ if (n.vary) headers.set("vary", n.vary);
1058
+ }
1059
+ return new Response(null, {
1060
+ status: n.status,
1061
+ headers
1062
+ });
1063
+ }
1064
+ const scope = await open(config, request, url, f ?? n.locale, options);
1065
+ if (ROUTES) scope.home = hostLocale(url.host, config);
1066
+ const response = await als.run(scope, () => next(request));
1067
+ if (DEV && !(response instanceof Response)) throw error("handle(): next() must return a Response");
1068
+ return finish(request, response, scope, n, url, config, options);
1069
+ }
1070
+ /**
1071
+ * Runs `fn` in the request's scope, for servers that build the response
1072
+ * themselves (Nuxt), and returns its value; a `Response` with a body comes
1073
+ * back with the same status, headers and bytes, its body read inside the
1074
+ * scope.
1075
+ */
1076
+ async function withRequest(request, fn, options) {
1077
+ const config = current();
1078
+ const url = new URL(request.url);
1079
+ const scope = await open(config, request, url, matched(config, options?.locale) ?? negotiateFor(request, url, config).locale, options);
1080
+ if (ROUTES) scope.home = hostLocale(url.host, config);
1081
+ const value = await als.run(scope, fn);
1082
+ return value instanceof Response && value.body ? new Response(inScope(value.body, scope), value) : value;
1083
+ }
1084
+ /**
1085
+ * Re-scopes the current request to `locale` (Astro server islands whose
1086
+ * props carry it). The scope object is updated in place, so the request's
1087
+ * resolver, its payload and its `<html>` attributes all follow.
1088
+ */
1089
+ async function scopeLocale(locale) {
1090
+ const scope = getScope();
1091
+ if (!scope) throw error("scopeLocale() called outside a request scope");
1092
+ const l = pickLocale(current(), locale);
1093
+ if (l === scope.locale) return;
1094
+ const cat = await prepare(l);
1095
+ scope.locale = l;
1096
+ scope.messages = cat.messages;
1097
+ scope.labels = cat.labels;
1098
+ }
1099
+ //#endregion
1100
+ //#region src/server/seo.ts
1101
+ /**
1102
+ * Prerender and SEO helpers (PLAN §5.13, §5.14): every locale's URL of a page
1103
+ * for crawlers (`alternates`), and the paths or params a prerenderer walks
1104
+ * (`localizedPaths`, `localizedParams`). Pseudo-locales are never advertised.
1105
+ */
1106
+ const advertised = (config) => config.locales.filter((l) => !config.pseudoLocales.includes(l));
1107
+ /** `pt-BR` → `pt_BR`; a missing region comes from the likely subtags (`de` → `de_DE`). */
1108
+ function ogLocale(locale) {
1109
+ try {
1110
+ const l = new Intl.Locale(locale);
1111
+ const region = l.region ?? l.maximize().region;
1112
+ return region ? l.language + "_" + region : l.language;
1113
+ } catch {
1114
+ return locale;
1115
+ }
1116
+ }
1117
+ /**
1118
+ * hreflang links for every locale plus `x-default` (the source locale's
1119
+ * URL), the self-canonical (`origin + pathname`: queries and fragments are
1120
+ * not separate pages), and `og:locale` metadata for the current locale.
1121
+ */
1122
+ function alternates(url) {
1123
+ const config = current();
1124
+ const locale = getScope()?.locale ?? config.sourceLocale;
1125
+ if (ROUTES && config.routing.domains) return onDomains(url, config, locale);
1126
+ const path = splitLocalePath(url.pathname, config).path;
1127
+ const origin = url.origin;
1128
+ const links = [];
1129
+ const locales = advertised(config);
1130
+ const alternate = (hreflang, l) => void links.push({
1131
+ rel: "alternate",
1132
+ hreflang,
1133
+ href: origin + localizePath(path, l, config)
1134
+ });
1135
+ if (config.routing.url !== "no_prefix") {
1136
+ for (const l of locales) alternate(l, l);
1137
+ alternate("x-default", config.sourceLocale);
1138
+ }
1139
+ links.push({
1140
+ rel: "canonical",
1141
+ href: origin + url.pathname
1142
+ });
1143
+ const meta = [{
1144
+ property: "og:locale",
1145
+ content: ogLocale(locale)
1146
+ }];
1147
+ for (const l of locales) if (l !== locale) meta.push({
1148
+ property: "og:locale:alternate",
1149
+ content: ogLocale(l)
1150
+ });
1151
+ return {
1152
+ links,
1153
+ meta
1154
+ };
1155
+ }
1156
+ /** `og:locale` for `locale`, then `og:locale:alternate` for every other advertised locale. */
1157
+ function ogMeta(config, locale) {
1158
+ const meta = [{
1159
+ property: "og:locale",
1160
+ content: ogLocale(locale)
1161
+ }];
1162
+ for (const l of advertised(config)) if (l !== locale) meta.push({
1163
+ property: "og:locale:alternate",
1164
+ content: ogLocale(l)
1165
+ });
1166
+ return meta;
1167
+ }
1168
+ /**
1169
+ * `alternates()` with domains (§6 D135): every URL comes from the config,
1170
+ * none from the request's `Host`. Each locale's alternate, `x-default` and
1171
+ * the canonical are on the locale's first listed origin, unprefixed, so a
1172
+ * page served on an alias, a preview host or with a forged `Host` still
1173
+ * names the configured site.
1174
+ */
1175
+ function onDomains(url, config, locale) {
1176
+ const path = splitLocalePath(url.pathname, atHost(config, url.host)).path;
1177
+ const at = (l) => (localeOrigin(l, config) ?? url.origin) + localizePath(path, l, atHome(config, l));
1178
+ const links = advertised(config).map((l) => ({
1179
+ rel: "alternate",
1180
+ hreflang: l,
1181
+ href: at(l)
1182
+ }));
1183
+ links.push({
1184
+ rel: "alternate",
1185
+ hreflang: "x-default",
1186
+ href: at(config.sourceLocale)
1187
+ });
1188
+ links.push({
1189
+ rel: "canonical",
1190
+ href: at(locale)
1191
+ });
1192
+ return {
1193
+ links,
1194
+ meta: ogMeta(config, locale)
1195
+ };
1196
+ }
1197
+ /** Rewritten for the codec with the app's base left out. */
1198
+ const atRoot = (config) => ({
1199
+ ...config,
1200
+ routing: {
1201
+ ...config.routing,
1202
+ base: "/"
1203
+ }
1204
+ });
1205
+ /**
1206
+ * Every path in every advertised locale, for prerender entry lists. Paths are
1207
+ * base-free, as for `/config`'s `localizedPaths` and `localizedParams`:
1208
+ * prerenderers list routes, and one helper must not need different input
1209
+ * at build time and at request time (PLAN §4).
1210
+ */
1211
+ function localizedPaths(paths) {
1212
+ return localizedPathsFor(paths, atRoot(current()));
1213
+ }
1214
+ /**
1215
+ * Astro `getStaticPaths()` params for a `[...lang]` route: the locale segment
1216
+ * of each advertised locale, `undefined` where the locale's URL carries none
1217
+ * (the source locale under `prefix_except_default`).
1218
+ */
1219
+ function localizedParams(key = "lang") {
1220
+ const config = current();
1221
+ const root = atRoot(config);
1222
+ const out = [];
1223
+ const seen = /* @__PURE__ */ new Set();
1224
+ for (const l of advertised(config)) {
1225
+ const localized = localizePath("/", l, root);
1226
+ const segment = localized === "/" ? void 0 : localized.slice(1);
1227
+ if (seen.has(segment)) continue;
1228
+ seen.add(segment);
1229
+ out.push({ params: { [key]: segment } });
1230
+ }
1231
+ return out;
1232
+ }
1233
+ //#endregion
1234
+ export { alternates, createTranslator, getScope, handle, htmlAttributes, localizedParams, localizedPaths, negotiate, payload, registerCatalogs, scopeLocale, setScopeResolver, withLocale, withRequest };