@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
package/dist/test.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { c as page, n as S, p as subs, r as changed, t as textDirection } from "./dir-zw6bGMKQ.js";
|
|
2
|
+
import { c as parseICU, i as normalizeSource, n as canonicalize, t as compileMessage } from "./compile-Cc97JaW2.js";
|
|
3
|
+
import { t as createScopedApi } from "./scoped-BMYpSthj.js";
|
|
4
|
+
import { t as pseudoLocalize } from "./pseudo-DRv30iN5.js";
|
|
5
|
+
//#region src/test/catalog.ts
|
|
6
|
+
/**
|
|
7
|
+
* `loadCatalog` (INTERNALS §3.8, §6 D194): one locale's translations read
|
|
8
|
+
* from the project's catalog files, as the build compiles them, for
|
|
9
|
+
* `createTestI18n({ catalog })`. The Node side loads on first use, so the
|
|
10
|
+
* `/test` entry itself stays loadable in browser test runners.
|
|
11
|
+
*/
|
|
12
|
+
const loads = /* @__PURE__ */ new Map();
|
|
13
|
+
/**
|
|
14
|
+
* The locale's translations, fallbacks applied; untranslated and unusable
|
|
15
|
+
* entries are left out, so those messages render their source text, as in
|
|
16
|
+
* `vite dev`. Memoised per project and locale for the test file.
|
|
17
|
+
*/
|
|
18
|
+
function loadCatalog(locale, options = {}) {
|
|
19
|
+
const project = S.registration?.project;
|
|
20
|
+
const root = options.root ?? project?.root ?? globalThis.process?.cwd() ?? ".";
|
|
21
|
+
const configFile = options.configFile ?? (options.root === void 0 ? project?.configFile : void 0) ?? null;
|
|
22
|
+
const key = JSON.stringify([
|
|
23
|
+
root,
|
|
24
|
+
configFile,
|
|
25
|
+
locale
|
|
26
|
+
]);
|
|
27
|
+
let load = loads.get(key);
|
|
28
|
+
if (load === void 0) {
|
|
29
|
+
load = import("./load-CQRz4PpM.js").then((m) => m.compileCatalog(locale, root, configFile));
|
|
30
|
+
loads.set(key, load);
|
|
31
|
+
load.catch(() => loads.delete(key));
|
|
32
|
+
}
|
|
33
|
+
return load;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/test/i18n.ts
|
|
37
|
+
/**
|
|
38
|
+
* `createTestI18n` (PLAN §5.22, INTERNALS §3.8): unit and component tests
|
|
39
|
+
* render in a locale, with real or hand-written translations, with message
|
|
40
|
+
* ids or pseudo text, and with a fixed time zone and `now`.
|
|
41
|
+
*
|
|
42
|
+
* It configures the page scope, the scope `t()` resolves in a test (under
|
|
43
|
+
* the plugin's test registration and without the plugin), rather than a
|
|
44
|
+
* scope of its own: components import the global `t`, and mounted ones
|
|
45
|
+
* re-render through their glue's trackers (§6 D190). The scope's `test` hook
|
|
46
|
+
* picks every message, because only it sees the descriptor (§6 D189).
|
|
47
|
+
*/
|
|
48
|
+
const DEFAULT_NOW = Date.UTC(2026, 0, 1);
|
|
49
|
+
const PSEUDO = /* @__PURE__ */ new Set(["en-XA", "ar-XB"]);
|
|
50
|
+
/** What the page held before the first `createTestI18n`; null while nothing is applied. */
|
|
51
|
+
let saved = null;
|
|
52
|
+
const html = () => typeof document === "object" && document?.documentElement ? document.documentElement : null;
|
|
53
|
+
function notify() {
|
|
54
|
+
changed(true);
|
|
55
|
+
for (const fn of subs) fn(page.locale);
|
|
56
|
+
}
|
|
57
|
+
/** Puts the page back as the first `createTestI18n` found it. */
|
|
58
|
+
function resetTestI18n() {
|
|
59
|
+
const s = saved;
|
|
60
|
+
if (s === null) return;
|
|
61
|
+
saved = null;
|
|
62
|
+
page.locale = s.locale;
|
|
63
|
+
page.messages = s.messages;
|
|
64
|
+
page.timeZone = s.timeZone;
|
|
65
|
+
page.now = s.now;
|
|
66
|
+
page.labels = s.labels;
|
|
67
|
+
delete page.test;
|
|
68
|
+
S.registration = s.registration;
|
|
69
|
+
S.locales.splice(0, S.locales.length, ...s.locales);
|
|
70
|
+
const h = html();
|
|
71
|
+
if (h && s.html) Object.assign(h, s.html);
|
|
72
|
+
notify();
|
|
73
|
+
}
|
|
74
|
+
/** A registration for suites the plugin does not run in, so `t()` and `/server` treat them as tests. */
|
|
75
|
+
function testRegistration(locale) {
|
|
76
|
+
const locales = [.../* @__PURE__ */ new Set(["en", locale])];
|
|
77
|
+
return {
|
|
78
|
+
mode: "dev",
|
|
79
|
+
consumer: "client",
|
|
80
|
+
config: {
|
|
81
|
+
sourceLocale: "en",
|
|
82
|
+
locales,
|
|
83
|
+
pseudoLocales: locales.filter((l) => PSEUDO.has(l)),
|
|
84
|
+
fallbacks: {},
|
|
85
|
+
routing: {
|
|
86
|
+
strategy: ["default"],
|
|
87
|
+
routeStrategies: {},
|
|
88
|
+
url: "no_prefix",
|
|
89
|
+
redirectRoot: false,
|
|
90
|
+
propagate: false,
|
|
91
|
+
switch: "in-place",
|
|
92
|
+
cookie: {
|
|
93
|
+
name: "vi18n_locale",
|
|
94
|
+
maxAge: 31536e3
|
|
95
|
+
},
|
|
96
|
+
base: "/"
|
|
97
|
+
},
|
|
98
|
+
ssr: {
|
|
99
|
+
timeZone: "UTC",
|
|
100
|
+
timeZoneFrom: {},
|
|
101
|
+
normalizeSpaces: true
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
test: true
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function memo(cache, key, make) {
|
|
108
|
+
let value = cache.get(key);
|
|
109
|
+
if (value === void 0) {
|
|
110
|
+
if (cache.size > 9999) cache.clear();
|
|
111
|
+
cache.set(key, value = make());
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
116
|
+
const sourceIr = /* @__PURE__ */ new Map();
|
|
117
|
+
const pseudoIr = /* @__PURE__ */ new Map();
|
|
118
|
+
const canonical = /* @__PURE__ */ new Map();
|
|
119
|
+
function parse(source) {
|
|
120
|
+
return memo(parsed, source, () => {
|
|
121
|
+
try {
|
|
122
|
+
return parseICU(normalizeSource(source));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw new Error(`vi18n: ${JSON.stringify(source)} is not a valid message: ${error.message}`, { cause: error });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const compiled = (source) => memo(sourceIr, source, () => compileMessage(parse(source)));
|
|
129
|
+
/** The text as a build's pseudo-locale renders it (`compileLocales` does the same). */
|
|
130
|
+
const pseudo = (source, locale) => memo(pseudoIr, locale + "\0" + source, () => compileMessage(pseudoLocalize(parse(source), locale)));
|
|
131
|
+
/** The canonical form, or the text itself when it does not parse (it then matches nothing). */
|
|
132
|
+
const canonicalOf = (source) => memo(canonical, source, () => {
|
|
133
|
+
try {
|
|
134
|
+
return canonicalize(source);
|
|
135
|
+
} catch {
|
|
136
|
+
return source;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
/**
|
|
140
|
+
* A generated id (INTERNALS §2.2.1): sha256 of the canonical text plus
|
|
141
|
+
* U+001F, base64url. Read through `getBuiltinModule` so the entry loads in
|
|
142
|
+
* browser test runners too; only this mode needs Node there.
|
|
143
|
+
*/
|
|
144
|
+
function generatedId(canonicalText, length) {
|
|
145
|
+
const crypto = globalThis.process?.getBuiltinModule?.("node:crypto");
|
|
146
|
+
if (crypto === void 0) throw new Error("vi18n: ids mode needs node:crypto for calls the plugin did not rewrite; run the test under the plugin or in Node");
|
|
147
|
+
return crypto.hash("sha256", canonicalText + "", "base64url").slice(0, length);
|
|
148
|
+
}
|
|
149
|
+
function check(condition, message) {
|
|
150
|
+
if (!condition) throw new RangeError("vi18n: " + message);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Renders every `t()` of the test in `options.locale` and `options.mode`
|
|
154
|
+
* until `reset()` (or the next `createTestI18n`, which replaces it). Without
|
|
155
|
+
* a call, tests under the plugin render the source locale as before.
|
|
156
|
+
*/
|
|
157
|
+
function createTestI18n(options = {}) {
|
|
158
|
+
const mode = options.mode ?? "source";
|
|
159
|
+
check([
|
|
160
|
+
"source",
|
|
161
|
+
"ids",
|
|
162
|
+
"pseudo"
|
|
163
|
+
].includes(mode), `unknown test mode ${JSON.stringify(mode)}`);
|
|
164
|
+
const timeZone = options.timeZone ?? "UTC";
|
|
165
|
+
new Intl.DateTimeFormat("en", { timeZone });
|
|
166
|
+
const now = options.now instanceof Date ? options.now.getTime() : options.now ?? DEFAULT_NOW;
|
|
167
|
+
check(Number.isFinite(now), "now must be a finite time");
|
|
168
|
+
const idLength = options.idLength ?? 12;
|
|
169
|
+
check(Number.isInteger(idLength) && idLength >= 8 && idLength <= 16, "idLength must be an integer from 8 to 16");
|
|
170
|
+
const catalog = options.catalog;
|
|
171
|
+
const requested = options.locale ?? catalog?.locale;
|
|
172
|
+
if (catalog && requested !== catalog.locale) throw new RangeError(`vi18n: the catalog is for ${catalog.locale}, not ${requested}`);
|
|
173
|
+
const own = /* @__PURE__ */ new Map();
|
|
174
|
+
for (const [key, text] of Object.entries(options.messages ?? {})) own.set(canonicalOf(key), compiled(text));
|
|
175
|
+
const base = saved ? saved.registration : S.registration;
|
|
176
|
+
const locale = requested ?? (mode === "pseudo" ? "en-XA" : base?.config.sourceLocale ?? "en");
|
|
177
|
+
Intl.getCanonicalLocales(locale);
|
|
178
|
+
check(base === null || base.config.locales.includes(locale) || PSEUDO.has(locale), `unknown locale ${locale} (${base?.config.locales.join(", ")})`);
|
|
179
|
+
const h = html();
|
|
180
|
+
saved ??= {
|
|
181
|
+
locale: page.locale,
|
|
182
|
+
messages: page.messages,
|
|
183
|
+
timeZone: page.timeZone,
|
|
184
|
+
now: page.now,
|
|
185
|
+
labels: page.labels,
|
|
186
|
+
registration: S.registration,
|
|
187
|
+
locales: [...S.locales],
|
|
188
|
+
html: h && {
|
|
189
|
+
lang: h.lang,
|
|
190
|
+
dir: h.dir
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
if (base === null) S.register(testRegistration(locale));
|
|
194
|
+
const style = locale === "ar-XB" ? "ar-XB" : "en-XA";
|
|
195
|
+
page.locale = locale;
|
|
196
|
+
page.messages = catalog?.messages ?? saved.messages;
|
|
197
|
+
page.timeZone = timeZone;
|
|
198
|
+
page.now = now;
|
|
199
|
+
page.test = (id, d, text) => {
|
|
200
|
+
const source = d?.message ?? text;
|
|
201
|
+
if (mode === "ids") return d?.id ?? (page.messages.has(id) ? id : generatedId(canonicalOf(source), idLength));
|
|
202
|
+
if (mode === "pseudo") return pseudo(source, style);
|
|
203
|
+
if (page.locale === locale && own.size > 0) {
|
|
204
|
+
const m = own.get(id) ?? own.get(canonicalOf(source));
|
|
205
|
+
if (m !== void 0) return m;
|
|
206
|
+
}
|
|
207
|
+
return page.messages.get(id) ?? d?.ir ?? compiled(source);
|
|
208
|
+
};
|
|
209
|
+
if (h) Object.assign(h, {
|
|
210
|
+
lang: locale,
|
|
211
|
+
dir: textDirection(locale)
|
|
212
|
+
});
|
|
213
|
+
notify();
|
|
214
|
+
return {
|
|
215
|
+
...createScopedApi(page),
|
|
216
|
+
mode,
|
|
217
|
+
reset: resetTestI18n
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/test/pseudo.ts
|
|
222
|
+
const LETTER = /\p{L}/u;
|
|
223
|
+
const BIDI_CONTROL = /[\u202A-\u202E\u2066-\u2069\u200E\u200F]/g;
|
|
224
|
+
/**
|
|
225
|
+
* Runs of letters at bracket depth 0 in `text`. The depth is per line: page
|
|
226
|
+
* text joins blocks with newlines, and a stray `]` in one block must not hide
|
|
227
|
+
* a leak in the next.
|
|
228
|
+
*/
|
|
229
|
+
function findPseudoLeaks(text, options = {}) {
|
|
230
|
+
const allow = (options.allow ?? []).map((a) => typeof a === "string" ? (run) => run.split(a).join(" ") : (run) => run.replace(new RegExp(a.source, a.flags.includes("g") ? a.flags : a.flags + "g"), " "));
|
|
231
|
+
const isAllowed = (run) => !LETTER.test(allow.reduce((rest, cut) => cut(rest), run));
|
|
232
|
+
const leaks = [];
|
|
233
|
+
for (const line of text.replace(BIDI_CONTROL, "").split("\n")) {
|
|
234
|
+
let depth = 0;
|
|
235
|
+
let run = "";
|
|
236
|
+
let runStart = 0;
|
|
237
|
+
const flush = (end) => {
|
|
238
|
+
const trimmed = run.trim();
|
|
239
|
+
if (trimmed && LETTER.test(trimmed) && !isAllowed(trimmed)) leaks.push({
|
|
240
|
+
text: trimmed,
|
|
241
|
+
context: line.slice(Math.max(0, runStart - 10), Math.min(line.length, end + 10))
|
|
242
|
+
});
|
|
243
|
+
run = "";
|
|
244
|
+
};
|
|
245
|
+
for (let i = 0; i < line.length; i++) {
|
|
246
|
+
const ch = line[i];
|
|
247
|
+
if (ch === "[") {
|
|
248
|
+
if (depth === 0) flush(i);
|
|
249
|
+
depth++;
|
|
250
|
+
} else if (ch === "]") {
|
|
251
|
+
depth = Math.max(0, depth - 1);
|
|
252
|
+
if (depth === 0) runStart = i + 1;
|
|
253
|
+
} else if (depth === 0) {
|
|
254
|
+
if (!run) runStart = i;
|
|
255
|
+
run += ch;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
flush(line.length);
|
|
259
|
+
}
|
|
260
|
+
return leaks;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The visible text of the page's document, one line per block. Runs in the
|
|
264
|
+
* page, so it is self-contained. Inline siblings stay on one line, so a
|
|
265
|
+
* message split by markup (`[Ŕéáď ţĥé <a>ďóçš</a>]`) is joined back.
|
|
266
|
+
*/
|
|
267
|
+
function pageText(arg) {
|
|
268
|
+
const skip = [
|
|
269
|
+
"script",
|
|
270
|
+
"style",
|
|
271
|
+
"noscript",
|
|
272
|
+
"template",
|
|
273
|
+
"[data-pseudo-ignore]"
|
|
274
|
+
].concat(arg).join(",");
|
|
275
|
+
const out = [document.title, "\n"];
|
|
276
|
+
const visit = (node) => {
|
|
277
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
278
|
+
const parent = node.parentElement;
|
|
279
|
+
if (parent && !parent.closest(skip)) out.push(node.nodeValue ?? "");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (!(node instanceof Element)) return;
|
|
283
|
+
if (node.matches(skip)) return;
|
|
284
|
+
if (typeof node.checkVisibility === "function" && !node.checkVisibility()) return;
|
|
285
|
+
const display = getComputedStyle(node).display;
|
|
286
|
+
const block = display !== "inline" && display !== "contents" && !display.startsWith("inline-");
|
|
287
|
+
if (block) out.push("\n");
|
|
288
|
+
for (const child of node.childNodes) visit(child);
|
|
289
|
+
if (block) out.push("\n");
|
|
290
|
+
};
|
|
291
|
+
visit(document.body);
|
|
292
|
+
return out.join("");
|
|
293
|
+
}
|
|
294
|
+
/** Text outside pseudo brackets on the page as it is now; empty means clean. */
|
|
295
|
+
async function scanPseudo(page, options = {}) {
|
|
296
|
+
return findPseudoLeaks(await page.evaluate(pageText, [...options.ignore ?? []]), options);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Switches the page to a pseudo-locale through its own vi18n runtime
|
|
300
|
+
* (`setLocale`, in place or by navigation, as the app's routing does it) and
|
|
301
|
+
* waits until `<html lang>` names it and its strings are loaded. Pseudo-locales
|
|
302
|
+
* exist in `vite dev` and non-production builds that list them in
|
|
303
|
+
* `pseudoLocales`.
|
|
304
|
+
*/
|
|
305
|
+
async function setPseudoLocale(page, locale = "en-XA", options = {}) {
|
|
306
|
+
const problem = await page.evaluate(async (arg) => {
|
|
307
|
+
const l = arg;
|
|
308
|
+
const state = globalThis[Symbol.for("menapo.vi18n")];
|
|
309
|
+
if (!state?.locales) return "vi18n is not loaded on this page";
|
|
310
|
+
if (!state.locales.includes(l)) return `${l} is not an active locale here (${state.locales.join(", ")})`;
|
|
311
|
+
await state.setLocale(l);
|
|
312
|
+
return null;
|
|
313
|
+
}, locale).catch(() => null);
|
|
314
|
+
if (typeof problem === "string") throw new Error(`vi18n: ${problem}; pseudo-locales exist in vite dev and in builds of another mode than production (vite build --mode staging) that list them in pseudoLocales`);
|
|
315
|
+
await page.waitForFunction((arg) => document.documentElement.lang === arg, locale, options.timeout === void 0 ? {} : { timeout: options.timeout });
|
|
316
|
+
await page.evaluate(() => globalThis[Symbol.for("menapo.vi18n")]?.ready());
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
export { createTestI18n, findPseudoLeaks, loadCatalog, resetTestI18n, scanPseudo, setPseudoLocale };
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { o as isDiagnosticError } from "./types-D3QGM8XO.js";
|
|
2
|
+
import { a as ownedMessages, n as libraryCoverage, o as catalogLayout } from "./library-D5CWvTQs.js";
|
|
3
|
+
import { a as table, r as plural } from "./output-DWE3SIUA.js";
|
|
4
|
+
import { t as EXIT } from "./types-wSh9fCBY.js";
|
|
5
|
+
import { a as loadCatalogs, f as scan, i as hasErrors, o as loadProject, p as targetLocales, t as CliExit, u as readErrors } from "./project-C4GpaN-n.js";
|
|
6
|
+
import { t as adaptiveConcurrency } from "./concurrency-C9UUWuDC.js";
|
|
7
|
+
import { a as firstRequests, c as runTranslations, i as estimateUsage, n as catalogPersister, o as planTranslations, r as createProvider, s as replayProvider } from "./pipeline-C9Dbotkh.js";
|
|
8
|
+
//#region src/cli/commands/translate.ts
|
|
9
|
+
/**
|
|
10
|
+
* `vi18n translate [--locale l]… [--provider name] [--model id] [--tier t]
|
|
11
|
+
* [--retranslate ai|all] [--dry-run] [--max-cost usd]` (INTERNALS §2.15,
|
|
12
|
+
* PLAN §5.20): plan every locale against the catalogs on disk, estimate,
|
|
13
|
+
* then translate one locale after another, writing each finished batch
|
|
14
|
+
* before the next is paid for. Exit 1 when any item still failed after its
|
|
15
|
+
* retries.
|
|
16
|
+
*
|
|
17
|
+
* `--tier batch` first submits every locale's first requests as Gemini
|
|
18
|
+
* batch jobs and waits for them; their answers then replay into the same
|
|
19
|
+
* per-locale runs, whose retries go out synchronously.
|
|
20
|
+
*/
|
|
21
|
+
async function translate(ctx, flags) {
|
|
22
|
+
const project = await loadProject(ctx);
|
|
23
|
+
const { config, codec } = project;
|
|
24
|
+
const locales = targetLocales(config, flags.locales);
|
|
25
|
+
const scanned = await scan(project);
|
|
26
|
+
ctx.out.report(scanned.diagnostics.filter((d) => d.severity === "error"));
|
|
27
|
+
const loaded = await loadCatalogs(project);
|
|
28
|
+
const unreadable = readErrors(loaded.diagnostics);
|
|
29
|
+
let provider;
|
|
30
|
+
try {
|
|
31
|
+
provider = createProvider(config, {
|
|
32
|
+
...flags.model === null ? {} : { model: flags.model },
|
|
33
|
+
...flags.provider === null ? {} : { provider: flags.provider },
|
|
34
|
+
tier: flags.tier,
|
|
35
|
+
...ctx.io.fetch === void 0 ? {} : { fetch: ctx.io.fetch },
|
|
36
|
+
env: ctx.io.env
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (isDiagnosticError(error)) throw new CliExit(EXIT.usage, [error.diagnostic]);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
if (typeof provider.apiKeyEnv === "string") ctx.out.addSecret(ctx.io.env[provider.apiKeyEnv]);
|
|
43
|
+
const configured = config.ai.provider;
|
|
44
|
+
if (flags.model !== null && flags.provider === null && configured !== "gemini" && configured.withModel === void 0) ctx.out.note(`vi18n: --model is ignored: ai.provider is a custom provider (${provider.name}, ${provider.model})`);
|
|
45
|
+
const data = {
|
|
46
|
+
dryRun: flags.dryRun,
|
|
47
|
+
provider: provider.name,
|
|
48
|
+
model: provider.model,
|
|
49
|
+
tier: flags.tier,
|
|
50
|
+
locales: [],
|
|
51
|
+
estimate: {
|
|
52
|
+
requests: 0,
|
|
53
|
+
inputTokens: 0,
|
|
54
|
+
outputTokens: 0,
|
|
55
|
+
cost: 0
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
if (unreadable.length > 0) {
|
|
59
|
+
ctx.out.report(unreadable);
|
|
60
|
+
return {
|
|
61
|
+
exitCode: EXIT.failure,
|
|
62
|
+
data
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const plans = /* @__PURE__ */ new Map();
|
|
66
|
+
const coverage = libraryCoverage(loaded.set, scanned.messages, config, codec);
|
|
67
|
+
for (const locale of locales) {
|
|
68
|
+
const plan = planTranslations({
|
|
69
|
+
config,
|
|
70
|
+
codec,
|
|
71
|
+
messages: ownedMessages(scanned.messages, locale, loaded.set.get(locale)?.catalog ?? null, coverage, {
|
|
72
|
+
sourceLocale: config.sourceLocale,
|
|
73
|
+
layout: catalogLayout(config),
|
|
74
|
+
codec
|
|
75
|
+
}),
|
|
76
|
+
catalogs: loaded.set,
|
|
77
|
+
locale,
|
|
78
|
+
retranslate: flags.retranslate
|
|
79
|
+
});
|
|
80
|
+
plans.set(locale, plan);
|
|
81
|
+
const usage = estimateUsage({
|
|
82
|
+
config,
|
|
83
|
+
locale,
|
|
84
|
+
plan
|
|
85
|
+
});
|
|
86
|
+
const estimate = {
|
|
87
|
+
...usage,
|
|
88
|
+
cost: costOf(provider, usage)
|
|
89
|
+
};
|
|
90
|
+
data.locales.push({
|
|
91
|
+
...emptySummary(locale),
|
|
92
|
+
estimate
|
|
93
|
+
});
|
|
94
|
+
addEstimate(data.estimate, estimate);
|
|
95
|
+
}
|
|
96
|
+
if (flags.maxCost !== null && data.estimate.requests > 0) {
|
|
97
|
+
const cost = data.estimate.cost;
|
|
98
|
+
if (cost === null || cost > flags.maxCost) {
|
|
99
|
+
ctx.out.report([{
|
|
100
|
+
code: "ai-cost-limit",
|
|
101
|
+
severity: "error",
|
|
102
|
+
message: cost === null ? `no price for ${provider.model}, so the cost of ${plural(data.estimate.requests, "request")} cannot be checked against --max-cost ${flags.maxCost}; nothing was sent` : `the estimated cost ${usd(cost)} of ${plural(data.estimate.requests, "request")} is over --max-cost ${usd(flags.maxCost)}; nothing was sent${flags.dryRun ? "" : " (see vi18n translate --dry-run)"}`
|
|
103
|
+
}]);
|
|
104
|
+
if (flags.dryRun) printEstimate(ctx, data);
|
|
105
|
+
return {
|
|
106
|
+
exitCode: EXIT.failure,
|
|
107
|
+
data
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (flags.dryRun) {
|
|
112
|
+
printEstimate(ctx, data);
|
|
113
|
+
return {
|
|
114
|
+
exitCode: hasErrors(ctx.out.diagnostics) ? EXIT.failure : EXIT.ok,
|
|
115
|
+
data
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (data.estimate.requests > 0) {
|
|
119
|
+
const missing = provider.missingKey?.() ?? null;
|
|
120
|
+
if (missing !== null) {
|
|
121
|
+
ctx.out.report([missing]);
|
|
122
|
+
return {
|
|
123
|
+
exitCode: EXIT.failure,
|
|
124
|
+
data
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let failed = false;
|
|
129
|
+
let finished = 0;
|
|
130
|
+
const aborted = () => ctx.io.signal?.aborted === true;
|
|
131
|
+
const interrupted = () => {
|
|
132
|
+
data.interrupted = true;
|
|
133
|
+
data.locales = data.locales.filter((_, index) => index < finished);
|
|
134
|
+
ctx.out.note("vi18n translate: interrupted; finished batches are saved, rerun to translate the rest");
|
|
135
|
+
return {
|
|
136
|
+
exitCode: EXIT.failure,
|
|
137
|
+
data
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
let runner = provider;
|
|
141
|
+
if (flags.tier === "batch" && data.estimate.requests > 0) {
|
|
142
|
+
const requests = [];
|
|
143
|
+
for (const locale of locales) requests.push(...firstRequests({
|
|
144
|
+
config,
|
|
145
|
+
locale,
|
|
146
|
+
plan: plans.get(locale) ?? []
|
|
147
|
+
}));
|
|
148
|
+
if (provider.translateBatch === void 0) throw new CliExit(EXIT.usage, [{
|
|
149
|
+
code: "config-invalid",
|
|
150
|
+
severity: "error",
|
|
151
|
+
message: `--tier batch needs a provider with a batch API, not ${provider.name}`
|
|
152
|
+
}]);
|
|
153
|
+
ctx.out.note(`vi18n translate: sending ${plural(requests.length, "request")} as batch jobs; results can take minutes to hours (Ctrl-C cancels)`);
|
|
154
|
+
const states = /* @__PURE__ */ new Map();
|
|
155
|
+
try {
|
|
156
|
+
const answers = await provider.translateBatch(requests, {
|
|
157
|
+
...ctx.io.signal === void 0 ? {} : { signal: ctx.io.signal },
|
|
158
|
+
onProgress: ({ job, state }) => {
|
|
159
|
+
if (states.get(job) === state) return;
|
|
160
|
+
states.set(job, state);
|
|
161
|
+
ctx.out.note(`vi18n translate: ${job}: ${state}`);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
runner = replayProvider(provider, requests, answers);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (aborted()) return interrupted();
|
|
167
|
+
if (!isDiagnosticError(error)) throw error;
|
|
168
|
+
ctx.out.report([error.diagnostic]);
|
|
169
|
+
return {
|
|
170
|
+
exitCode: EXIT.failure,
|
|
171
|
+
data
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const concurrency = adaptiveConcurrency(config.ai.concurrency);
|
|
176
|
+
for (let i = 0; i < locales.length; i++) {
|
|
177
|
+
if (aborted()) return interrupted();
|
|
178
|
+
const locale = locales[i];
|
|
179
|
+
const plan = plans.get(locale);
|
|
180
|
+
const persist = catalogPersister({
|
|
181
|
+
config,
|
|
182
|
+
codec,
|
|
183
|
+
locale,
|
|
184
|
+
messages: scanned.messages,
|
|
185
|
+
catalog: loaded.set.get(locale),
|
|
186
|
+
sourceCatalog: loaded.set.get(config.sourceLocale),
|
|
187
|
+
model: provider.model
|
|
188
|
+
});
|
|
189
|
+
let summary;
|
|
190
|
+
try {
|
|
191
|
+
summary = await runTranslations({
|
|
192
|
+
provider: runner,
|
|
193
|
+
config,
|
|
194
|
+
locale,
|
|
195
|
+
plan,
|
|
196
|
+
persist,
|
|
197
|
+
concurrency,
|
|
198
|
+
...ctx.io.signal === void 0 ? {} : { signal: ctx.io.signal }
|
|
199
|
+
});
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (aborted()) return interrupted();
|
|
202
|
+
if (!isDiagnosticError(error)) throw error;
|
|
203
|
+
ctx.out.report([{
|
|
204
|
+
...error.diagnostic,
|
|
205
|
+
locale: error.diagnostic.locale ?? locale
|
|
206
|
+
}]);
|
|
207
|
+
return {
|
|
208
|
+
exitCode: EXIT.failure,
|
|
209
|
+
data
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const entry = data.locales[i];
|
|
213
|
+
Object.assign(entry, summary, { estimate: entry.estimate });
|
|
214
|
+
ctx.out.report(summaryDiagnostics(summary));
|
|
215
|
+
if (summary.failed.length > 0) failed = true;
|
|
216
|
+
if (summary.rateLimited !== void 0) ctx.out.note(`vi18n translate: ${locale} was rate-limited ${plural(summary.rateLimited.count, "time")}; requests in flight now ${summary.rateLimited.concurrency} of ${concurrency.max}`);
|
|
217
|
+
finished = i + 1;
|
|
218
|
+
ctx.out.result(`${locale}: ${summary.translated} translated, ${summary.reused} reused, ${summary.failed.length} failed of ${summary.requested} requested (${summary.usage.inputTokens} input / ${summary.usage.outputTokens} output tokens)`);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
exitCode: failed || hasErrors(ctx.out.diagnostics) ? EXIT.failure : EXIT.ok,
|
|
222
|
+
data
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function emptySummary(locale) {
|
|
226
|
+
return {
|
|
227
|
+
locale,
|
|
228
|
+
requested: 0,
|
|
229
|
+
translated: 0,
|
|
230
|
+
reused: 0,
|
|
231
|
+
identical: [],
|
|
232
|
+
failed: [],
|
|
233
|
+
usage: {
|
|
234
|
+
inputTokens: 0,
|
|
235
|
+
outputTokens: 0
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** USD from the provider's pinned price table; null when it has none for the model. */
|
|
240
|
+
function costOf(provider, usage) {
|
|
241
|
+
if (usage.inputTokens === 0 && usage.outputTokens === 0) return 0;
|
|
242
|
+
const cost = provider.estimateCost?.(usage) ?? NaN;
|
|
243
|
+
return Number.isFinite(cost) ? cost : null;
|
|
244
|
+
}
|
|
245
|
+
function addEstimate(total, part) {
|
|
246
|
+
total.requests += part.requests;
|
|
247
|
+
total.inputTokens += part.inputTokens;
|
|
248
|
+
total.outputTokens += part.outputTokens;
|
|
249
|
+
total.cost = total.cost === null || part.cost === null ? null : total.cost + part.cost;
|
|
250
|
+
}
|
|
251
|
+
function usd(value) {
|
|
252
|
+
return `$${value < .01 && value > 0 ? value.toFixed(4) : value.toFixed(2)}`;
|
|
253
|
+
}
|
|
254
|
+
function printEstimate(ctx, data) {
|
|
255
|
+
const rows = data.locales.map((l) => [
|
|
256
|
+
l.locale,
|
|
257
|
+
l.estimate.requests,
|
|
258
|
+
l.estimate.inputTokens,
|
|
259
|
+
l.estimate.outputTokens,
|
|
260
|
+
l.estimate.cost === null ? "no price" : usd(l.estimate.cost)
|
|
261
|
+
]);
|
|
262
|
+
const t = data.estimate;
|
|
263
|
+
rows.push([
|
|
264
|
+
"total",
|
|
265
|
+
t.requests,
|
|
266
|
+
t.inputTokens,
|
|
267
|
+
t.outputTokens,
|
|
268
|
+
t.cost === null ? "no price" : usd(t.cost)
|
|
269
|
+
]);
|
|
270
|
+
ctx.out.result(table([
|
|
271
|
+
"locale",
|
|
272
|
+
"requests",
|
|
273
|
+
"input tokens",
|
|
274
|
+
"output tokens",
|
|
275
|
+
"cost"
|
|
276
|
+
], rows));
|
|
277
|
+
const tier = data.tier === "standard" ? "" : `, ${data.tier} tier`;
|
|
278
|
+
ctx.out.result(`dry run with ${data.model}${tier}: nothing was sent (tokens estimated at 4 characters each)`);
|
|
279
|
+
}
|
|
280
|
+
function summaryDiagnostics(summary) {
|
|
281
|
+
const out = [];
|
|
282
|
+
for (const id of summary.identical) out.push({
|
|
283
|
+
code: "ai-identical-output",
|
|
284
|
+
severity: "info",
|
|
285
|
+
message: `the ${summary.locale} translation of ${id} is identical to the source (often right for names and words like "OK"); review it if unexpected`,
|
|
286
|
+
locale: summary.locale,
|
|
287
|
+
id
|
|
288
|
+
});
|
|
289
|
+
for (const failure of summary.failed) out.push({
|
|
290
|
+
code: "ai-validation-failed",
|
|
291
|
+
severity: "error",
|
|
292
|
+
message: `${failure.id} was left untranslated after its retries: ${failure.checks.join("; ")}`,
|
|
293
|
+
locale: summary.locale,
|
|
294
|
+
id: failure.id
|
|
295
|
+
});
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
//#endregion
|
|
299
|
+
export { translate };
|