@babelize/engine 0.3.0 → 0.3.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/dist/local/hosted-provider.d.ts +4 -0
- package/dist/local/hosted-provider.js +8 -1
- package/dist/local/report.d.ts +2 -0
- package/dist/local/report.js +1 -0
- package/dist/local/translate-core.d.ts +2 -0
- package/dist/local/translate-core.js +1 -0
- package/dist/modules/translator/icu-branches.d.ts +7 -0
- package/dist/modules/translator/icu-branches.js +29 -3
- package/dist/utils/icu-message.js +25 -5
- package/package.json +1 -1
|
@@ -16,6 +16,8 @@ export declare const HOSTED_MAX_BATCH: 100;
|
|
|
16
16
|
export declare const HOSTED_REQUEST_TIMEOUT_MS = 150000;
|
|
17
17
|
/** Response header naming the model that served a batch. */
|
|
18
18
|
export declare const HOSTED_MODEL_HEADER = "x-babelize-model";
|
|
19
|
+
/** Whole credits the server charged for one batch. */
|
|
20
|
+
export declare const HOSTED_CREDITS_HEADER = "x-credits-charged";
|
|
19
21
|
export declare const API_KEYS_URL = "https://app.babelize.co/api-keys";
|
|
20
22
|
export declare const BILLING_URL = "https://app.babelize.co/billing";
|
|
21
23
|
export type HostedErrorCode =
|
|
@@ -75,6 +77,8 @@ export declare class HostedTranslationProvider implements TranslationProvider {
|
|
|
75
77
|
* before sending, as it does for cloud runs. Set by detectCapabilities().
|
|
76
78
|
*/
|
|
77
79
|
plainText: boolean;
|
|
80
|
+
/** Credits charged so far by this provider's batches (X-Credits-Charged). */
|
|
81
|
+
creditsCharged: number;
|
|
78
82
|
/** Set once a request fails in a way no retry can fix (bad key, no credits). */
|
|
79
83
|
fatalError?: HostedTranslationError;
|
|
80
84
|
private readonly opts;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HostedTranslationProvider = exports.HostedTranslationError = exports.BILLING_URL = exports.API_KEYS_URL = exports.HOSTED_MODEL_HEADER = exports.HOSTED_REQUEST_TIMEOUT_MS = exports.HOSTED_MAX_BATCH = void 0;
|
|
3
|
+
exports.HostedTranslationProvider = exports.HostedTranslationError = exports.BILLING_URL = exports.API_KEYS_URL = exports.HOSTED_CREDITS_HEADER = exports.HOSTED_MODEL_HEADER = exports.HOSTED_REQUEST_TIMEOUT_MS = exports.HOSTED_MAX_BATCH = void 0;
|
|
4
4
|
exports.parseRetryAfterMs = parseRetryAfterMs;
|
|
5
5
|
const node_crypto_1 = require("node:crypto");
|
|
6
6
|
const language_codes_1 = require("../modules/translator/language-codes");
|
|
@@ -22,6 +22,8 @@ exports.HOSTED_MAX_BATCH = types_1.TRANSLATE_BATCH_LIMITS.maxEntries;
|
|
|
22
22
|
exports.HOSTED_REQUEST_TIMEOUT_MS = 150000;
|
|
23
23
|
/** Response header naming the model that served a batch. */
|
|
24
24
|
exports.HOSTED_MODEL_HEADER = 'x-babelize-model';
|
|
25
|
+
/** Whole credits the server charged for one batch. */
|
|
26
|
+
exports.HOSTED_CREDITS_HEADER = 'x-credits-charged';
|
|
25
27
|
exports.API_KEYS_URL = 'https://app.babelize.co/api-keys';
|
|
26
28
|
exports.BILLING_URL = 'https://app.babelize.co/billing';
|
|
27
29
|
class HostedTranslationError extends Error {
|
|
@@ -55,6 +57,8 @@ class HostedTranslationProvider {
|
|
|
55
57
|
* before sending, as it does for cloud runs. Set by detectCapabilities().
|
|
56
58
|
*/
|
|
57
59
|
this.plainText = false;
|
|
60
|
+
/** Credits charged so far by this provider's batches (X-Credits-Charged). */
|
|
61
|
+
this.creditsCharged = 0;
|
|
58
62
|
this.token = options.apiKey;
|
|
59
63
|
this.opts = {
|
|
60
64
|
fetch: globalThis.fetch.bind(globalThis),
|
|
@@ -194,6 +198,9 @@ class HostedTranslationProvider {
|
|
|
194
198
|
throw new HostedTranslationError(`Lost the response from Babelize at ${this.opts.apiUrl} (${err.message}).`, 'hosted_unavailable');
|
|
195
199
|
}
|
|
196
200
|
this.model = res.headers.get(exports.HOSTED_MODEL_HEADER) ?? this.model;
|
|
201
|
+
const charged = Number(res.headers.get(exports.HOSTED_CREDITS_HEADER));
|
|
202
|
+
if (Number.isFinite(charged) && charged > 0)
|
|
203
|
+
this.creditsCharged += charged;
|
|
197
204
|
return Array.isArray(data.results) ? data.results : [];
|
|
198
205
|
}
|
|
199
206
|
// An expired session: refresh it once and try again with the same key.
|
package/dist/local/report.d.ts
CHANGED
package/dist/local/report.js
CHANGED
|
@@ -107,6 +107,7 @@ function buildTranslateReport(r) {
|
|
|
107
107
|
framework: r.framework,
|
|
108
108
|
frameworkStatus: r.frameworkStatus,
|
|
109
109
|
provider: r.provider ? { provider: r.provider.provider, name: r.provider.name, ...(r.provider.model ? { model: r.provider.model } : {}) } : null,
|
|
110
|
+
...(r.creditsCharged !== undefined ? { creditsCharged: r.creditsCharged } : {}),
|
|
110
111
|
sourceLocale: r.sourceLocale,
|
|
111
112
|
targetLocales: r.targetLocales,
|
|
112
113
|
keptLocales: r.keptLocales,
|
|
@@ -123,6 +123,8 @@ export interface LocalTranslateReport {
|
|
|
123
123
|
name: string;
|
|
124
124
|
model?: string;
|
|
125
125
|
};
|
|
126
|
+
/** Babelize credits this run was charged; set for hosted runs only. */
|
|
127
|
+
creditsCharged?: number;
|
|
126
128
|
sourceLocale: string;
|
|
127
129
|
/** Locales translated in this run: `--to` plus earlier locales (unless replace). */
|
|
128
130
|
targetLocales: string[];
|
|
@@ -462,6 +462,7 @@ async function runScoped(opts, deps, engineLog, state) {
|
|
|
462
462
|
framework,
|
|
463
463
|
frameworkStatus,
|
|
464
464
|
provider,
|
|
465
|
+
...(deps.hosted ? { creditsCharged: deps.hosted.creditsCharged } : {}),
|
|
465
466
|
sourceLocale: opts.sourceLocale,
|
|
466
467
|
targetLocales,
|
|
467
468
|
keptLocales,
|
|
@@ -49,6 +49,13 @@ interface NumberSlot {
|
|
|
49
49
|
* instead of showing it, and the branch is still exact.
|
|
50
50
|
*/
|
|
51
51
|
mayBeSpelledOut: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Words for "one" in the target language, when this slot is the number 1 in
|
|
54
|
+
* a branch that also covers other numbers (French "one" is 0 and 1). The
|
|
55
|
+
* translator often writes "Vous avez un article" for "You have 1 item"; the
|
|
56
|
+
* word is put back as the count, which keeps the branch right for 0 too.
|
|
57
|
+
*/
|
|
58
|
+
oneWords?: string[];
|
|
52
59
|
}
|
|
53
60
|
export interface BranchUnit {
|
|
54
61
|
/** Unique within the plan. */
|
|
@@ -54,6 +54,21 @@ const CANDIDATES = (() => {
|
|
|
54
54
|
const ARG_SENTINEL = /\uE001(\d+)\uE001/g;
|
|
55
55
|
const POUND_SENTINEL = '\uE002';
|
|
56
56
|
const TOKEN_PATTERN = /\[\s*#\s*VAR_\d+\s*#\s*\]/gi;
|
|
57
|
+
/** Spelled-out "one" (as an article or numeral) in languages whose "one" category has several numbers. */
|
|
58
|
+
const ONE_WORDS = {
|
|
59
|
+
fr: ['un', 'une'],
|
|
60
|
+
es: ['un', 'una', 'uno'],
|
|
61
|
+
pt: ['um', 'uma'],
|
|
62
|
+
it: ['un', 'uno', 'una'],
|
|
63
|
+
ca: ['un', 'una'],
|
|
64
|
+
ro: ['un', 'o'],
|
|
65
|
+
hi: ['एक'],
|
|
66
|
+
bn: ['এক', 'একটি'],
|
|
67
|
+
gu: ['એક'],
|
|
68
|
+
mr: ['एक'],
|
|
69
|
+
fa: ['یک'],
|
|
70
|
+
hy: ['մեկ'],
|
|
71
|
+
};
|
|
57
72
|
class PlanError extends Error {
|
|
58
73
|
}
|
|
59
74
|
/** Plural categories of a locale, CLDR order. */
|
|
@@ -218,7 +233,8 @@ function makeUnit(nodes, path, state) {
|
|
|
218
233
|
used.add(pick);
|
|
219
234
|
// A plural added over a plain count must show it in every branch (compareIcuMessages), so no spelling out there.
|
|
220
235
|
const mayBeSpelledOut = numbers.length === 1 && !(state.options.pluralizeArgs ?? []).includes(occ.owner.name);
|
|
221
|
-
|
|
236
|
+
const oneWords = pick === '1' && !mayBeSpelledOut ? ONE_WORDS[state.targetLocale.split(/[-_]/)[0].toLowerCase()] : undefined;
|
|
237
|
+
slot = { text: pick, count: 0, restore: occ.restore, label: occ.label, mayBeSpelledOut, ...(oneWords ? { oneWords } : {}) };
|
|
222
238
|
slotsByKind.set(kindKey, slot);
|
|
223
239
|
}
|
|
224
240
|
slot.count++;
|
|
@@ -287,10 +303,20 @@ function restoreNumbers(translated, slots) {
|
|
|
287
303
|
return slot.restore;
|
|
288
304
|
});
|
|
289
305
|
}).join('');
|
|
306
|
+
let text = out;
|
|
290
307
|
for (const slot of slots) {
|
|
291
|
-
|
|
308
|
+
let n = found.get(slot) ?? 0;
|
|
292
309
|
if (n === 0 && slot.mayBeSpelledOut)
|
|
293
310
|
continue;
|
|
311
|
+
if (n === 0 && slot.count === 1 && slot.oneWords) {
|
|
312
|
+
// "Vous avez un article" → "Vous avez # article", when the word is there exactly once.
|
|
313
|
+
const word = new RegExp(`(?<![\\p{L}\\p{M}])(?:${slot.oneWords.join('|')})(?![\\p{L}\\p{M}])`, 'giu');
|
|
314
|
+
const hits = text.match(word) ?? [];
|
|
315
|
+
if (hits.length === 1) {
|
|
316
|
+
text = text.replace(word, slot.restore);
|
|
317
|
+
n = 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
294
320
|
if (n !== slot.count) {
|
|
295
321
|
return {
|
|
296
322
|
problem: n === 0
|
|
@@ -299,7 +325,7 @@ function restoreNumbers(translated, slots) {
|
|
|
299
325
|
};
|
|
300
326
|
}
|
|
301
327
|
}
|
|
302
|
-
return { text
|
|
328
|
+
return { text };
|
|
303
329
|
}
|
|
304
330
|
/** ICU-escape literal text: apostrophes before syntax, and `{`, `}` (and `#` in a plural) quoted. */
|
|
305
331
|
function escapeIcuText(text, inPlural) {
|
|
@@ -208,6 +208,23 @@ function integersOf(locale, type, category) {
|
|
|
208
208
|
return [];
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Whether ordinary counts ever pick `category`: a whole number under 1000 or
|
|
213
|
+
* a simple fraction. Spanish, French, Italian, Portuguese and Catalan `many`
|
|
214
|
+
* is only reached by exact millions ("1 millón de archivos"), where `other`
|
|
215
|
+
* reads fine, so a missing branch for it isn't worth a warning.
|
|
216
|
+
*/
|
|
217
|
+
function reachedByOrdinaryCounts(locale, type, category) {
|
|
218
|
+
if (integersOf(locale, type, category).length > 0)
|
|
219
|
+
return true;
|
|
220
|
+
try {
|
|
221
|
+
const rules = new Intl.PluralRules(locale, { type });
|
|
222
|
+
return [0.5, 1.5, 2.5, 3.5, 0.1, 1.1, 2.1, 0.25].some((n) => rules.select(n) === category);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
211
228
|
/**
|
|
212
229
|
* Compare a translated ICU message to its source. Errors mean the message is
|
|
213
230
|
* broken at runtime (unparseable, arguments renamed, select branches changed,
|
|
@@ -326,11 +343,14 @@ function compareIcuMessages(source, translated, targetLocale, options = {}) {
|
|
|
326
343
|
});
|
|
327
344
|
}
|
|
328
345
|
else {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
346
|
+
const noticeable = missing.filter((c) => reachedByOrdinaryCounts(targetLocale, type, c));
|
|
347
|
+
if (noticeable.length) {
|
|
348
|
+
issues.push({
|
|
349
|
+
severity: 'warning',
|
|
350
|
+
code: 'icu_plural_category_missing',
|
|
351
|
+
message: `{${target.name}, ${target.kind}} has no [${noticeable.join(', ')}] branch for ${targetLocale}; "other" will be used`,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
334
354
|
}
|
|
335
355
|
}
|
|
336
356
|
}
|
package/package.json
CHANGED