@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.
- 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-BIKEuu6B.js +201 -0
- package/dist/check-HsmmpJZg.js +1 -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-BaDh0cHG.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-C2ENBRJe.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-C9Sy6UWv.js +103 -0
- package/dist/extract-C3S8btx0.js +298 -0
- package/dist/formats-9unqj_fZ.js +781 -0
- package/dist/i18next-9HAoxwUI.js +1 -0
- package/dist/i18next-BEJgv_S8.js +1596 -0
- package/dist/import-nANvQutw.js +225 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +239 -0
- package/dist/init-C2ScuW6a.js +792 -0
- package/dist/library-D5CWvTQs.js +1882 -0
- package/dist/lingui-zUiCl9Um.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-RMMMc88P.js +31 -0
- package/dist/match-BGr8EXB3.js +89 -0
- package/dist/mcp-CHVwBoRP.js +28 -0
- package/dist/merge-driver-BTX87aVm.js +422 -0
- package/dist/messages-BfjIExyB.js +166 -0
- package/dist/migrate-IpLDMXmq.js +1484 -0
- package/dist/migrate-rCQuOGsf.js +1 -0
- package/dist/openai-CTaMDcl-.js +346 -0
- package/dist/output-DWE3SIUA.js +164 -0
- package/dist/parse.d.ts +62 -0
- package/dist/parse.js +156 -0
- package/dist/pipeline-C9Dbotkh.js +2031 -0
- package/dist/pipeline-DbX6rlo5.js +1 -0
- package/dist/project-Bw9UPIdx.js +1046 -0
- package/dist/project-C4GpaN-n.js +381 -0
- package/dist/pseudo-DRv30iN5.js +145 -0
- package/dist/raw-C4Y6vyGY.js +4708 -0
- package/dist/react-intl-BP0u-D5m.js +519 -0
- package/dist/restore-CiYv-jxS.js +175 -0
- package/dist/rewrite-EsIw_lsq.js +803 -0
- package/dist/rich-DCUKl5HM.js +97 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/run-BEcnBbu-.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-Da1Ot9IN.js +10485 -0
- package/dist/server-deBOq2vq.js +11823 -0
- package/dist/server.d.ts +169 -0
- package/dist/server.js +1234 -0
- package/dist/sitemap-DJXzKoza.js +159 -0
- package/dist/sources-D19czDSA.js +2242 -0
- package/dist/status-zr4jY59Y.js +149 -0
- package/dist/stdio-CVaJfxA7.js +29 -0
- package/dist/stdio-FPOKQAgu.js +95 -0
- package/dist/sync-B9V2KFwb.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-CQnJfLjO.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-Byqi2guX.js +28 -0
- package/dist/vite-scan-CT_vsp4g.js +242 -0
- package/dist/vite.d.ts +34 -0
- package/dist/vite.js +7558 -0
- package/dist/vue-i18n-g0iGy6CZ.js +651 -0
- package/dist/which-BOemZ5Q8.js +116 -0
- package/dist/worker-84U6uYB3.js +1656 -0
- package/package.json +97 -3
- package/schemas/catalog.json +57 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/cli/types.ts
|
|
2
|
+
/** Exit codes of every command. */
|
|
3
|
+
const EXIT = {
|
|
4
|
+
ok: 0,
|
|
5
|
+
/** Errors found, drift, a safety refusal, failed translations. */
|
|
6
|
+
failure: 1,
|
|
7
|
+
/** Bad flags, no config, invalid config. */
|
|
8
|
+
usage: 2
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
export { EXIT as t };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { o as isDiagnosticError } from "./types-D3QGM8XO.js";
|
|
2
|
+
import { C as usableTranslation, S as entryKey, x as entryCanonical } from "./library-D5CWvTQs.js";
|
|
3
|
+
import { r as resolveDuplicates } from "./duplicates-yuRljQ6h.js";
|
|
4
|
+
//#region src/core/exchange/units.ts
|
|
5
|
+
/**
|
|
6
|
+
* Catalog exchange (PLAN §4.4, §5.19, INTERNALS §2.6.9): one format-neutral
|
|
7
|
+
* view of a locale's entries that every export format writes, and the
|
|
8
|
+
* source-identity rules that export, import and restore share. A unit's
|
|
9
|
+
* source is always the text its translation was made for: for an `"id"`-layout
|
|
10
|
+
* target that is the source locale's text when its label still fingerprints
|
|
11
|
+
* it, else the entry's `#. source:` note, else nothing.
|
|
12
|
+
*/
|
|
13
|
+
const EXCHANGE_FORMATS = [
|
|
14
|
+
"po",
|
|
15
|
+
"json",
|
|
16
|
+
"flat",
|
|
17
|
+
"xliff2",
|
|
18
|
+
"arb",
|
|
19
|
+
"csv"
|
|
20
|
+
];
|
|
21
|
+
/** File extension per format; `vi18n import` infers the format from it. */
|
|
22
|
+
const EXTENSIONS = {
|
|
23
|
+
po: ".po",
|
|
24
|
+
json: ".json",
|
|
25
|
+
flat: ".json",
|
|
26
|
+
xliff2: ".xliff",
|
|
27
|
+
arb: ".arb",
|
|
28
|
+
csv: ".csv"
|
|
29
|
+
};
|
|
30
|
+
/** `"id"`-layout targets join the source locale's entries by key (INTERNALS §2.6.1). */
|
|
31
|
+
function sourceIndexOf(sourceCatalog, layout, codec) {
|
|
32
|
+
if (layout !== "id" || sourceCatalog === null) return null;
|
|
33
|
+
const index = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const entry of sourceCatalog.entries) {
|
|
35
|
+
const key = entryKey(entry, layout, codec);
|
|
36
|
+
if (key !== null && entry.source !== null && !index.has(key)) index.set(key, entry.source);
|
|
37
|
+
}
|
|
38
|
+
return index;
|
|
39
|
+
}
|
|
40
|
+
function canonicalOrNull(text, codec) {
|
|
41
|
+
try {
|
|
42
|
+
return codec.canonicalize(text);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (isDiagnosticError(error)) return null;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The canonical source and label an entry's translation belongs to, or null
|
|
50
|
+
* when the catalog cannot say (an `"id"`-layout entry whose label no known
|
|
51
|
+
* text fingerprints, or a source that does not parse).
|
|
52
|
+
*/
|
|
53
|
+
function entrySource(entry, key, context) {
|
|
54
|
+
const { codec, layout } = context;
|
|
55
|
+
if (layout === "id") {
|
|
56
|
+
if (entry.label === null) return null;
|
|
57
|
+
for (const text of [context.sourceIndex?.get(key) ?? null, entry.source]) {
|
|
58
|
+
if (text === null) continue;
|
|
59
|
+
const canonical = canonicalOrNull(text, codec);
|
|
60
|
+
if (canonical !== null && codec.label(canonical) === entry.label) return {
|
|
61
|
+
canonical,
|
|
62
|
+
label: entry.label
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const canonical = entryCanonical(entry, codec);
|
|
68
|
+
return canonical === null ? null : {
|
|
69
|
+
canonical,
|
|
70
|
+
label: codec.label(canonical)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The units of one locale's catalog, in catalog order, one per key (the
|
|
75
|
+
* read-side duplicate pick). Foreign contexts, and entries whose source is
|
|
76
|
+
* unknown, are counted in `unsourced` and left out.
|
|
77
|
+
*/
|
|
78
|
+
function exchangeUnits(input) {
|
|
79
|
+
const { catalog, context, locale, isSourceLocale } = input;
|
|
80
|
+
if (catalog === null) return {
|
|
81
|
+
units: [],
|
|
82
|
+
unsourced: 0
|
|
83
|
+
};
|
|
84
|
+
const { codec, layout } = context;
|
|
85
|
+
const resolved = resolveDuplicates(catalog, codec).catalog;
|
|
86
|
+
const units = [];
|
|
87
|
+
const seen = /* @__PURE__ */ new Set();
|
|
88
|
+
let unsourced = 0;
|
|
89
|
+
for (const entry of resolved.entries) {
|
|
90
|
+
const key = entryKey(entry, layout, codec);
|
|
91
|
+
if (key === null) {
|
|
92
|
+
unsourced++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (seen.has(key)) continue;
|
|
96
|
+
seen.add(key);
|
|
97
|
+
const source = entrySource(entry, key, context);
|
|
98
|
+
if (source === null) {
|
|
99
|
+
unsourced++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const manual = entry.manualId !== null;
|
|
103
|
+
const translation = isSourceLocale ? "" : entry.translation;
|
|
104
|
+
const usable = !isSourceLocale && usableTranslation(entry, layout, {
|
|
105
|
+
id: key,
|
|
106
|
+
manual,
|
|
107
|
+
canonical: source.canonical,
|
|
108
|
+
sourceHash: codec.sourceHash(source.canonical),
|
|
109
|
+
label: layout === "id" ? entry.label : source.label
|
|
110
|
+
}, codec, locale).ok;
|
|
111
|
+
units.push({
|
|
112
|
+
id: key,
|
|
113
|
+
manual,
|
|
114
|
+
source: source.canonical,
|
|
115
|
+
label: source.label,
|
|
116
|
+
translation,
|
|
117
|
+
state: isSourceLocale ? "untranslated" : entry.state,
|
|
118
|
+
fuzzy: !isSourceLocale && entry.fuzzy,
|
|
119
|
+
usable,
|
|
120
|
+
entry
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
units,
|
|
125
|
+
unsourced
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Key-only formats (flat JSON, ARB, CSV) carry no source, so a manual id is
|
|
130
|
+
* keyed with its source's label (PLAN §5.19): a row translated against older
|
|
131
|
+
* text no longer matches and is reported instead of imported. A generated
|
|
132
|
+
* id is already its source's label.
|
|
133
|
+
*/
|
|
134
|
+
function exchangeKey(unit) {
|
|
135
|
+
return unit.manual ? `${unit.id}~${unit.label}` : unit.id;
|
|
136
|
+
}
|
|
137
|
+
/** `hero.cta~Hpi4JmqaQa60` → the manual id and the label; a plain key has no label. */
|
|
138
|
+
function parseExchangeKey(raw) {
|
|
139
|
+
const at = raw.lastIndexOf("~");
|
|
140
|
+
if (at === -1) return {
|
|
141
|
+
key: raw,
|
|
142
|
+
label: null
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
key: raw.slice(0, at),
|
|
146
|
+
label: raw.slice(at + 1)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { exchangeKey as a, sourceIndexOf as c, entrySource as i, EXTENSIONS as n, exchangeUnits as o, canonicalOrNull as r, parseExchangeKey as s, EXCHANGE_FORMATS as t };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
//#region src/runtime/url.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether localized paths and domains are compiled in (§6 D134). The plugin
|
|
4
|
+
* defines `__VI18N_ROUTES__` false for a config with neither, which folds
|
|
5
|
+
* every `ROUTES` branch out of the app, so a default app ships the codec it
|
|
6
|
+
* shipped before; code bundled without the plugin (servers registered with
|
|
7
|
+
* `registerCatalogs()`, `/config` under Node) keeps them.
|
|
8
|
+
*/
|
|
9
|
+
const ROUTES = typeof __VI18N_ROUTES__ === "undefined" || !!__VI18N_ROUTES__;
|
|
10
|
+
/** The base as a path starting and ending with `/`; a relative Vite base has no path to strip. */
|
|
11
|
+
const baseOf = (config) => {
|
|
12
|
+
const b = config.routing.base;
|
|
13
|
+
return b[0] === "/" ? b.replace(/\/?$/, "/") : "/";
|
|
14
|
+
};
|
|
15
|
+
/** The part of `pathname` after the base, or null when the pathname is outside it. */
|
|
16
|
+
const rest = (pathname, base) => pathname.startsWith(base) ? pathname.slice(base.length) : pathname + "/" === base ? "" : null;
|
|
17
|
+
/**
|
|
18
|
+
* Collapses leading slashes and backslashes to one `/`. Browsers read `//host`
|
|
19
|
+
* and `/\host` as another origin, so a same-origin path such as
|
|
20
|
+
* `/de//evil.example/` must never come out of the codec in that form: its
|
|
21
|
+
* output becomes page loads, router navigations and the server's
|
|
22
|
+
* canonical redirect.
|
|
23
|
+
*/
|
|
24
|
+
const local = (path) => path.replace(/^[/\\]{2,}/, "/");
|
|
25
|
+
/**
|
|
26
|
+
* Splits the locale segment off a pathname. Only a segment equal to a
|
|
27
|
+
* configured locale (case-sensitive) is a locale; `path` keeps the base and
|
|
28
|
+
* any trailing slash, and a localized root (`/de`, `/de/`) becomes the base.
|
|
29
|
+
*/
|
|
30
|
+
function splitLocalePath(pathname, config) {
|
|
31
|
+
const base = baseOf(config);
|
|
32
|
+
const p = pathname + "/";
|
|
33
|
+
if (config.routing.url !== "no_prefix" && p.startsWith(base)) {
|
|
34
|
+
const r = p.slice(base.length);
|
|
35
|
+
const i = r.indexOf("/");
|
|
36
|
+
const seg = r.slice(0, i);
|
|
37
|
+
if (config.locales.includes(seg)) {
|
|
38
|
+
if (ROUTES) return {
|
|
39
|
+
locale: seg,
|
|
40
|
+
path: translate(local(base + r.slice(i + 1, -1)), seg, config, 1)
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
locale: seg,
|
|
44
|
+
path: local(base + r.slice(i + 1, -1))
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (ROUTES) return {
|
|
49
|
+
locale: null,
|
|
50
|
+
path: translate(pathname, unprefixed(config), config, 1)
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
locale: null,
|
|
54
|
+
path: pathname
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The pathname of `path` in `locale`. Any locale segment already in `path` is
|
|
59
|
+
* replaced, so localizing is idempotent. An unconfigured locale, `no_prefix`
|
|
60
|
+
* and pathnames outside the base leave `path` unchanged.
|
|
61
|
+
*/
|
|
62
|
+
function localizePath(path, locale, config) {
|
|
63
|
+
const url = config.routing.url;
|
|
64
|
+
let p = path;
|
|
65
|
+
if (url !== "no_prefix" && config.locales.includes(locale)) {
|
|
66
|
+
p = splitLocalePath(path, config).path;
|
|
67
|
+
if (ROUTES) p = translate(p, locale, config, 0);
|
|
68
|
+
if (locale !== (ROUTES && config.routing.home || config.sourceLocale) || url === "prefix") {
|
|
69
|
+
const base = baseOf(config);
|
|
70
|
+
const q = p + "/";
|
|
71
|
+
if (q.startsWith(base)) {
|
|
72
|
+
const r = q.slice(base.length, -1);
|
|
73
|
+
p = base + locale + (r && "/" + r);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return local(p);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Every path in every configured locale: per path in order, one entry per
|
|
81
|
+
* locale in `locales` order, duplicates dropped. Pseudo-locales are never
|
|
82
|
+
* prerendered or advertised, so they are left out.
|
|
83
|
+
*/
|
|
84
|
+
function localizedPathsFor(paths, config) {
|
|
85
|
+
const out = /* @__PURE__ */ new Set();
|
|
86
|
+
const pseudo = config.pseudoLocales ?? [];
|
|
87
|
+
for (const path of paths) for (const l of config.locales) if (!pseudo.includes(l)) out.add(localizePath(path, l, config));
|
|
88
|
+
return [...out];
|
|
89
|
+
}
|
|
90
|
+
/** Segments of a path as a pathname spells them: `/über-uns` → `%C3%BCber-uns`. */
|
|
91
|
+
const segmentsOf = (path) => new URL(path, "http://h").pathname.split("/").slice(1);
|
|
92
|
+
/**
|
|
93
|
+
* `from` as a pattern and `to` as its template: a `*` segment matches one
|
|
94
|
+
* segment and a final `**` any number (with their slashes), and the
|
|
95
|
+
* template puts them back in order (`$1`, `$2`). A template never has a
|
|
96
|
+
* digit right after `$n`: a slash or its end follows.
|
|
97
|
+
*/
|
|
98
|
+
function pathRule(from, to) {
|
|
99
|
+
let source = "";
|
|
100
|
+
let template = "";
|
|
101
|
+
let n = 0;
|
|
102
|
+
for (const s of segmentsOf(from)) source += s === "**" ? "((?:/[^/]+)*)" : s === "*" ? "/([^/]+)" : "/" + s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
103
|
+
for (const s of segmentsOf(to)) template += s === "**" ? "$" + ++n : s === "*" ? "/$" + ++n : "/" + s.replace(/\$/g, "$$$$");
|
|
104
|
+
return [new RegExp("^" + source + "$"), template];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `routing.paths` as rules, for the codec, the SPA boot script and the CLI.
|
|
108
|
+
* Exact paths come before patterns (each group in config order), so
|
|
109
|
+
* `/blog/new` wins over `/blog/*` in both directions, as routers rank them.
|
|
110
|
+
*/
|
|
111
|
+
function pathTable(paths) {
|
|
112
|
+
const table = {
|
|
113
|
+
to: {},
|
|
114
|
+
from: {}
|
|
115
|
+
};
|
|
116
|
+
const wild = (p) => +p.includes("*");
|
|
117
|
+
for (const key of Object.keys(paths).sort((a, b) => wild(a) - wild(b))) {
|
|
118
|
+
const entry = paths[key];
|
|
119
|
+
for (const l of Object.keys(entry)) {
|
|
120
|
+
(table.to[l] ??= []).push(pathRule(key, entry[l]));
|
|
121
|
+
(table.from[l] ??= []).push(pathRule(entry[l], key));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return table;
|
|
125
|
+
}
|
|
126
|
+
const compiled = /* @__PURE__ */ new WeakMap();
|
|
127
|
+
/** The locale of an unprefixed path: the host's, else the source locale; none under `prefix`. */
|
|
128
|
+
const unprefixed = (config) => {
|
|
129
|
+
const url = config.routing.url;
|
|
130
|
+
return url === "prefix" || url === "no_prefix" ? void 0 : config.routing.home ?? config.sourceLocale;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* `path` (a pathname with base) in locale `l` (`back` 0: from a route path)
|
|
134
|
+
* or back to its route path (`back` 1). A path outside the base, the root
|
|
135
|
+
* and paths no rule matches are left alone; a trailing slash is kept.
|
|
136
|
+
*/
|
|
137
|
+
function translate(path, l, config, back) {
|
|
138
|
+
const paths = config.routing.paths;
|
|
139
|
+
if (!paths || l === void 0) return path;
|
|
140
|
+
let table = compiled.get(paths);
|
|
141
|
+
if (!table) compiled.set(paths, table = pathTable(paths));
|
|
142
|
+
const rules = back ? table.from : table.to;
|
|
143
|
+
const list = Object.hasOwn(rules, l) ? rules[l] : void 0;
|
|
144
|
+
const base = baseOf(config);
|
|
145
|
+
const r = list ? rest(path, base) : null;
|
|
146
|
+
if (!list || !r) return path;
|
|
147
|
+
const end = r.endsWith("/") ? "/" : "";
|
|
148
|
+
const p = "/" + (end ? r.slice(0, -1) : r);
|
|
149
|
+
for (const [re, template] of list) if (re.test(p)) return local(base + p.replace(re, template).slice(1) + end);
|
|
150
|
+
return path;
|
|
151
|
+
}
|
|
152
|
+
/** Where links to `locale` go: its first listed origin. */
|
|
153
|
+
function localeOrigin(locale, config) {
|
|
154
|
+
const domains = config.routing.domains;
|
|
155
|
+
if (domains) {
|
|
156
|
+
for (const o in domains) if (domains[o] === locale) return o;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* `config` for a page whose host serves `home` (undefined: an unlisted
|
|
161
|
+
* host). The copy shares every table with `config`, so compiled path rules
|
|
162
|
+
* and route-glob keys stay cached across copies.
|
|
163
|
+
*/
|
|
164
|
+
const atHome = (config, home) => home === config.routing.home ? config : {
|
|
165
|
+
...config,
|
|
166
|
+
routing: {
|
|
167
|
+
...config.routing,
|
|
168
|
+
home
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
//#endregion
|
|
172
|
+
export { pathTable as a, localizedPathsFor as i, localeOrigin as n, localizePath as r, atHome as t };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
//#region src/runtime/url.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether localized paths and domains are compiled in (§6 D134). The plugin
|
|
4
|
+
* defines `__VI18N_ROUTES__` false for a config with neither, which folds
|
|
5
|
+
* every `ROUTES` branch out of the app, so a default app ships the codec it
|
|
6
|
+
* shipped before; code bundled without the plugin (servers registered with
|
|
7
|
+
* `registerCatalogs()`, `/config` under Node) keeps them.
|
|
8
|
+
*/
|
|
9
|
+
const ROUTES = typeof __VI18N_ROUTES__ === "undefined" || !!__VI18N_ROUTES__;
|
|
10
|
+
/** The base as a path starting and ending with `/`; a relative Vite base has no path to strip. */
|
|
11
|
+
const baseOf = (config) => {
|
|
12
|
+
const b = config.routing.base;
|
|
13
|
+
return b[0] === "/" ? b.replace(/\/?$/, "/") : "/";
|
|
14
|
+
};
|
|
15
|
+
/** The part of `pathname` after the base, or null when the pathname is outside it. */
|
|
16
|
+
const rest = (pathname, base) => pathname.startsWith(base) ? pathname.slice(base.length) : pathname + "/" === base ? "" : null;
|
|
17
|
+
/**
|
|
18
|
+
* Collapses leading slashes and backslashes to one `/`. Browsers read `//host`
|
|
19
|
+
* and `/\host` as another origin, so a same-origin path such as
|
|
20
|
+
* `/de//evil.example/` must never come out of the codec in that form: its
|
|
21
|
+
* output becomes page loads, router navigations and the server's
|
|
22
|
+
* canonical redirect.
|
|
23
|
+
*/
|
|
24
|
+
const local = (path) => path.replace(/^[/\\]{2,}/, "/");
|
|
25
|
+
/**
|
|
26
|
+
* Splits the locale segment off a pathname. Only a segment equal to a
|
|
27
|
+
* configured locale (case-sensitive) is a locale; `path` keeps the base and
|
|
28
|
+
* any trailing slash, and a localized root (`/de`, `/de/`) becomes the base.
|
|
29
|
+
*/
|
|
30
|
+
function splitLocalePath(pathname, config) {
|
|
31
|
+
const base = baseOf(config);
|
|
32
|
+
const p = pathname + "/";
|
|
33
|
+
if (config.routing.url !== "no_prefix" && p.startsWith(base)) {
|
|
34
|
+
const r = p.slice(base.length);
|
|
35
|
+
const i = r.indexOf("/");
|
|
36
|
+
const seg = r.slice(0, i);
|
|
37
|
+
if (config.locales.includes(seg)) {
|
|
38
|
+
if (ROUTES) return {
|
|
39
|
+
locale: seg,
|
|
40
|
+
path: translate(local(base + r.slice(i + 1, -1)), seg, config, 1)
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
locale: seg,
|
|
44
|
+
path: local(base + r.slice(i + 1, -1))
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (ROUTES) return {
|
|
49
|
+
locale: null,
|
|
50
|
+
path: translate(pathname, unprefixed(config), config, 1)
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
locale: null,
|
|
54
|
+
path: pathname
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The pathname of `path` in `locale`. Any locale segment already in `path` is
|
|
59
|
+
* replaced, so localizing is idempotent. An unconfigured locale, `no_prefix`
|
|
60
|
+
* and pathnames outside the base leave `path` unchanged.
|
|
61
|
+
*/
|
|
62
|
+
function localizePath(path, locale, config) {
|
|
63
|
+
const url = config.routing.url;
|
|
64
|
+
let p = path;
|
|
65
|
+
if (url !== "no_prefix" && config.locales.includes(locale)) {
|
|
66
|
+
p = splitLocalePath(path, config).path;
|
|
67
|
+
if (ROUTES) p = translate(p, locale, config, 0);
|
|
68
|
+
if (locale !== (ROUTES && config.routing.home || config.sourceLocale) || url === "prefix") {
|
|
69
|
+
const base = baseOf(config);
|
|
70
|
+
const q = p + "/";
|
|
71
|
+
if (q.startsWith(base)) {
|
|
72
|
+
const r = q.slice(base.length, -1);
|
|
73
|
+
p = base + locale + (r && "/" + r);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return local(p);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Every path in every configured locale: per path in order, one entry per
|
|
81
|
+
* locale in `locales` order, duplicates dropped. Pseudo-locales are never
|
|
82
|
+
* prerendered or advertised, so they are left out.
|
|
83
|
+
*/
|
|
84
|
+
function localizedPathsFor(paths, config) {
|
|
85
|
+
const out = /* @__PURE__ */ new Set();
|
|
86
|
+
const pseudo = config.pseudoLocales ?? [];
|
|
87
|
+
for (const path of paths) for (const l of config.locales) if (!pseudo.includes(l)) out.add(localizePath(path, l, config));
|
|
88
|
+
return [...out];
|
|
89
|
+
}
|
|
90
|
+
/** Segments of a path as a pathname spells them: `/über-uns` → `%C3%BCber-uns`. */
|
|
91
|
+
const segmentsOf = (path) => new URL(path, "http://h").pathname.split("/").slice(1);
|
|
92
|
+
/**
|
|
93
|
+
* `from` as a pattern and `to` as its template: a `*` segment matches one
|
|
94
|
+
* segment and a final `**` any number (with their slashes), and the
|
|
95
|
+
* template puts them back in order (`$1`, `$2`). A template never has a
|
|
96
|
+
* digit right after `$n`: a slash or its end follows.
|
|
97
|
+
*/
|
|
98
|
+
function pathRule(from, to) {
|
|
99
|
+
let source = "";
|
|
100
|
+
let template = "";
|
|
101
|
+
let n = 0;
|
|
102
|
+
for (const s of segmentsOf(from)) source += s === "**" ? "((?:/[^/]+)*)" : s === "*" ? "/([^/]+)" : "/" + s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
103
|
+
for (const s of segmentsOf(to)) template += s === "**" ? "$" + ++n : s === "*" ? "/$" + ++n : "/" + s.replace(/\$/g, "$$$$");
|
|
104
|
+
return [new RegExp("^" + source + "$"), template];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `routing.paths` as rules, for the codec, the SPA boot script and the CLI.
|
|
108
|
+
* Exact paths come before patterns (each group in config order), so
|
|
109
|
+
* `/blog/new` wins over `/blog/*` in both directions, as routers rank them.
|
|
110
|
+
*/
|
|
111
|
+
function pathTable(paths) {
|
|
112
|
+
const table = {
|
|
113
|
+
to: {},
|
|
114
|
+
from: {}
|
|
115
|
+
};
|
|
116
|
+
const wild = (p) => +p.includes("*");
|
|
117
|
+
for (const key of Object.keys(paths).sort((a, b) => wild(a) - wild(b))) {
|
|
118
|
+
const entry = paths[key];
|
|
119
|
+
for (const l of Object.keys(entry)) {
|
|
120
|
+
(table.to[l] ??= []).push(pathRule(key, entry[l]));
|
|
121
|
+
(table.from[l] ??= []).push(pathRule(entry[l], key));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return table;
|
|
125
|
+
}
|
|
126
|
+
const compiled = /* @__PURE__ */ new WeakMap();
|
|
127
|
+
/** The locale of an unprefixed path: the host's, else the source locale; none under `prefix`. */
|
|
128
|
+
const unprefixed = (config) => {
|
|
129
|
+
const url = config.routing.url;
|
|
130
|
+
return url === "prefix" || url === "no_prefix" ? void 0 : config.routing.home ?? config.sourceLocale;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* `path` (a pathname with base) in locale `l` (`back` 0: from a route path)
|
|
134
|
+
* or back to its route path (`back` 1). A path outside the base, the root
|
|
135
|
+
* and paths no rule matches are left alone; a trailing slash is kept.
|
|
136
|
+
*/
|
|
137
|
+
function translate(path, l, config, back) {
|
|
138
|
+
const paths = config.routing.paths;
|
|
139
|
+
if (!paths || l === void 0) return path;
|
|
140
|
+
let table = compiled.get(paths);
|
|
141
|
+
if (!table) compiled.set(paths, table = pathTable(paths));
|
|
142
|
+
const rules = back ? table.from : table.to;
|
|
143
|
+
const list = Object.hasOwn(rules, l) ? rules[l] : void 0;
|
|
144
|
+
const base = baseOf(config);
|
|
145
|
+
const r = list ? rest(path, base) : null;
|
|
146
|
+
if (!list || !r) return path;
|
|
147
|
+
const end = r.endsWith("/") ? "/" : "";
|
|
148
|
+
const p = "/" + (end ? r.slice(0, -1) : r);
|
|
149
|
+
for (const [re, template] of list) if (re.test(p)) return local(base + p.replace(re, template).slice(1) + end);
|
|
150
|
+
return path;
|
|
151
|
+
}
|
|
152
|
+
/** The locale a listed host (`example.de`, `localhost:5173`) serves; undefined for any other host. */
|
|
153
|
+
function hostLocale(host, config) {
|
|
154
|
+
const domains = config.routing.domains;
|
|
155
|
+
if (domains) {
|
|
156
|
+
const h = host.toLowerCase();
|
|
157
|
+
for (const o in domains) if (o.slice(o.indexOf("//") + 2) === h) return domains[o];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Where links to `locale` go: its first listed origin. */
|
|
161
|
+
function localeOrigin(locale, config) {
|
|
162
|
+
const domains = config.routing.domains;
|
|
163
|
+
if (domains) {
|
|
164
|
+
for (const o in domains) if (domains[o] === locale) return o;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* `config` for a page whose host serves `home` (undefined: an unlisted
|
|
169
|
+
* host). The copy shares every table with `config`, so compiled path rules
|
|
170
|
+
* and route-glob keys stay cached across copies.
|
|
171
|
+
*/
|
|
172
|
+
const atHome = (config, home) => home === config.routing.home ? config : {
|
|
173
|
+
...config,
|
|
174
|
+
routing: {
|
|
175
|
+
...config.routing,
|
|
176
|
+
home
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
/** `config` for a page on `host`: a listed host's locale is its unprefixed one. */
|
|
180
|
+
const atHost = (config, host) => config.routing.domains ? atHome(config, hostLocale(host, config)) : config;
|
|
181
|
+
/**
|
|
182
|
+
* The absolute URL of `route` (a route path with base) in `locale`, when
|
|
183
|
+
* the page is on a listed host and `locale` lives on another: that host's
|
|
184
|
+
* origin and the locale's unprefixed path. Null on the same host, on an
|
|
185
|
+
* unlisted host (dev servers keep their prefixes) and for a locale without
|
|
186
|
+
* a host (pseudo-locales). Only configured origins ever come out.
|
|
187
|
+
*/
|
|
188
|
+
function awayHref(route, locale, config) {
|
|
189
|
+
const home = config.routing.home;
|
|
190
|
+
const o = home !== void 0 && home !== locale ? localeOrigin(locale, config) : void 0;
|
|
191
|
+
return o ? o + localizePath(route, locale, atHome(config, locale)) : null;
|
|
192
|
+
}
|
|
193
|
+
/** `p` without a trailing slash; the root stays `/`. */
|
|
194
|
+
const trail = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
|
|
195
|
+
/**
|
|
196
|
+
* The pathname as route globs see it (INTERNALS §2.7.3): delocalized, without
|
|
197
|
+
* the base and without a trailing slash; the root is `/`.
|
|
198
|
+
*/
|
|
199
|
+
function routePath(pathname, config) {
|
|
200
|
+
const p = splitLocalePath(pathname, config).path;
|
|
201
|
+
const r = rest(p, baseOf(config));
|
|
202
|
+
return trail(r === null ? p : "/" + r);
|
|
203
|
+
}
|
|
204
|
+
const globs = /* @__PURE__ */ new Map();
|
|
205
|
+
const globToRegExp = (pattern) => {
|
|
206
|
+
let src = "";
|
|
207
|
+
for (const seg of (pattern[0] === "/" ? pattern : "/" + pattern).split("/").slice(1)) src += seg === "**" ? "(?:/.*)?" : "/" + seg.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]+");
|
|
208
|
+
return new RegExp("^" + (src || "/") + "$");
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* The first pattern (in the given order) matching `path`: `*` is one or more
|
|
212
|
+
* characters other than `/`, a `**` segment is any number of whole segments.
|
|
213
|
+
* Case-sensitive. A trailing slash on `path` is ignored.
|
|
214
|
+
*/
|
|
215
|
+
function matchRoute(patterns, path) {
|
|
216
|
+
const p = trail(path);
|
|
217
|
+
for (const pattern of patterns) {
|
|
218
|
+
let re = globs.get(pattern);
|
|
219
|
+
if (!re) globs.set(pattern, re = globToRegExp(pattern));
|
|
220
|
+
if (re.test(p)) return pattern;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
export { hostLocale as a, localizedPathsFor as c, splitLocalePath as d, awayHref as i, matchRoute as l, atHome as n, localeOrigin as o, atHost as r, localizePath as s, ROUTES as t, routePath as u };
|