@menapo/vi18n 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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-D34P-S--.js +1 -0
  6. package/dist/check-DyrAbv73.js +201 -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-B5Mz6WJm.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-B7p40M__.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-DGsMbcTZ.js +103 -0
  25. package/dist/extract-Dujlsgnk.js +298 -0
  26. package/dist/formats-KPIg-08V.js +781 -0
  27. package/dist/i18next-CrUnKFfh.js +1596 -0
  28. package/dist/i18next-Rgo-v9l0.js +1 -0
  29. package/dist/import-Cwdhcywt.js +225 -0
  30. package/dist/index.d.ts +156 -0
  31. package/dist/index.js +239 -0
  32. package/dist/init-BecCTvKi.js +792 -0
  33. package/dist/library-D5CWvTQs.js +1882 -0
  34. package/dist/lingui--PcOOYry.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-Cq1Nuu-Q.js +31 -0
  41. package/dist/match-BGr8EXB3.js +89 -0
  42. package/dist/mcp-Cf0T7qPW.js +28 -0
  43. package/dist/merge-driver-CpiiSVIc.js +422 -0
  44. package/dist/messages-Cxa-5tR0.js +166 -0
  45. package/dist/migrate-CntqvrW4.js +1 -0
  46. package/dist/migrate-dPgq9uzy.js +1484 -0
  47. package/dist/openai-CTaMDcl-.js +346 -0
  48. package/dist/output-DsaHKHEt.js +164 -0
  49. package/dist/parse.d.ts +62 -0
  50. package/dist/parse.js +156 -0
  51. package/dist/pipeline-C7g11THL.js +2031 -0
  52. package/dist/pipeline-wxS0SApG.js +1 -0
  53. package/dist/project-Dwx7iPm6.js +381 -0
  54. package/dist/project-pXipT8iG.js +1046 -0
  55. package/dist/pseudo-DRv30iN5.js +145 -0
  56. package/dist/raw-C4Y6vyGY.js +4708 -0
  57. package/dist/react-intl-CaBVwJ9u.js +519 -0
  58. package/dist/restore-Btq66Tid.js +175 -0
  59. package/dist/rewrite-CiTRAICl.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-DYD_VXKy.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-BDkykMjb.js +11823 -0
  68. package/dist/server-qv3mz70d.js +10485 -0
  69. package/dist/server.d.ts +169 -0
  70. package/dist/server.js +1234 -0
  71. package/dist/sitemap-DV3o8QD7.js +159 -0
  72. package/dist/sources-D19czDSA.js +2242 -0
  73. package/dist/status-DZArW5aE.js +149 -0
  74. package/dist/stdio-CVaJfxA7.js +29 -0
  75. package/dist/stdio-FPOKQAgu.js +95 -0
  76. package/dist/sync-CcVzger_.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-DZR3MkSA.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-BlyLGlOa.js +28 -0
  93. package/dist/vite-scan-B_YW4X_a.js +242 -0
  94. package/dist/vite.d.ts +34 -0
  95. package/dist/vite.js +7558 -0
  96. package/dist/vue-i18n-CEMnpv7k.js +651 -0
  97. package/dist/which-C6PFI1Dm.js +116 -0
  98. package/dist/worker-84U6uYB3.js +1656 -0
  99. package/package.json +97 -3
  100. 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 };