@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,747 @@
|
|
|
1
|
+
import { a as diagnosticError, i as ID_LENGTH, n as DEFAULT_CONFIG, r as DEFAULT_IMPORT_SOURCES, t as CONFIG_FILE } from "./types-D3QGM8XO.js";
|
|
2
|
+
import { t as excludeKind } from "./exclude-Du-Zd_qI.js";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { readFile, stat } from "node:fs/promises";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
//#region src/core/config/schema.ts
|
|
8
|
+
/**
|
|
9
|
+
* The zod 4 schema of `vi18n.config.ts` (PLAN §5.7, INTERNALS §2.7.1). It
|
|
10
|
+
* checks shapes and per-field rules only; rules that relate two keys
|
|
11
|
+
* (`sourceLocale` ∈ `locales`, fallbacks, pseudo-locales, `ai.exclude`) need
|
|
12
|
+
* the defaults applied first and live in `crossFieldProblems`, so a config
|
|
13
|
+
* that leaves `locales` out is judged against `[sourceLocale]`.
|
|
14
|
+
*
|
|
15
|
+
* Every key is optional: defaults are applied by `resolveConfig`, never by
|
|
16
|
+
* zod, so the parsed value still tells which keys the user wrote.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* `TIME_ZONE_COOKIE` of `src/server/types.ts`, which core may not import
|
|
20
|
+
* (INTERNALS §0); the schema tests check the two agree.
|
|
21
|
+
*/
|
|
22
|
+
const TIME_ZONE_COOKIE = "vi18n_tz";
|
|
23
|
+
/** Problem with a locale tag, or null when it is valid and canonical. */
|
|
24
|
+
function localeProblem(tag) {
|
|
25
|
+
let canonical;
|
|
26
|
+
try {
|
|
27
|
+
canonical = Intl.getCanonicalLocales(tag)[0];
|
|
28
|
+
} catch {
|
|
29
|
+
return `${JSON.stringify(tag)} is not a valid BCP 47 locale tag`;
|
|
30
|
+
}
|
|
31
|
+
if (canonical !== tag) return `${JSON.stringify(tag)} is not canonical (write ${JSON.stringify(canonical)})`;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const IANA_ZONE = "an IANA time zone name such as \"Europe/Berlin\"";
|
|
35
|
+
/**
|
|
36
|
+
* Problem with `ssr.timeZone`, or null. The rule is `isTimeZone` of
|
|
37
|
+
* `src/server/catalogs.ts`, which core may not import (the schema tests check
|
|
38
|
+
* the two agree): a name starts with a letter, has at most 64 characters and
|
|
39
|
+
* Intl accepts it. UTC offsets, which Intl accepts too, are refused there
|
|
40
|
+
* (each spelling would get its own formatter in caches keyed by zone), so a
|
|
41
|
+
* config naming one must fail here rather than in `registerCatalogs()`.
|
|
42
|
+
*/
|
|
43
|
+
function timeZoneProblem(zone) {
|
|
44
|
+
if (/^[+\-\d]/.test(zone) || zone.charCodeAt(0) === 8722) return `must be ${IANA_ZONE}, not a UTC offset`;
|
|
45
|
+
const problem = `must be ${IANA_ZONE} that Intl.DateTimeFormat accepts`;
|
|
46
|
+
if (zone.length > 64 || !/^[a-z]/i.test(zone)) return problem;
|
|
47
|
+
try {
|
|
48
|
+
new Intl.DateTimeFormat("en", { timeZone: zone });
|
|
49
|
+
return null;
|
|
50
|
+
} catch {
|
|
51
|
+
return problem;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isProvider(value) {
|
|
55
|
+
if (typeof value !== "object" || value === null) return false;
|
|
56
|
+
const provider = value;
|
|
57
|
+
return typeof provider.name === "string" && provider.name !== "" && typeof provider.model === "string" && typeof provider.translate === "function";
|
|
58
|
+
}
|
|
59
|
+
/** `defaults.tags` keys: the message tag names FormatJS parses, ASCII only. */
|
|
60
|
+
const TAG_NAME = /^[A-Za-z][\w.-]*$/;
|
|
61
|
+
const ATTRIBUTE_NAME = /^[A-Za-z][\w:.-]*$/;
|
|
62
|
+
/**
|
|
63
|
+
* Elements whose content HTML does not parse as text: escaped translator
|
|
64
|
+
* text inside `<script>` or `<style>` would still run (INTERNALS §6 D205).
|
|
65
|
+
*/
|
|
66
|
+
const RAW_TEXT = /* @__PURE__ */ new Set([
|
|
67
|
+
"script",
|
|
68
|
+
"style",
|
|
69
|
+
"iframe",
|
|
70
|
+
"noembed",
|
|
71
|
+
"noframes",
|
|
72
|
+
"noscript",
|
|
73
|
+
"plaintext",
|
|
74
|
+
"xmp"
|
|
75
|
+
]);
|
|
76
|
+
const element = z.string().regex(/^[a-z][a-z\d-]*$/, { error: "an HTML element name in lower case" }).refine((name) => !RAW_TEXT.has(name), { error: "must render its children as text (not script, style, iframe or noscript)" });
|
|
77
|
+
/** PLAN §5.20: batches of 40–80 items. */
|
|
78
|
+
const BATCH_SIZE_RANGE = "must be an integer from 40 to 80";
|
|
79
|
+
const text = z.string().min(1, { error: "must be a non-empty string" });
|
|
80
|
+
const locale = z.string().superRefine((value, ctx) => {
|
|
81
|
+
const problem = localeProblem(value);
|
|
82
|
+
if (problem !== null) ctx.addIssue({
|
|
83
|
+
code: "custom",
|
|
84
|
+
message: problem
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
const positiveInt = z.number({ error: "must be a positive integer" }).int({ error: "must be a positive integer" }).positive({ error: "must be a positive integer" });
|
|
88
|
+
const withLocale = z.string().includes("{locale}", { error: "must contain {locale}" });
|
|
89
|
+
const manifestPath = withLocale.refine((path) => path.indexOf("{locale}", path.indexOf("{locale}") + 1) < 0, { error: "must contain {locale} exactly once" });
|
|
90
|
+
const routePattern = z.string().startsWith("/", { error: "route patterns start with /" });
|
|
91
|
+
const strategy = z.enum([
|
|
92
|
+
"url",
|
|
93
|
+
"cookie",
|
|
94
|
+
"header",
|
|
95
|
+
"localStorage",
|
|
96
|
+
"default"
|
|
97
|
+
]);
|
|
98
|
+
const level = z.enum([
|
|
99
|
+
"off",
|
|
100
|
+
"warn",
|
|
101
|
+
"error"
|
|
102
|
+
]);
|
|
103
|
+
const outputSchema = z.strictObject({
|
|
104
|
+
split: z.strictObject({
|
|
105
|
+
shared: z.enum(["per-set", "single"]).optional(),
|
|
106
|
+
dupBelow: positiveInt.optional(),
|
|
107
|
+
minBytes: positiveInt.optional(),
|
|
108
|
+
maxBundlesPerRoot: positiveInt.nullable().optional()
|
|
109
|
+
}).optional(),
|
|
110
|
+
guard: z.enum([
|
|
111
|
+
"auto",
|
|
112
|
+
"always",
|
|
113
|
+
"off"
|
|
114
|
+
]).optional(),
|
|
115
|
+
manifestPath: manifestPath.optional(),
|
|
116
|
+
manifest: z.enum(["data", "module"]).optional(),
|
|
117
|
+
baseEntries: z.array(text).optional(),
|
|
118
|
+
compactIds: z.boolean().optional(),
|
|
119
|
+
perLocale: z.boolean().optional()
|
|
120
|
+
});
|
|
121
|
+
const configSchema = z.strictObject({
|
|
122
|
+
sourceLocale: locale.optional(),
|
|
123
|
+
locales: z.array(locale).min(1, { error: "must list at least one locale" }).optional(),
|
|
124
|
+
pseudoLocales: z.array(z.enum(["en-XA", "ar-XB"])).optional(),
|
|
125
|
+
fallbacks: z.record(locale, z.array(locale)).optional(),
|
|
126
|
+
project: z.strictObject({
|
|
127
|
+
include: z.array(text).optional(),
|
|
128
|
+
extensions: z.array(z.string().regex(/^\.[^./\\]+$/, { error: "must look like \".tsx\"" })).optional(),
|
|
129
|
+
exclude: z.array(text).optional()
|
|
130
|
+
}).optional(),
|
|
131
|
+
extraction: z.strictObject({
|
|
132
|
+
importSources: z.array(text).optional(),
|
|
133
|
+
aliases: z.array(text).optional(),
|
|
134
|
+
globals: z.array(text).optional(),
|
|
135
|
+
keep: z.array(text).optional(),
|
|
136
|
+
dependencies: z.array(z.string().regex(/^(?:@[a-z0-9][\w.~-]*\/)?[a-z0-9][\w.~-]*$/, { error: "must be a package name, like \"@acme/ui\"" })).refine((names) => new Set(names).size === names.length, { error: "lists a package twice" }).optional()
|
|
137
|
+
}).optional(),
|
|
138
|
+
ids: z.strictObject({ length: z.number({ error: `must be an integer from ${ID_LENGTH.min} to ${ID_LENGTH.max}` }).int({ error: `must be an integer from ${ID_LENGTH.min} to ${ID_LENGTH.max}` }).min(ID_LENGTH.min, { error: `must be an integer from ${ID_LENGTH.min} to ${ID_LENGTH.max}` }).max(ID_LENGTH.max, { error: `must be an integer from ${ID_LENGTH.min} to ${ID_LENGTH.max}` }).optional() }).optional(),
|
|
139
|
+
catalogs: z.strictObject({
|
|
140
|
+
format: z.enum(["po", "json"]).optional(),
|
|
141
|
+
path: withLocale.optional(),
|
|
142
|
+
msgid: z.enum(["source", "id"]).optional(),
|
|
143
|
+
icuFlag: z.boolean().optional()
|
|
144
|
+
}).refine((catalogs) => !(catalogs.icuFlag === true && catalogs.format === "json"), {
|
|
145
|
+
error: "is a PO flag; JSON catalogs have no #, line to carry it",
|
|
146
|
+
path: ["icuFlag"]
|
|
147
|
+
}).optional(),
|
|
148
|
+
output: outputSchema.optional(),
|
|
149
|
+
routing: z.strictObject({
|
|
150
|
+
strategy: z.array(strategy).optional(),
|
|
151
|
+
routeStrategies: z.record(routePattern, z.array(strategy)).optional(),
|
|
152
|
+
url: z.enum([
|
|
153
|
+
"prefix_except_default",
|
|
154
|
+
"prefix",
|
|
155
|
+
"prefix_and_default",
|
|
156
|
+
"no_prefix"
|
|
157
|
+
]).optional(),
|
|
158
|
+
redirectRoot: z.boolean().optional(),
|
|
159
|
+
propagate: z.union([z.literal("header"), z.literal(false)], { error: "must be \"header\" or false" }).optional(),
|
|
160
|
+
switch: z.enum(["in-place", "navigate"]).optional(),
|
|
161
|
+
cookie: z.strictObject({
|
|
162
|
+
name: z.string().regex(/^[!#$%&'*+\-.^`|~\w]+$/, { error: "must be a valid cookie name" }).refine((name) => name !== TIME_ZONE_COOKIE, { error: `${JSON.stringify(TIME_ZONE_COOKIE)} is reserved for the time-zone cookie` }).optional(),
|
|
163
|
+
maxAge: positiveInt.optional()
|
|
164
|
+
}).optional(),
|
|
165
|
+
base: z.string().regex(/^\/(?!\/)/, { error: "must be a path starting with \"/\"" }).nullable().optional(),
|
|
166
|
+
paths: z.record(z.string(), z.record(z.string(), z.string())).optional(),
|
|
167
|
+
domains: z.record(z.string(), z.string()).optional()
|
|
168
|
+
}).optional(),
|
|
169
|
+
ssr: z.strictObject({
|
|
170
|
+
timeZone: z.string().superRefine((value, ctx) => {
|
|
171
|
+
const problem = timeZoneProblem(value);
|
|
172
|
+
if (problem !== null) ctx.addIssue({
|
|
173
|
+
code: "custom",
|
|
174
|
+
message: problem
|
|
175
|
+
});
|
|
176
|
+
}).optional(),
|
|
177
|
+
timeZoneFrom: z.record(routePattern, z.enum(["config", "request"])).optional(),
|
|
178
|
+
payload: z.enum(["bundles", "preload"]).optional(),
|
|
179
|
+
catalogs: z.enum([
|
|
180
|
+
"auto",
|
|
181
|
+
"modules",
|
|
182
|
+
"assets"
|
|
183
|
+
]).optional(),
|
|
184
|
+
maxLoadedLocales: positiveInt.optional(),
|
|
185
|
+
normalizeSpaces: z.boolean().optional()
|
|
186
|
+
}).optional(),
|
|
187
|
+
ai: z.strictObject({
|
|
188
|
+
provider: z.custom((value) => value === "gemini" || isProvider(value), { error: "must be \"gemini\" or a provider object with name, model and translate()" }).optional(),
|
|
189
|
+
model: text.optional(),
|
|
190
|
+
exclude: z.array(z.string()).optional(),
|
|
191
|
+
glossary: z.record(text, z.record(z.string(), text)).optional(),
|
|
192
|
+
doNotTranslate: z.array(text).optional(),
|
|
193
|
+
tone: z.record(z.string(), z.string()).optional(),
|
|
194
|
+
description: z.string().optional(),
|
|
195
|
+
apiKeyEnv: z.string().regex(/^[A-Za-z_]\w*$/, { error: "must be an environment variable name" }).optional(),
|
|
196
|
+
batchSize: z.number({ error: BATCH_SIZE_RANGE }).int({ error: BATCH_SIZE_RANGE }).min(40, { error: BATCH_SIZE_RANGE }).max(80, { error: BATCH_SIZE_RANGE }).optional(),
|
|
197
|
+
concurrency: positiveInt.optional(),
|
|
198
|
+
dev: z.boolean().optional()
|
|
199
|
+
}).optional(),
|
|
200
|
+
defaults: z.strictObject({ tags: z.custom((tags) => typeof tags !== "object" || tags === null || !Object.hasOwn(tags, "__proto__"), { error: "\"__proto__\" is not a message tag name" }).pipe(z.record(z.string().regex(TAG_NAME, { error: "a message tag name (letters, digits, _ and -)" }), z.union([element, z.strictObject({
|
|
201
|
+
element,
|
|
202
|
+
attributes: z.record(z.string().regex(ATTRIBUTE_NAME, { error: "an HTML attribute name" }), z.string()).optional()
|
|
203
|
+
})]))).optional() }).optional(),
|
|
204
|
+
devtools: z.boolean().optional(),
|
|
205
|
+
strict: z.union([z.boolean(), z.strictObject({
|
|
206
|
+
missing: level.optional(),
|
|
207
|
+
invalid: level.optional(),
|
|
208
|
+
moduleScope: level.optional()
|
|
209
|
+
})], { error: "must be a boolean or { missing, invalid, moduleScope }" }).optional()
|
|
210
|
+
});
|
|
211
|
+
/** `a.b["pt-BR"][0]`: the key path as a user would write it. */
|
|
212
|
+
function formatPath(path) {
|
|
213
|
+
let out = "";
|
|
214
|
+
for (const key of path) if (typeof key === "number") out += `[${key}]`;
|
|
215
|
+
else if (typeof key === "string" && /^[A-Za-z_$][\w$]*$/.test(key)) out += out === "" ? key : `.${key}`;
|
|
216
|
+
else out += `[${JSON.stringify(String(key))}]`;
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* One `<key path>: <problem>` line per zod issue, nested union and key
|
|
221
|
+
* issues flattened. `label` names the object the paths start from.
|
|
222
|
+
*/
|
|
223
|
+
function issueLines(issues, prefix = [], label = "") {
|
|
224
|
+
const lines = [];
|
|
225
|
+
const line = (path, message) => {
|
|
226
|
+
const key = formatPath(path);
|
|
227
|
+
const at = label === "" ? key || "config" : key === "" ? label : `${label}${key.startsWith("[") ? "" : "."}${key}`;
|
|
228
|
+
lines.push(`${at}: ${message}`);
|
|
229
|
+
};
|
|
230
|
+
for (const issue of issues) {
|
|
231
|
+
const path = [...prefix, ...issue.path];
|
|
232
|
+
switch (issue.code) {
|
|
233
|
+
case "unrecognized_keys":
|
|
234
|
+
for (const key of issue.keys) line([...path, key], "unknown key");
|
|
235
|
+
break;
|
|
236
|
+
case "invalid_key":
|
|
237
|
+
lines.push(...issueLines(issue.issues, path, label));
|
|
238
|
+
break;
|
|
239
|
+
case "invalid_union": {
|
|
240
|
+
const deep = issue.errors.filter((branch) => branch.some((inner) => inner.path.length > 0));
|
|
241
|
+
if (deep.length === 1) lines.push(...issueLines(deep[0], path, label));
|
|
242
|
+
else line(path, issue.message);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
default: line(path, issue.message.replace(/^Invalid (?:input|option): /, ""));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return lines;
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/core/config/routing.ts
|
|
252
|
+
/** Segments of a validated path as a pathname spells them (`*` and `**` stay). */
|
|
253
|
+
const segmentsOf = (path) => new URL(path, "http://h").pathname.split("/").slice(1);
|
|
254
|
+
/**
|
|
255
|
+
* What is wrong with a `routing.paths` key or value, or null. A route path
|
|
256
|
+
* as routers spell it: from the root, no empty, `.` or `..` segment, no
|
|
257
|
+
* query, fragment or backslash, no trailing slash; `*` stands for one whole
|
|
258
|
+
* segment and a final `**` for any number, and one segment at least is fixed.
|
|
259
|
+
*/
|
|
260
|
+
function pathProblem(path) {
|
|
261
|
+
if (!path.startsWith("/")) return "must start with /";
|
|
262
|
+
if (path === "/") return "the root is not translated";
|
|
263
|
+
if (/[?#\\]/.test(path)) return "must be a path without ?, # or \\";
|
|
264
|
+
const segments = path.slice(1).split("/");
|
|
265
|
+
for (const [i, s] of segments.entries()) {
|
|
266
|
+
if (s === "") return "must not have empty segments or a trailing slash";
|
|
267
|
+
if (s === "." || s === "..") return "must not have . or .. segments";
|
|
268
|
+
if (s.includes("*") && s !== "*" && s !== "**") return "a wildcard is a whole segment: * or **";
|
|
269
|
+
if (s === "**" && i !== segments.length - 1) return "** must be the last segment";
|
|
270
|
+
}
|
|
271
|
+
if (segments.every((s) => s.startsWith("*"))) return "must have a segment that is not a wildcard";
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
/** The wildcard sequence of a path: what a translation must keep, in order. */
|
|
275
|
+
const wildcards = (path) => path.split("/").filter((s) => s === "*" || s === "**").join("/");
|
|
276
|
+
/** Whether some pathname matches both patterns. */
|
|
277
|
+
function overlap(a, b) {
|
|
278
|
+
for (let i = 0;; i++) {
|
|
279
|
+
const x = a[i];
|
|
280
|
+
const y = b[i];
|
|
281
|
+
if (x === "**" || y === "**") return true;
|
|
282
|
+
if (x === void 0 || y === void 0) return x === y;
|
|
283
|
+
if (x !== y && x !== "*" && y !== "*") return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** Host and port of a `routing.domains` key in origin form, or null when it is no plain origin. */
|
|
287
|
+
function domainOrigin(key) {
|
|
288
|
+
const withScheme = key.includes("://") ? key : "https://" + key;
|
|
289
|
+
let url;
|
|
290
|
+
try {
|
|
291
|
+
url = new URL(withScheme);
|
|
292
|
+
} catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
296
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/") return null;
|
|
297
|
+
if (withScheme.replace(/^[a-z]+:\/\//i, "").includes("/")) return null;
|
|
298
|
+
return url.origin;
|
|
299
|
+
}
|
|
300
|
+
/** `routing.domains` with every key as `https://host[:port]` (or its `http:` origin). */
|
|
301
|
+
function resolveDomains(domains) {
|
|
302
|
+
const out = {};
|
|
303
|
+
for (const [key, locale] of Object.entries(domains)) out[domainOrigin(key)] = locale;
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Rules between `routing.paths`, `routing.domains` and the other keys. Only
|
|
308
|
+
* configured locales (never pseudo-locales, which are not advertised) may
|
|
309
|
+
* have a path or a host.
|
|
310
|
+
*/
|
|
311
|
+
function routingProblems(routing, sourceLocale, locales) {
|
|
312
|
+
const problems = [];
|
|
313
|
+
const url = routing?.url ?? "prefix_except_default";
|
|
314
|
+
const paths = routing?.paths ?? {};
|
|
315
|
+
const domains = routing?.domains ?? {};
|
|
316
|
+
const hasDomains = Object.keys(domains).length > 0;
|
|
317
|
+
const at = (...keys) => formatPath(["routing", ...keys]);
|
|
318
|
+
if (hasDomains && url !== "prefix_except_default") problems.push(`${at("domains")}: needs routing.url "prefix_except_default" (a host's locale is its unprefixed one, every other host's prefix is redirected to that host)`);
|
|
319
|
+
const seen = /* @__PURE__ */ new Map();
|
|
320
|
+
for (const [key, locale] of Object.entries(domains)) {
|
|
321
|
+
const origin = domainOrigin(key);
|
|
322
|
+
if (origin === null) {
|
|
323
|
+
problems.push(`${at("domains", key)}: must be a host such as "example.de" or an origin such as "http://localhost:5173"`);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const other = seen.get(origin);
|
|
327
|
+
if (other !== void 0) problems.push(`${at("domains", key)}: the same host as "${other}"`);
|
|
328
|
+
seen.set(origin, key);
|
|
329
|
+
if (!locales.includes(locale)) problems.push(`${at("domains", key)}: ${JSON.stringify(locale)} is not in locales`);
|
|
330
|
+
}
|
|
331
|
+
if (hasDomains) {
|
|
332
|
+
const served = new Set(Object.values(domains));
|
|
333
|
+
for (const l of locales) if (!served.has(l)) problems.push(`${at("domains")}: no host serves ${JSON.stringify(l)} (every locale needs one)`);
|
|
334
|
+
}
|
|
335
|
+
const keys = Object.keys(paths);
|
|
336
|
+
if (keys.length > 0 && url === "no_prefix") problems.push(`${at("paths")}: needs URLs that name their locale (a routing.url other than "no_prefix")`);
|
|
337
|
+
const valid = [];
|
|
338
|
+
for (const key of keys) {
|
|
339
|
+
const problem = pathProblem(key);
|
|
340
|
+
if (problem !== null) {
|
|
341
|
+
problems.push(`${at("paths", key)}: ${problem}`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
let ok = true;
|
|
345
|
+
for (const [locale, value] of Object.entries(paths[key])) {
|
|
346
|
+
const where = at("paths", key, locale);
|
|
347
|
+
if (!locales.includes(locale)) {
|
|
348
|
+
problems.push(`${where}: ${JSON.stringify(locale)} is not in locales`);
|
|
349
|
+
ok = false;
|
|
350
|
+
}
|
|
351
|
+
const p = pathProblem(value);
|
|
352
|
+
if (p !== null) {
|
|
353
|
+
problems.push(`${where}: ${p}`);
|
|
354
|
+
ok = false;
|
|
355
|
+
} else if (wildcards(value) !== wildcards(key)) {
|
|
356
|
+
problems.push(`${where}: must keep the wildcards of ${JSON.stringify(key)}, in order`);
|
|
357
|
+
ok = false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (ok) valid.push(key);
|
|
361
|
+
}
|
|
362
|
+
const form = (key, l) => Object.hasOwn(paths[key], l) ? paths[key][l] : key;
|
|
363
|
+
const seg = new Map(valid.map((k) => [k, segmentsOf(k)]));
|
|
364
|
+
const segOf = (p) => seg.get(p) ?? segmentsOf(p);
|
|
365
|
+
const unprefixed = url === "prefix" || url === "no_prefix" ? [] : hasDomains ? locales : [sourceLocale];
|
|
366
|
+
for (const l of locales) {
|
|
367
|
+
for (let i = 0; i < valid.length; i++) {
|
|
368
|
+
const a = valid[i];
|
|
369
|
+
const fa = form(a, l);
|
|
370
|
+
for (let j = i + 1; j < valid.length; j++) {
|
|
371
|
+
const b = valid[j];
|
|
372
|
+
const fb = form(b, l);
|
|
373
|
+
const where = at("paths", Object.hasOwn(paths[b], l) ? b : a, l);
|
|
374
|
+
if (fa === fb) problems.push(`${where}: ${JSON.stringify(a)} and ${JSON.stringify(b)} share the path ${JSON.stringify(fa)} in ${l}`);
|
|
375
|
+
else if (overlap(segOf(fa), segOf(fb)) && !overlap(segOf(a), segOf(b))) problems.push(`${where}: ${JSON.stringify(fa)} and ${JSON.stringify(fb)} overlap in ${l}, but ${JSON.stringify(a)} and ${JSON.stringify(b)} do not`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (!unprefixed.includes(l)) continue;
|
|
379
|
+
for (const a of valid) for (const b of valid) {
|
|
380
|
+
if (a === b || !Object.hasOwn(paths[b], l)) continue;
|
|
381
|
+
if (overlap(segOf(a), segOf(paths[b][l]))) problems.push(`${at("paths", b, l)}: ${JSON.stringify(paths[b][l])} would read as the route ${JSON.stringify(a)} where ${l} is unprefixed`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return problems;
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/core/config/load.ts
|
|
388
|
+
/**
|
|
389
|
+
* Finding, loading and resolving `vi18n.config.ts` (PLAN §5.7, INTERNALS
|
|
390
|
+
* §2.7.1, spike S8). The plugin and the CLI both come through `loadConfig`,
|
|
391
|
+
* so they can never disagree about a project's config.
|
|
392
|
+
*/
|
|
393
|
+
const JSON_CATALOG_PATH = "locales/{locale}.json";
|
|
394
|
+
/**
|
|
395
|
+
* Walks up from `from` to the workspace root (inclusive) and returns the
|
|
396
|
+
* first `vi18n.config.ts`. Stopping at the workspace root keeps a project
|
|
397
|
+
* from picking up a config that belongs to an unrelated checkout above it.
|
|
398
|
+
*/
|
|
399
|
+
async function findConfigFile(from) {
|
|
400
|
+
let dir = resolve(from);
|
|
401
|
+
for (;;) {
|
|
402
|
+
const candidate = join(dir, CONFIG_FILE);
|
|
403
|
+
if (await isFile(candidate)) return candidate;
|
|
404
|
+
if (await isWorkspaceRoot(dir)) return null;
|
|
405
|
+
const parent = dirname(dir);
|
|
406
|
+
if (parent === dir) return null;
|
|
407
|
+
dir = parent;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function isWorkspaceRoot(dir) {
|
|
411
|
+
if (await exists(join(dir, ".git"))) return true;
|
|
412
|
+
if (await isFile(join(dir, "pnpm-workspace.yaml"))) return true;
|
|
413
|
+
let manifest;
|
|
414
|
+
try {
|
|
415
|
+
manifest = JSON.parse((await readFile(join(dir, "package.json"), "utf8")).replace(/^\uFEFF/, ""));
|
|
416
|
+
} catch {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
return typeof manifest === "object" && manifest !== null && manifest.workspaces != null;
|
|
420
|
+
}
|
|
421
|
+
async function exists(file) {
|
|
422
|
+
try {
|
|
423
|
+
await stat(file);
|
|
424
|
+
return true;
|
|
425
|
+
} catch {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function isFile(file) {
|
|
430
|
+
try {
|
|
431
|
+
return (await stat(file)).isFile();
|
|
432
|
+
} catch {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function loadConfig(options) {
|
|
437
|
+
const cwd = resolve(options.cwd);
|
|
438
|
+
let file;
|
|
439
|
+
if (options.configFile != null) {
|
|
440
|
+
file = resolve(cwd, options.configFile);
|
|
441
|
+
if (!await isFile(file)) throw diagnosticError({
|
|
442
|
+
code: "config-not-found",
|
|
443
|
+
severity: "error",
|
|
444
|
+
message: `Config file ${file} not found`,
|
|
445
|
+
file: toPosix(file)
|
|
446
|
+
});
|
|
447
|
+
if (!/\.m?ts$/.test(file)) throw invalid(basename(file), `${basename(file)}: only a TypeScript module (vi18n.config.ts) is supported as a config`);
|
|
448
|
+
} else file = await findConfigFile(cwd);
|
|
449
|
+
const inline = options.output === void 0 ? void 0 : parseInlineOutput(options.output);
|
|
450
|
+
if (file === null) return resolveWith({}, cwd, null, inline);
|
|
451
|
+
return resolveWith(await importConfig(file), dirname(file), file, inline);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Error codes of a native `import()` that a Vite transform can fix:
|
|
455
|
+
* non-erasable TypeScript, extensionless or aliased specifiers, `.ts` inside
|
|
456
|
+
* `node_modules`, JSON without an import attribute. Anything else was thrown
|
|
457
|
+
* by the config's own code and would only be thrown again.
|
|
458
|
+
*/
|
|
459
|
+
const TRANSFORMABLE = /* @__PURE__ */ new Set([
|
|
460
|
+
"ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX",
|
|
461
|
+
"ERR_INVALID_TYPESCRIPT_SYNTAX",
|
|
462
|
+
"ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING",
|
|
463
|
+
"ERR_UNKNOWN_FILE_EXTENSION",
|
|
464
|
+
"ERR_MODULE_NOT_FOUND",
|
|
465
|
+
"ERR_UNSUPPORTED_DIR_IMPORT",
|
|
466
|
+
"ERR_PACKAGE_IMPORT_NOT_DEFINED",
|
|
467
|
+
"ERR_IMPORT_ATTRIBUTE_MISSING",
|
|
468
|
+
"ERR_IMPORT_ASSERTION_TYPE_MISSING"
|
|
469
|
+
]);
|
|
470
|
+
function needsTransform(error) {
|
|
471
|
+
if (!(error instanceof Error) || thrownByEvaluatedCode(error)) return false;
|
|
472
|
+
const code = error.code;
|
|
473
|
+
if (typeof code === "string") return TRANSFORMABLE.has(code);
|
|
474
|
+
return error.name === "SyntaxError";
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Parse, resolution and link errors are raised by Node's loader before any
|
|
478
|
+
* module body runs, so all their synchronous frames are `node:` internals.
|
|
479
|
+
* An error with a synchronous frame in a `file:` module was thrown while the
|
|
480
|
+
* config or one of its imports ran: retrying would run their side effects a
|
|
481
|
+
* second time and throw again (a runtime `JSON.parse` SyntaxError, say).
|
|
482
|
+
* `at async` frames are skipped because they are callers such as this loader.
|
|
483
|
+
*/
|
|
484
|
+
function thrownByEvaluatedCode(error) {
|
|
485
|
+
return /^\s*at (?!async )(?:.*\()?file:\/\//m.test(error.stack ?? "");
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Configs this process has already imported natively. Node caches ESM by URL
|
|
489
|
+
* for the life of the process and the `?t=` query busts only the config
|
|
490
|
+
* itself, so a reload would see stale versions of the modules it imports;
|
|
491
|
+
* the runner evaluates the whole graph afresh, so reloads go through it.
|
|
492
|
+
*/
|
|
493
|
+
const importedNatively = /* @__PURE__ */ new Set();
|
|
494
|
+
async function importConfig(file) {
|
|
495
|
+
const rel = basename(file);
|
|
496
|
+
let module;
|
|
497
|
+
const vite = importedNatively.has(file) ? await loadVite() : null;
|
|
498
|
+
if (vite !== null) module = await runnerImport(vite, file, rel);
|
|
499
|
+
else {
|
|
500
|
+
importedNatively.add(file);
|
|
501
|
+
try {
|
|
502
|
+
const { mtimeMs } = await stat(file);
|
|
503
|
+
module = await import(pathToFileURL(file).href + "?t=" + Math.trunc(mtimeMs * 1e3));
|
|
504
|
+
} catch (nativeError) {
|
|
505
|
+
if (!needsTransform(nativeError)) throw loadError(rel, nativeError);
|
|
506
|
+
const fallback = await loadVite();
|
|
507
|
+
if (fallback === null) throw invalid(rel, `${rel} needs a transform Node cannot apply (${firstLine(nativeError)}); install vite or use erasable TypeScript only`);
|
|
508
|
+
module = await runnerImport(fallback, file, rel);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (!("default" in module) || module.default === void 0) throw invalid(rel, `${rel} has no default export; export default defineConfig({ … })`);
|
|
512
|
+
try {
|
|
513
|
+
return await (typeof module.default === "function" ? module.default() : module.default);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
throw loadError(rel, error);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async function loadVite() {
|
|
519
|
+
try {
|
|
520
|
+
return await import("vite");
|
|
521
|
+
} catch {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async function runnerImport(vite, file, rel) {
|
|
526
|
+
try {
|
|
527
|
+
const { module } = await vite.runnerImport(file, {
|
|
528
|
+
root: dirname(file),
|
|
529
|
+
logLevel: "silent"
|
|
530
|
+
});
|
|
531
|
+
return module;
|
|
532
|
+
} catch (error) {
|
|
533
|
+
throw loadError(rel, error);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function loadError(rel, cause) {
|
|
537
|
+
const error = invalid(rel, `${rel} could not be loaded: ${firstLine(cause)}`);
|
|
538
|
+
error.cause = cause;
|
|
539
|
+
return error;
|
|
540
|
+
}
|
|
541
|
+
function firstLine(error) {
|
|
542
|
+
return (error instanceof Error ? error.message : String(error)).split("\n", 1)[0].replace(/\.$/, "");
|
|
543
|
+
}
|
|
544
|
+
function invalid(file, message) {
|
|
545
|
+
return diagnosticError({
|
|
546
|
+
code: "config-invalid",
|
|
547
|
+
severity: "error",
|
|
548
|
+
message,
|
|
549
|
+
...file === void 0 ? {} : { file }
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function parseInlineOutput(output) {
|
|
553
|
+
const result = outputSchema.safeParse(output);
|
|
554
|
+
if (result.success) return result.data;
|
|
555
|
+
throw invalid(void 0, issueLines(result.error.issues, [], "vi18n({ output })").join("; "));
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* `inline` over the file's already validated `output`, `split` merged key by
|
|
559
|
+
* key. Merging after validation means an inline value can never hide an
|
|
560
|
+
* invalid one in the file, which the CLI (no inline options) would reject.
|
|
561
|
+
*/
|
|
562
|
+
function withOutput(file, inline) {
|
|
563
|
+
const merged = mergeDefined(file ?? {}, inline);
|
|
564
|
+
if (file?.split !== void 0 && inline.split !== void 0) merged.split = mergeDefined(file.split, inline.split);
|
|
565
|
+
return merged;
|
|
566
|
+
}
|
|
567
|
+
function mergeDefined(base, over) {
|
|
568
|
+
const out = { ...base };
|
|
569
|
+
for (const key of Object.keys(over)) if (over[key] !== void 0) out[key] = over[key];
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Validates `input` (the config's default export, already awaited) and
|
|
574
|
+
* applies every default. Throws `config-invalid` naming each bad key path.
|
|
575
|
+
*/
|
|
576
|
+
function resolveConfig(input, root, configFile) {
|
|
577
|
+
return resolveWith(input, root, configFile, void 0);
|
|
578
|
+
}
|
|
579
|
+
function resolveWith(input, root, configFile, inline) {
|
|
580
|
+
root = resolve(root);
|
|
581
|
+
const file = configFile === null ? void 0 : toPosix(relativeOrAbsolute(root, configFile));
|
|
582
|
+
const result = configSchema.safeParse(input);
|
|
583
|
+
if (!result.success) throw invalid(file, issueLines(result.error.issues).join("; "));
|
|
584
|
+
const user = inline === void 0 ? result.data : {
|
|
585
|
+
...result.data,
|
|
586
|
+
output: withOutput(result.data.output, inline)
|
|
587
|
+
};
|
|
588
|
+
const sourceLocale = user.sourceLocale ?? DEFAULT_CONFIG.sourceLocale;
|
|
589
|
+
const locales = user.locales ? [...user.locales] : [sourceLocale];
|
|
590
|
+
const problems = crossFieldProblems(user, sourceLocale, locales);
|
|
591
|
+
if (problems.length > 0) throw invalid(file, problems.join("; "));
|
|
592
|
+
const d = DEFAULT_CONFIG;
|
|
593
|
+
const format = user.catalogs?.format ?? d.catalogs.format;
|
|
594
|
+
const catalogPath = user.catalogs?.path ?? (format === "json" ? JSON_CATALOG_PATH : d.catalogs.path);
|
|
595
|
+
const provider = user.ai?.provider ?? d.ai.provider;
|
|
596
|
+
return {
|
|
597
|
+
root,
|
|
598
|
+
configFile: configFile === null ? null : resolve(configFile),
|
|
599
|
+
sourceLocale,
|
|
600
|
+
locales,
|
|
601
|
+
pseudoLocales: [...new Set(user.pseudoLocales ?? d.pseudoLocales)],
|
|
602
|
+
fallbacks: copyRecord(user.fallbacks ?? d.fallbacks, (chain) => [...chain]),
|
|
603
|
+
project: {
|
|
604
|
+
include: (user.project?.include ?? d.project.include).map((entry) => resolve(root, entry)),
|
|
605
|
+
extensions: [...user.project?.extensions ?? d.project.extensions],
|
|
606
|
+
exclude: [...user.project?.exclude ?? d.project.exclude]
|
|
607
|
+
},
|
|
608
|
+
extraction: {
|
|
609
|
+
importSources: [.../* @__PURE__ */ new Set([...DEFAULT_IMPORT_SOURCES, ...user.extraction?.importSources ?? []])],
|
|
610
|
+
aliases: [...user.extraction?.aliases ?? d.extraction.aliases],
|
|
611
|
+
globals: [...user.extraction?.globals ?? d.extraction.globals],
|
|
612
|
+
keep: [...user.extraction?.keep ?? d.extraction.keep],
|
|
613
|
+
dependencies: [...user.extraction?.dependencies ?? d.extraction.dependencies]
|
|
614
|
+
},
|
|
615
|
+
ids: { length: user.ids?.length ?? d.ids.length },
|
|
616
|
+
catalogs: {
|
|
617
|
+
format,
|
|
618
|
+
path: resolve(root, catalogPath),
|
|
619
|
+
msgid: user.catalogs?.msgid ?? d.catalogs.msgid,
|
|
620
|
+
icuFlag: user.catalogs?.icuFlag ?? d.catalogs.icuFlag
|
|
621
|
+
},
|
|
622
|
+
output: resolveOutput(user.output),
|
|
623
|
+
routing: {
|
|
624
|
+
strategy: [...user.routing?.strategy ?? d.routing.strategy],
|
|
625
|
+
routeStrategies: copyRecord(user.routing?.routeStrategies ?? d.routing.routeStrategies, (chain) => [...chain]),
|
|
626
|
+
url: user.routing?.url ?? d.routing.url,
|
|
627
|
+
redirectRoot: user.routing?.redirectRoot ?? d.routing.redirectRoot,
|
|
628
|
+
propagate: user.routing?.propagate ?? d.routing.propagate,
|
|
629
|
+
switch: user.routing?.switch ?? d.routing.switch,
|
|
630
|
+
cookie: {
|
|
631
|
+
name: user.routing?.cookie?.name ?? d.routing.cookie.name,
|
|
632
|
+
maxAge: user.routing?.cookie?.maxAge ?? d.routing.cookie.maxAge
|
|
633
|
+
},
|
|
634
|
+
base: user.routing?.base ?? d.routing.base,
|
|
635
|
+
paths: copyRecord(user.routing?.paths ?? d.routing.paths, (entry) => ({ ...entry })),
|
|
636
|
+
domains: resolveDomains(user.routing?.domains ?? d.routing.domains)
|
|
637
|
+
},
|
|
638
|
+
ssr: {
|
|
639
|
+
timeZone: user.ssr?.timeZone ?? d.ssr.timeZone,
|
|
640
|
+
timeZoneFrom: copyRecord(user.ssr?.timeZoneFrom ?? d.ssr.timeZoneFrom, (v) => v),
|
|
641
|
+
payload: user.ssr?.payload ?? d.ssr.payload,
|
|
642
|
+
catalogs: user.ssr?.catalogs ?? d.ssr.catalogs,
|
|
643
|
+
maxLoadedLocales: user.ssr?.maxLoadedLocales ?? d.ssr.maxLoadedLocales,
|
|
644
|
+
normalizeSpaces: user.ssr?.normalizeSpaces ?? d.ssr.normalizeSpaces
|
|
645
|
+
},
|
|
646
|
+
ai: {
|
|
647
|
+
provider,
|
|
648
|
+
model: user.ai?.model ?? (typeof provider === "string" ? d.ai.model : provider.model),
|
|
649
|
+
exclude: [...user.ai?.exclude ?? d.ai.exclude],
|
|
650
|
+
glossary: copyRecord(user.ai?.glossary ?? d.ai.glossary, (targets) => copyRecord(targets, (v) => v)),
|
|
651
|
+
doNotTranslate: [...user.ai?.doNotTranslate ?? d.ai.doNotTranslate],
|
|
652
|
+
tone: copyRecord(user.ai?.tone ?? d.ai.tone, (v) => v),
|
|
653
|
+
description: user.ai?.description ?? d.ai.description,
|
|
654
|
+
apiKeyEnv: user.ai?.apiKeyEnv ?? d.ai.apiKeyEnv,
|
|
655
|
+
batchSize: user.ai?.batchSize ?? d.ai.batchSize,
|
|
656
|
+
concurrency: user.ai?.concurrency ?? d.ai.concurrency,
|
|
657
|
+
dev: user.ai?.dev ?? d.ai.dev
|
|
658
|
+
},
|
|
659
|
+
defaults: { tags: copyRecord(user.defaults?.tags ?? d.defaults.tags, (spec) => typeof spec === "string" ? spec : {
|
|
660
|
+
element: spec.element,
|
|
661
|
+
attributes: { ...spec.attributes }
|
|
662
|
+
}) },
|
|
663
|
+
devtools: user.devtools ?? d.devtools,
|
|
664
|
+
strict: resolveStrict(user.strict)
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
/** Rules between keys, checked once the locale list is known. */
|
|
668
|
+
function crossFieldProblems(user, sourceLocale, locales) {
|
|
669
|
+
const problems = [];
|
|
670
|
+
const configured = /* @__PURE__ */ new Set();
|
|
671
|
+
locales.forEach((tag, index) => {
|
|
672
|
+
if (configured.has(tag)) problems.push(`locales[${index}]: ${JSON.stringify(tag)} is listed twice`);
|
|
673
|
+
configured.add(tag);
|
|
674
|
+
});
|
|
675
|
+
if (!configured.has(sourceLocale)) problems.push(`sourceLocale: ${JSON.stringify(sourceLocale)} is not in locales (add it to locales)`);
|
|
676
|
+
for (const [tag, chain] of Object.entries(user.fallbacks ?? {})) chain.forEach((target, index) => {
|
|
677
|
+
if (!configured.has(target)) problems.push(`${formatPath([
|
|
678
|
+
"fallbacks",
|
|
679
|
+
tag,
|
|
680
|
+
index
|
|
681
|
+
])}: ${JSON.stringify(target)} is not in locales`);
|
|
682
|
+
});
|
|
683
|
+
(user.pseudoLocales ?? []).forEach((tag, index) => {
|
|
684
|
+
if (configured.has(tag)) problems.push(`pseudoLocales[${index}]: ${JSON.stringify(tag)} is also in locales`);
|
|
685
|
+
});
|
|
686
|
+
const idLength = user.ids?.length ?? DEFAULT_CONFIG.ids.length;
|
|
687
|
+
(user.ai?.exclude ?? []).forEach((entry, index) => {
|
|
688
|
+
if (excludeKind(entry, locales, idLength) === null) problems.push(`ai.exclude[${index}]: ${JSON.stringify(entry)} is not a configured locale, a message id or a file glob (a glob contains / or *)`);
|
|
689
|
+
});
|
|
690
|
+
problems.push(...routingProblems(user.routing, sourceLocale, locales));
|
|
691
|
+
const languages = new Set(locales.map((tag) => tag.split("-")[0]));
|
|
692
|
+
for (const [term, targets] of Object.entries(user.ai?.glossary ?? {})) for (const tag of Object.keys(targets)) if (!configured.has(tag) && !languages.has(tag)) problems.push(`${formatPath([
|
|
693
|
+
"ai",
|
|
694
|
+
"glossary",
|
|
695
|
+
term,
|
|
696
|
+
tag
|
|
697
|
+
])}: ${JSON.stringify(tag)} is not in locales (nor the language of one)`);
|
|
698
|
+
return problems;
|
|
699
|
+
}
|
|
700
|
+
function resolveOutput(user) {
|
|
701
|
+
const d = DEFAULT_CONFIG.output;
|
|
702
|
+
return {
|
|
703
|
+
split: {
|
|
704
|
+
shared: user?.split?.shared ?? d.split.shared,
|
|
705
|
+
dupBelow: user?.split?.dupBelow ?? d.split.dupBelow,
|
|
706
|
+
minBytes: user?.split?.minBytes ?? d.split.minBytes,
|
|
707
|
+
maxBundlesPerRoot: user?.split?.maxBundlesPerRoot === void 0 ? d.split.maxBundlesPerRoot : user.split.maxBundlesPerRoot
|
|
708
|
+
},
|
|
709
|
+
guard: user?.guard ?? d.guard,
|
|
710
|
+
manifestPath: user?.manifestPath ?? d.manifestPath,
|
|
711
|
+
manifest: user?.manifest ?? d.manifest,
|
|
712
|
+
baseEntries: [...user?.baseEntries ?? d.baseEntries],
|
|
713
|
+
compactIds: user?.compactIds ?? d.compactIds,
|
|
714
|
+
perLocale: user?.perLocale ?? d.perLocale
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function resolveStrict(strict) {
|
|
718
|
+
const d = DEFAULT_CONFIG.strict;
|
|
719
|
+
if (strict === true) return {
|
|
720
|
+
missing: "error",
|
|
721
|
+
invalid: "error",
|
|
722
|
+
moduleScope: "error"
|
|
723
|
+
};
|
|
724
|
+
if (strict === void 0 || strict === false) return { ...d };
|
|
725
|
+
const pick = (value, fallback) => value ?? fallback;
|
|
726
|
+
return {
|
|
727
|
+
missing: pick(strict.missing, d.missing),
|
|
728
|
+
invalid: pick(strict.invalid, d.invalid),
|
|
729
|
+
moduleScope: pick(strict.moduleScope, d.moduleScope)
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* A fresh copy, so resolved configs never share arrays with the input or the
|
|
734
|
+
* defaults. `fromEntries` defines properties, so a `__proto__` key stays a key.
|
|
735
|
+
*/
|
|
736
|
+
function copyRecord(record, copy) {
|
|
737
|
+
return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, copy(value)]));
|
|
738
|
+
}
|
|
739
|
+
function relativeOrAbsolute(root, file) {
|
|
740
|
+
const rel = relative(root, resolve(file));
|
|
741
|
+
return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? resolve(file) : rel;
|
|
742
|
+
}
|
|
743
|
+
function toPosix(path) {
|
|
744
|
+
return sep === "/" ? path : path.split(sep).join("/");
|
|
745
|
+
}
|
|
746
|
+
//#endregion
|
|
747
|
+
export { loadConfig as n, resolveConfig as r, findConfigFile as t };
|