@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.
- 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-D34P-S--.js +1 -0
- package/dist/check-DyrAbv73.js +201 -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-B5Mz6WJm.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-B7p40M__.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-DGsMbcTZ.js +103 -0
- package/dist/extract-Dujlsgnk.js +298 -0
- package/dist/formats-KPIg-08V.js +781 -0
- package/dist/i18next-CrUnKFfh.js +1596 -0
- package/dist/i18next-Rgo-v9l0.js +1 -0
- package/dist/import-Cwdhcywt.js +225 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +239 -0
- package/dist/init-BecCTvKi.js +792 -0
- package/dist/library-D5CWvTQs.js +1882 -0
- package/dist/lingui--PcOOYry.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-Cq1Nuu-Q.js +31 -0
- package/dist/match-BGr8EXB3.js +89 -0
- package/dist/mcp-Cf0T7qPW.js +28 -0
- package/dist/merge-driver-CpiiSVIc.js +422 -0
- package/dist/messages-Cxa-5tR0.js +166 -0
- package/dist/migrate-CntqvrW4.js +1 -0
- package/dist/migrate-dPgq9uzy.js +1484 -0
- package/dist/openai-CTaMDcl-.js +346 -0
- package/dist/output-DsaHKHEt.js +164 -0
- package/dist/parse.d.ts +62 -0
- package/dist/parse.js +156 -0
- package/dist/pipeline-C7g11THL.js +2031 -0
- package/dist/pipeline-wxS0SApG.js +1 -0
- package/dist/project-Dwx7iPm6.js +381 -0
- package/dist/project-pXipT8iG.js +1046 -0
- package/dist/pseudo-DRv30iN5.js +145 -0
- package/dist/raw-C4Y6vyGY.js +4708 -0
- package/dist/react-intl-CaBVwJ9u.js +519 -0
- package/dist/restore-Btq66Tid.js +175 -0
- package/dist/rewrite-CiTRAICl.js +803 -0
- package/dist/rich-DCUKl5HM.js +97 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/run-DYD_VXKy.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-BDkykMjb.js +11823 -0
- package/dist/server-qv3mz70d.js +10485 -0
- package/dist/server.d.ts +169 -0
- package/dist/server.js +1234 -0
- package/dist/sitemap-DV3o8QD7.js +159 -0
- package/dist/sources-D19czDSA.js +2242 -0
- package/dist/status-DZArW5aE.js +149 -0
- package/dist/stdio-CVaJfxA7.js +29 -0
- package/dist/stdio-FPOKQAgu.js +95 -0
- package/dist/sync-CcVzger_.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-DZR3MkSA.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-BlyLGlOa.js +28 -0
- package/dist/vite-scan-B_YW4X_a.js +242 -0
- package/dist/vite.d.ts +34 -0
- package/dist/vite.js +7558 -0
- package/dist/vue-i18n-CEMnpv7k.js +651 -0
- package/dist/which-C6PFI1Dm.js +116 -0
- package/dist/worker-84U6uYB3.js +1656 -0
- package/package.json +97 -3
- package/schemas/catalog.json +57 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { a as diagnosticError } from "./types-D3QGM8XO.js";
|
|
2
|
+
/** Longest wait between retries, whatever the server asks for. */
|
|
3
|
+
const MAX_DELAY_MS = 6e4;
|
|
4
|
+
/** Error bodies are quoted in messages; past this they are noise. */
|
|
5
|
+
const MAX_DETAIL = 300;
|
|
6
|
+
/**
|
|
7
|
+
* One request, rate limits waited out: on a rate limit, `onRateLimit` hears
|
|
8
|
+
* of it, then the request is sent again after the server's delay (else
|
|
9
|
+
* 2ⁿ s, at most {@link MAX_DELAY_MS}), up to {@link RATE_LIMIT_RETRIES}
|
|
10
|
+
* times. Returns the first answer that is not a rate limit; a network
|
|
11
|
+
* failure is an `ai-provider-error`, an abort is rethrown as it is.
|
|
12
|
+
*/
|
|
13
|
+
async function exchange(options) {
|
|
14
|
+
const { signal, key } = options;
|
|
15
|
+
for (let attempt = 0;; attempt++) {
|
|
16
|
+
signal?.throwIfAborted();
|
|
17
|
+
let response;
|
|
18
|
+
try {
|
|
19
|
+
response = await options.fetch(options.url, {
|
|
20
|
+
...options.init,
|
|
21
|
+
...signal === void 0 ? {} : { signal }
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (signal?.aborted === true || isAbort(error)) throw error;
|
|
25
|
+
throw providerError(`${options.label}: request failed: ${errorText(error)}`, key);
|
|
26
|
+
}
|
|
27
|
+
const text = await response.text();
|
|
28
|
+
const answer = {
|
|
29
|
+
status: response.status,
|
|
30
|
+
ok: response.ok,
|
|
31
|
+
headers: response.headers,
|
|
32
|
+
text,
|
|
33
|
+
json: parseJson(text)
|
|
34
|
+
};
|
|
35
|
+
if (answer.ok || !options.isRateLimit(answer)) return answer;
|
|
36
|
+
if (attempt >= 5) throw providerError(`${options.label} is still rate-limited after 5 retries (HTTP ${answer.status}); ${options.advice}: ${detail(answer, key)}`, key);
|
|
37
|
+
const asked = options.serverDelay(answer);
|
|
38
|
+
options.onRateLimit?.(asked);
|
|
39
|
+
await sleep(Math.min(asked ?? 1e3 * 2 ** attempt, MAX_DELAY_MS), signal);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** The user content of a request: the items as data. */
|
|
43
|
+
function requestText(request) {
|
|
44
|
+
return JSON.stringify({
|
|
45
|
+
sourceLocale: request.sourceLocale,
|
|
46
|
+
targetLocale: request.targetLocale,
|
|
47
|
+
items: request.items
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The `{ items: [{ id, translation }] }` an answer text holds. Strict: the
|
|
52
|
+
* whole text is the JSON, or one fenced block holding only it (models
|
|
53
|
+
* without structured output wrap their answer so). Anything else yields no
|
|
54
|
+
* items, so the pipeline retries every item instead of failing the batch.
|
|
55
|
+
*/
|
|
56
|
+
function answerItems(text) {
|
|
57
|
+
const trimmed = text.trim();
|
|
58
|
+
const fenced = /^```(?:json)?[ \t]*\n([\s\S]*)\n```$/.exec(trimmed);
|
|
59
|
+
const items = field(parseJson(fenced === null ? trimmed : fenced[1]), "items");
|
|
60
|
+
const out = [];
|
|
61
|
+
if (!Array.isArray(items)) return out;
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
const id = field(item, "id");
|
|
64
|
+
const translation = field(item, "translation");
|
|
65
|
+
if (typeof id === "string" && typeof translation === "string") out.push({
|
|
66
|
+
id,
|
|
67
|
+
translation
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function field(value, name) {
|
|
73
|
+
return typeof value === "object" && value !== null ? value[name] : void 0;
|
|
74
|
+
}
|
|
75
|
+
function count(value) {
|
|
76
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
77
|
+
}
|
|
78
|
+
function parseJson(text) {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(text);
|
|
81
|
+
} catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The server's error text, shortened. The key is redacted before the cut: a
|
|
87
|
+
* cut through an echoed key would leave a prefix that `redact` no longer
|
|
88
|
+
* recognises.
|
|
89
|
+
*/
|
|
90
|
+
function detail(answer, key) {
|
|
91
|
+
const message = field(field(answer.json, "error"), "message");
|
|
92
|
+
const flat = redact(typeof message === "string" ? message : answer.text, key).replace(/\s+/g, " ").trim();
|
|
93
|
+
return flat.length > MAX_DETAIL ? `${flat.slice(0, MAX_DETAIL)}…` : flat || "(empty body)";
|
|
94
|
+
}
|
|
95
|
+
/** `Retry-After` in seconds (an HTTP date is not worth parsing for a wait this short). */
|
|
96
|
+
function retryAfterDelay(headers) {
|
|
97
|
+
const value = headers.get("retry-after");
|
|
98
|
+
if (value === null || !/^\d+$/.test(value.trim())) return null;
|
|
99
|
+
return Number(value) * 1e3;
|
|
100
|
+
}
|
|
101
|
+
function sleep(ms, signal) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
if (signal?.aborted === true) {
|
|
104
|
+
reject(signal.reason);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const onAbort = () => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
reject(signal?.reason);
|
|
110
|
+
};
|
|
111
|
+
const timer = setTimeout(() => {
|
|
112
|
+
signal?.removeEventListener("abort", onAbort);
|
|
113
|
+
resolve();
|
|
114
|
+
}, ms);
|
|
115
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function isAbort(error) {
|
|
119
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
120
|
+
}
|
|
121
|
+
function errorText(error) {
|
|
122
|
+
if (error instanceof Error) {
|
|
123
|
+
const cause = error.cause;
|
|
124
|
+
return cause instanceof Error ? `${error.message} (${cause.message})` : error.message;
|
|
125
|
+
}
|
|
126
|
+
return String(error);
|
|
127
|
+
}
|
|
128
|
+
/** Servers echo requests in errors; the key must never reach a terminal or a log. */
|
|
129
|
+
function redact(text, secret) {
|
|
130
|
+
return secret === "" ? text : text.split(secret).join("***");
|
|
131
|
+
}
|
|
132
|
+
function providerError(message, key) {
|
|
133
|
+
return diagnosticError({
|
|
134
|
+
code: "ai-provider-error",
|
|
135
|
+
severity: "error",
|
|
136
|
+
message: redact(message, key)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** The key in `env[name]`, or null when it is unset or blank. */
|
|
140
|
+
function readKey(env, name) {
|
|
141
|
+
const key = (env ?? globalThis.process?.env ?? {})[name];
|
|
142
|
+
return key === void 0 || key.trim() === "" ? null : key;
|
|
143
|
+
}
|
|
144
|
+
const OPENAI_KEY_ENV = "OPENAI_API_KEY";
|
|
145
|
+
/** `--provider` names: the built-in Gemini provider and the presets below. */
|
|
146
|
+
const PROVIDER_PRESETS = [
|
|
147
|
+
"gemini",
|
|
148
|
+
"openai",
|
|
149
|
+
"openrouter",
|
|
150
|
+
"ollama"
|
|
151
|
+
];
|
|
152
|
+
/**
|
|
153
|
+
* OpenRouter's key variable is named here, which is the explicit
|
|
154
|
+
* `apiKeyEnv` for its host; Ollama runs locally and gets no key.
|
|
155
|
+
*/
|
|
156
|
+
const PRESETS = {
|
|
157
|
+
openai: {},
|
|
158
|
+
openrouter: {
|
|
159
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
160
|
+
apiKeyEnv: "OPENROUTER_API_KEY"
|
|
161
|
+
},
|
|
162
|
+
ollama: { baseURL: "http://localhost:11434/v1" }
|
|
163
|
+
};
|
|
164
|
+
/** `vi18n translate --provider openai|openrouter|ollama --model <id>`. */
|
|
165
|
+
function openaiPreset(name, options) {
|
|
166
|
+
return openaiCompatible({
|
|
167
|
+
...PRESETS[name],
|
|
168
|
+
...options
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const FORMATS = [
|
|
172
|
+
"json_schema",
|
|
173
|
+
"json_object",
|
|
174
|
+
"text"
|
|
175
|
+
];
|
|
176
|
+
function openaiCompatible(options) {
|
|
177
|
+
if (typeof options?.model !== "string" || options.model.trim() === "") throw diagnosticError({
|
|
178
|
+
code: "config-invalid",
|
|
179
|
+
severity: "error",
|
|
180
|
+
message: "openaiCompatible() needs a model, e.g. { model: \"gpt-5-mini\" }"
|
|
181
|
+
});
|
|
182
|
+
const model = options.model;
|
|
183
|
+
const base = (options.baseURL ?? "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
184
|
+
let origin;
|
|
185
|
+
try {
|
|
186
|
+
origin = new URL(base).origin;
|
|
187
|
+
} catch {
|
|
188
|
+
throw diagnosticError({
|
|
189
|
+
code: "config-invalid",
|
|
190
|
+
severity: "error",
|
|
191
|
+
message: `openaiCompatible({ baseURL: ${JSON.stringify(base)} }) is not a URL`
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const apiKeyEnv = options.apiKeyEnv ?? (origin === new URL("https://api.openai.com/v1").origin ? OPENAI_KEY_ENV : null);
|
|
195
|
+
const url = `${base}/chat/completions`;
|
|
196
|
+
const label = `${model} (${origin})`;
|
|
197
|
+
const missingKey = () => apiKeyEnv !== null && readKey(options.env, apiKeyEnv) === null ? {
|
|
198
|
+
code: "ai-missing-key",
|
|
199
|
+
severity: "error",
|
|
200
|
+
message: `Set ${apiKeyEnv} to the API key of ${origin} to translate with ${model}`
|
|
201
|
+
} : null;
|
|
202
|
+
let format = FORMATS.indexOf(options.responseFormat ?? "json_schema");
|
|
203
|
+
let temperature = true;
|
|
204
|
+
return {
|
|
205
|
+
name: "openai-compatible",
|
|
206
|
+
model,
|
|
207
|
+
apiKeyEnv,
|
|
208
|
+
async translate(request, callOptions) {
|
|
209
|
+
const problem = missingKey();
|
|
210
|
+
if (problem !== null) throw diagnosticError(problem);
|
|
211
|
+
const key = apiKeyEnv === null ? "" : readKey(options.env, apiKeyEnv);
|
|
212
|
+
const headers = { "content-type": "application/json" };
|
|
213
|
+
if (key !== "") headers.authorization = `Bearer ${key}`;
|
|
214
|
+
for (;;) {
|
|
215
|
+
const sentFormat = format;
|
|
216
|
+
const sentTemperature = temperature;
|
|
217
|
+
const answer = await exchange({
|
|
218
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
219
|
+
url,
|
|
220
|
+
init: {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers,
|
|
223
|
+
body: JSON.stringify(requestBody(request, model, FORMATS[sentFormat], sentTemperature))
|
|
224
|
+
},
|
|
225
|
+
key,
|
|
226
|
+
label,
|
|
227
|
+
signal: callOptions?.signal,
|
|
228
|
+
onRateLimit: callOptions?.onRateLimit,
|
|
229
|
+
isRateLimit: (answer) => answer.status === 429 && !quotaExhausted(answer.json),
|
|
230
|
+
serverDelay: (answer) => retryAfterMsDelay(answer.headers),
|
|
231
|
+
advice: "lower ai.concurrency or wait for the rate limit to reset"
|
|
232
|
+
});
|
|
233
|
+
if (answer.ok) return readAnswer(answer.json);
|
|
234
|
+
if (answer.status === 400 || answer.status === 404) {
|
|
235
|
+
const refused = refusedParameter(answer);
|
|
236
|
+
if ((refused === "response_format" || refused === "unnamed") && sentFormat < FORMATS.length - 1) {
|
|
237
|
+
format = Math.max(format, sentFormat + 1);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if ((refused === "temperature" || refused === "unnamed") && sentTemperature) {
|
|
241
|
+
temperature = false;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
throw providerError(failureMessage(answer, label, key, apiKeyEnv, origin), key);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
estimateCost(usage) {
|
|
249
|
+
const prices = options.prices;
|
|
250
|
+
if (prices === void 0) return NaN;
|
|
251
|
+
return (usage.inputTokens * prices.input + usage.outputTokens * prices.output) / 1e6;
|
|
252
|
+
},
|
|
253
|
+
missingKey,
|
|
254
|
+
withModel(next) {
|
|
255
|
+
return openaiCompatible({
|
|
256
|
+
...options,
|
|
257
|
+
model: next
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function requestBody(request, model, format, temperature) {
|
|
263
|
+
const body = {
|
|
264
|
+
model,
|
|
265
|
+
messages: [{
|
|
266
|
+
role: "system",
|
|
267
|
+
content: request.system
|
|
268
|
+
}, {
|
|
269
|
+
role: "user",
|
|
270
|
+
content: requestText(request)
|
|
271
|
+
}]
|
|
272
|
+
};
|
|
273
|
+
if (temperature) body.temperature = 0;
|
|
274
|
+
if (format === "json_schema") body.response_format = {
|
|
275
|
+
type: "json_schema",
|
|
276
|
+
json_schema: {
|
|
277
|
+
name: "translations",
|
|
278
|
+
strict: true,
|
|
279
|
+
schema: strictSchema(request.responseSchema)
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
else if (format === "json_object") body.response_format = { type: "json_object" };
|
|
283
|
+
return body;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* OpenAI's strict mode wants every object closed (`additionalProperties:
|
|
287
|
+
* false`) and every property required; the response schema already
|
|
288
|
+
* requires all of its properties, so only the closing is added.
|
|
289
|
+
*/
|
|
290
|
+
function strictSchema(schema) {
|
|
291
|
+
const out = {};
|
|
292
|
+
for (const [key, value] of Object.entries(schema)) if (key === "properties" && typeof value === "object" && value !== null) out[key] = Object.fromEntries(Object.entries(value).map(([name, inner]) => [name, strictSchema(inner)]));
|
|
293
|
+
else if (key === "items" && typeof value === "object" && value !== null) out[key] = strictSchema(value);
|
|
294
|
+
else out[key] = value;
|
|
295
|
+
if (out.type === "object" && out.additionalProperties === void 0) out.additionalProperties = false;
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* `choices[0].message.content`, parsed strictly; a refusal or an empty
|
|
300
|
+
* message yields no items, so the pipeline retries them.
|
|
301
|
+
*/
|
|
302
|
+
function readAnswer(json) {
|
|
303
|
+
const result = { items: [] };
|
|
304
|
+
const usage = field(json, "usage");
|
|
305
|
+
if (usage !== void 0) result.usage = {
|
|
306
|
+
inputTokens: count(field(usage, "prompt_tokens")),
|
|
307
|
+
outputTokens: count(field(usage, "completion_tokens"))
|
|
308
|
+
};
|
|
309
|
+
const choices = field(json, "choices");
|
|
310
|
+
const content = field(field(Array.isArray(choices) ? choices[0] : void 0, "message"), "content");
|
|
311
|
+
if (typeof content === "string") result.items = answerItems(content);
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
/** A 429 that waiting cannot fix: the account is out of credit. */
|
|
315
|
+
function quotaExhausted(json) {
|
|
316
|
+
const error = field(json, "error");
|
|
317
|
+
return field(error, "code") === "insufficient_quota" || field(error, "type") === "insufficient_quota";
|
|
318
|
+
}
|
|
319
|
+
/** OpenAI's `retry-after-ms`, else the standard `Retry-After`. */
|
|
320
|
+
function retryAfterMsDelay(headers) {
|
|
321
|
+
const ms = headers.get("retry-after-ms");
|
|
322
|
+
if (ms !== null && /^\d+(?:\.\d+)?$/.test(ms.trim())) return Math.ceil(Number(ms));
|
|
323
|
+
return retryAfterDelay(headers);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Which optional parameter an answer objects to, as OpenAI-style servers
|
|
327
|
+
* word it; `unnamed` when it objects to some parameter without naming one,
|
|
328
|
+
* and the structured output goes first, then the temperature.
|
|
329
|
+
*/
|
|
330
|
+
function refusedParameter(answer) {
|
|
331
|
+
const error = field(answer.json, "error");
|
|
332
|
+
const param = field(error, "param");
|
|
333
|
+
const message = field(error, "message");
|
|
334
|
+
const text = `${typeof param === "string" ? param : ""} ${typeof message === "string" ? message : answer.text}`;
|
|
335
|
+
if (/response_format|json_schema|json_object|structured output/i.test(text)) return "response_format";
|
|
336
|
+
if (/temperature/i.test(text)) return "temperature";
|
|
337
|
+
if (/requested parameters|unsupported parameter/i.test(text)) return "unnamed";
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
function failureMessage(answer, label, key, apiKeyEnv, origin) {
|
|
341
|
+
const base = `${label} answered HTTP ${answer.status}: ${detail(answer, key)}`;
|
|
342
|
+
if ((answer.status === 401 || answer.status === 403) && apiKeyEnv === null) return `${base}; no key was sent: set apiKeyEnv to the variable holding the key of ${origin} (a key goes to a host other than OpenAI's only when apiKeyEnv names it)`;
|
|
343
|
+
return base;
|
|
344
|
+
}
|
|
345
|
+
//#endregion
|
|
346
|
+
export { answerItems as a, exchange as c, readKey as d, requestText as f, MAX_DELAY_MS as i, field as l, sleep as m, openaiCompatible as n, count as o, retryAfterDelay as p, openaiPreset as r, detail as s, PROVIDER_PRESETS as t, providerError as u };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { t as VERSION } from "./version-BlyLGlOa.js";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
//#region src/cli/output.ts
|
|
5
|
+
/**
|
|
6
|
+
* Everything the CLI prints (INTERNALS §2.14): results on stdout, diagnostics
|
|
7
|
+
* on stderr as `file:line:col severity code message`, GitHub workflow
|
|
8
|
+
* annotations, or one `--json` envelope and nothing else on stdout. Every
|
|
9
|
+
* string goes through redaction, so a provider key that a server echoed into
|
|
10
|
+
* an error never reaches a terminal, a CI log or an agent.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Secrets shorter than this are not redacted: replacing every occurrence of
|
|
14
|
+
* a one- or two-character value would garble the whole output while hiding
|
|
15
|
+
* nothing a real API key looks like.
|
|
16
|
+
*/
|
|
17
|
+
const MIN_SECRET = 4;
|
|
18
|
+
function createOutput(io, format) {
|
|
19
|
+
const secrets = /* @__PURE__ */ new Set();
|
|
20
|
+
const diagnostics = [];
|
|
21
|
+
let root = io.cwd;
|
|
22
|
+
let gitRoot;
|
|
23
|
+
const redact = (text) => {
|
|
24
|
+
let out = text;
|
|
25
|
+
for (const secret of secrets) {
|
|
26
|
+
if (out.includes(secret)) out = out.split(secret).join("***");
|
|
27
|
+
const escaped = JSON.stringify(secret).slice(1, -1);
|
|
28
|
+
if (escaped !== secret && out.includes(escaped)) out = out.split(escaped).join("***");
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
};
|
|
32
|
+
const absolute = (file) => isAbsolute(file) ? file : resolve(root, file);
|
|
33
|
+
const display = (file) => {
|
|
34
|
+
const rel = relative(io.cwd, absolute(file));
|
|
35
|
+
return (rel === "" ? "." : rel).split(sep).join("/");
|
|
36
|
+
};
|
|
37
|
+
const repoPath = (file) => {
|
|
38
|
+
if (gitRoot === void 0) gitRoot = findGitRoot(io.cwd);
|
|
39
|
+
const abs = absolute(file);
|
|
40
|
+
const rel = relative(gitRoot ?? io.cwd, abs);
|
|
41
|
+
return (rel.startsWith("..") || isAbsolute(rel) ? abs : rel).split(sep).join("/");
|
|
42
|
+
};
|
|
43
|
+
const print = (diagnostic) => {
|
|
44
|
+
if (format === "json") return;
|
|
45
|
+
if (format === "github") {
|
|
46
|
+
io.stdout(redact(githubLine(diagnostic, repoPath)) + "\n");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
io.stderr(redact(textLine(diagnostic, display)) + "\n");
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
format,
|
|
53
|
+
diagnostics,
|
|
54
|
+
setRoot(value) {
|
|
55
|
+
root = value;
|
|
56
|
+
},
|
|
57
|
+
addSecret(value) {
|
|
58
|
+
if (value === void 0) return;
|
|
59
|
+
const trimmed = value.trim();
|
|
60
|
+
if (trimmed.length >= MIN_SECRET) secrets.add(trimmed);
|
|
61
|
+
if (value.length >= MIN_SECRET) secrets.add(value);
|
|
62
|
+
},
|
|
63
|
+
redact,
|
|
64
|
+
report(list) {
|
|
65
|
+
for (const diagnostic of list) {
|
|
66
|
+
diagnostics.push(diagnostic);
|
|
67
|
+
print(diagnostic);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
result(text) {
|
|
71
|
+
if (format !== "json") io.stdout(redact(text.endsWith("\n") ? text : text + "\n"));
|
|
72
|
+
},
|
|
73
|
+
note(text) {
|
|
74
|
+
if (format !== "json") io.stderr(redact(text.endsWith("\n") ? text : text + "\n"));
|
|
75
|
+
},
|
|
76
|
+
finish(command, exitCode, data) {
|
|
77
|
+
if (format !== "json") return;
|
|
78
|
+
const envelope = {
|
|
79
|
+
command,
|
|
80
|
+
ok: exitCode === 0,
|
|
81
|
+
exitCode,
|
|
82
|
+
diagnostics,
|
|
83
|
+
data,
|
|
84
|
+
version: VERSION
|
|
85
|
+
};
|
|
86
|
+
io.stdout(redact(JSON.stringify(envelope, null, 2)) + "\n");
|
|
87
|
+
},
|
|
88
|
+
display
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** `file:line:col severity code message`; the location falls back to the locale. */
|
|
92
|
+
function textLine(diagnostic, display) {
|
|
93
|
+
let location;
|
|
94
|
+
if (diagnostic.file !== void 0 && diagnostic.file !== "") {
|
|
95
|
+
location = display(diagnostic.file);
|
|
96
|
+
if (diagnostic.line !== void 0 && diagnostic.line > 0) {
|
|
97
|
+
location += `:${diagnostic.line}`;
|
|
98
|
+
if (diagnostic.column !== void 0 && diagnostic.column > 0) location += `:${diagnostic.column}`;
|
|
99
|
+
}
|
|
100
|
+
} else location = diagnostic.locale ?? "vi18n";
|
|
101
|
+
return `${location} ${diagnostic.severity} ${diagnostic.code} ${diagnostic.message}`;
|
|
102
|
+
}
|
|
103
|
+
const LEVEL = {
|
|
104
|
+
error: "error",
|
|
105
|
+
warning: "warning",
|
|
106
|
+
info: "notice"
|
|
107
|
+
};
|
|
108
|
+
/** GitHub's escaping for a workflow command's message. */
|
|
109
|
+
function escapeData(text) {
|
|
110
|
+
return text.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
111
|
+
}
|
|
112
|
+
/** Property values additionally escape the separators of the property list. */
|
|
113
|
+
function escapeProperty(text) {
|
|
114
|
+
return escapeData(text).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
115
|
+
}
|
|
116
|
+
function githubLine(diagnostic, repoPath) {
|
|
117
|
+
const props = [];
|
|
118
|
+
if (diagnostic.file !== void 0 && diagnostic.file !== "") {
|
|
119
|
+
props.push(`file=${escapeProperty(repoPath(diagnostic.file))}`);
|
|
120
|
+
if (diagnostic.line !== void 0 && diagnostic.line > 0) {
|
|
121
|
+
props.push(`line=${diagnostic.line}`);
|
|
122
|
+
if (diagnostic.column !== void 0 && diagnostic.column > 0) props.push(`col=${diagnostic.column}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
props.push(`title=${escapeProperty(`vi18n(${diagnostic.code})`)}`);
|
|
126
|
+
const message = diagnostic.locale !== void 0 && !diagnostic.message.includes(diagnostic.locale) ? `${diagnostic.locale}: ${diagnostic.message}` : diagnostic.message;
|
|
127
|
+
return `::${LEVEL[diagnostic.severity]} ${props.join(",")}::${escapeData(message)}`;
|
|
128
|
+
}
|
|
129
|
+
/** The nearest directory upward with a `.git` entry (a directory, or a file in worktrees). */
|
|
130
|
+
function findGitRoot(from) {
|
|
131
|
+
for (let dir = resolve(from);; dir = dirname(dir)) {
|
|
132
|
+
if (existsSync(join(dir, ".git"))) return dir;
|
|
133
|
+
if (dirname(dir) === dir) return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** A fixed-width table for text output; numbers right-aligned. */
|
|
137
|
+
function table(header, rows) {
|
|
138
|
+
const cells = [header, ...rows].map((row) => row.map(String));
|
|
139
|
+
const widths = header.map((_, i) => Math.max(...cells.map((row) => (row[i] ?? "").length)));
|
|
140
|
+
return cells.map((row, r) => row.map((cell, i) => {
|
|
141
|
+
const width = widths[i];
|
|
142
|
+
return r > 0 && typeof rows[r - 1]?.[i] === "number" || r === 0 && i > 0 ? cell.padStart(width) : cell.padEnd(width);
|
|
143
|
+
}).join(" ").trimEnd()).join("\n");
|
|
144
|
+
}
|
|
145
|
+
/** Posix path of `file` relative to `root` (absolute when outside it), as diagnostics store paths. */
|
|
146
|
+
function rootRelative(root, file) {
|
|
147
|
+
const rel = relative(root, file);
|
|
148
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return file.split(sep).join("/");
|
|
149
|
+
return rel.split(sep).join("/");
|
|
150
|
+
}
|
|
151
|
+
function countBySeverity(list) {
|
|
152
|
+
const counts = {
|
|
153
|
+
error: 0,
|
|
154
|
+
warning: 0,
|
|
155
|
+
info: 0
|
|
156
|
+
};
|
|
157
|
+
for (const diagnostic of list) counts[diagnostic.severity]++;
|
|
158
|
+
return counts;
|
|
159
|
+
}
|
|
160
|
+
function plural(count, one, many = one + "s") {
|
|
161
|
+
return `${count} ${count === 1 ? one : many}`;
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
export { table as a, rootRelative as i, createOutput as n, plural as r, countBySeverity as t };
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { a as Message, b as Values, s as MessageRef } from "./types-DsdWBG0b.js";
|
|
2
|
+
//#region src/parse/index.d.ts
|
|
3
|
+
interface ParseOptions {
|
|
4
|
+
/**
|
|
5
|
+
* `"auto"` (default): ICU MessageFormat, or MessageFormat 2 when the text
|
|
6
|
+
* is an MF2 complex message (it starts with `.input`, `.local`, `.match`
|
|
7
|
+
* or `{{`). `"mf2"` reads simple MF2 messages too; `"icu"` never reads MF2.
|
|
8
|
+
*/
|
|
9
|
+
readonly syntax?: "auto" | "icu" | "mf2";
|
|
10
|
+
/**
|
|
11
|
+
* The text this one translates. The message is then checked as a
|
|
12
|
+
* translation (the same arguments and tags, as `vi18n check` requires), and
|
|
13
|
+
* renders the source while it has errors.
|
|
14
|
+
*/
|
|
15
|
+
readonly source?: string;
|
|
16
|
+
/** The text's locale: adds the plural-category warnings, which depend on it. */
|
|
17
|
+
readonly locale?: string;
|
|
18
|
+
}
|
|
19
|
+
/** A problem found in a message: errors keep it from rendering, warnings do not. */
|
|
20
|
+
interface ParseIssue {
|
|
21
|
+
/** A vi18n diagnostic code, such as `icu-syntax` or `icu-argument-mismatch`. */
|
|
22
|
+
readonly code: string;
|
|
23
|
+
readonly severity: "error" | "warning";
|
|
24
|
+
readonly message: string;
|
|
25
|
+
/** 1-based position in the text (after NFC and LF normalisation), when known. */
|
|
26
|
+
readonly line?: number;
|
|
27
|
+
readonly column?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* What `parse()` returns: `t()`, `rich()`, `richHtml()`, `richParts()` and
|
|
31
|
+
* `format()` render it. Plain data, so it survives `JSON.stringify` and
|
|
32
|
+
* `structuredClone`: parse on the server, render on the client without the
|
|
33
|
+
* parser.
|
|
34
|
+
*/
|
|
35
|
+
interface ParsedMessage extends MessageRef<[values?: Values]> {
|
|
36
|
+
/** Never a catalog id: `t()` renders `ir` without looking anything up. */
|
|
37
|
+
readonly id: "";
|
|
38
|
+
/** The text as given. */
|
|
39
|
+
readonly message: string;
|
|
40
|
+
/** What renders: the compiled message, or the fallback while it has errors. */
|
|
41
|
+
readonly ir: Message;
|
|
42
|
+
readonly errors: readonly ParseIssue[];
|
|
43
|
+
readonly warnings: readonly ParseIssue[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parses and validates `message`. Never throws: errors are in `errors`, and
|
|
47
|
+
* the result still renders (see the module comment).
|
|
48
|
+
*/
|
|
49
|
+
export declare function parse(message: string, options?: ParseOptions): ParsedMessage;
|
|
50
|
+
/**
|
|
51
|
+
* Renders `message` as plain text (tags render their children): in the
|
|
52
|
+
* current locale, as `t(parse(message), values)` does, or in `locale` with
|
|
53
|
+
* the current time zone.
|
|
54
|
+
*/
|
|
55
|
+
export declare function format(message: string | ParsedMessage, values?: Values, locale?: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Calls `handler` with every message `parse()` finds errors in, once per
|
|
58
|
+
* text and options while it is cached; returns the unsubscribe function.
|
|
59
|
+
*/
|
|
60
|
+
export declare function onParseError(handler: (message: ParsedMessage) => void): () => void;
|
|
61
|
+
//#endregion
|
|
62
|
+
export type { ParseIssue, ParseOptions, ParsedMessage };
|