@babelize/engine 0.2.1 → 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/detection/i18n.d.ts +19 -0
- package/dist/detection/i18n.js +144 -0
- package/dist/detection/stack.d.ts +2 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +9 -2
- package/dist/local/hosted-provider.d.ts +34 -2
- package/dist/local/hosted-provider.js +58 -4
- 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 +5 -0
- package/dist/modules/lockfile/index.d.ts +1 -1
- package/dist/modules/lockfile/index.js +2 -1
- package/dist/modules/lockfile/lockfile-manager.d.ts +9 -0
- package/dist/modules/lockfile/lockfile-manager.js +43 -1
- package/dist/modules/lockfile/schema.d.ts +14 -0
- package/dist/modules/lockfile/schema.js +2 -0
- package/dist/modules/overrides/index.d.ts +41 -0
- package/dist/modules/overrides/index.js +50 -0
- package/dist/modules/translator/budget.d.ts +25 -0
- package/dist/modules/translator/budget.js +46 -0
- package/dist/modules/translator/icu-branches.d.ts +7 -0
- package/dist/modules/translator/icu-branches.js +29 -3
- package/dist/modules/translator/translate.d.ts +11 -0
- package/dist/modules/translator/translate.js +50 -10
- package/dist/orchestrator.js +54 -6
- package/dist/providers/types.d.ts +6 -0
- package/dist/runtime/config.js +1 -0
- package/dist/runtime/types.d.ts +21 -0
- package/dist/utils/icu-message.js +25 -5
- package/dist/utils/translator-caller.js +6 -3
- package/package.json +1 -1
|
@@ -25,6 +25,8 @@ export declare const lockfileEntrySchema: z.ZodObject<{
|
|
|
25
25
|
* catalogue), not from Babelize: glossary edits don't overwrite it.
|
|
26
26
|
*/
|
|
27
27
|
external: z.ZodOptional<z.ZodBoolean>;
|
|
28
|
+
/** Edited in the dashboard (a translation override), not translated by Babelize. */
|
|
29
|
+
manual: z.ZodOptional<z.ZodBoolean>;
|
|
28
30
|
}, "strip", z.ZodTypeAny, {
|
|
29
31
|
sourceHash?: string | undefined;
|
|
30
32
|
fromMemory?: boolean | undefined;
|
|
@@ -32,6 +34,7 @@ export declare const lockfileEntrySchema: z.ZodObject<{
|
|
|
32
34
|
pseudo?: boolean | undefined;
|
|
33
35
|
glossaryHash?: string | undefined;
|
|
34
36
|
external?: boolean | undefined;
|
|
37
|
+
manual?: boolean | undefined;
|
|
35
38
|
}, {
|
|
36
39
|
sourceHash?: string | undefined;
|
|
37
40
|
fromMemory?: boolean | undefined;
|
|
@@ -39,6 +42,7 @@ export declare const lockfileEntrySchema: z.ZodObject<{
|
|
|
39
42
|
pseudo?: boolean | undefined;
|
|
40
43
|
glossaryHash?: string | undefined;
|
|
41
44
|
external?: boolean | undefined;
|
|
45
|
+
manual?: boolean | undefined;
|
|
42
46
|
}>>>;
|
|
43
47
|
}, "strip", z.ZodTypeAny, {
|
|
44
48
|
sourceHash: string;
|
|
@@ -51,6 +55,7 @@ export declare const lockfileEntrySchema: z.ZodObject<{
|
|
|
51
55
|
pseudo?: boolean | undefined;
|
|
52
56
|
glossaryHash?: string | undefined;
|
|
53
57
|
external?: boolean | undefined;
|
|
58
|
+
manual?: boolean | undefined;
|
|
54
59
|
}> | undefined;
|
|
55
60
|
}, {
|
|
56
61
|
sourceHash: string;
|
|
@@ -63,6 +68,7 @@ export declare const lockfileEntrySchema: z.ZodObject<{
|
|
|
63
68
|
pseudo?: boolean | undefined;
|
|
64
69
|
glossaryHash?: string | undefined;
|
|
65
70
|
external?: boolean | undefined;
|
|
71
|
+
manual?: boolean | undefined;
|
|
66
72
|
}> | undefined;
|
|
67
73
|
}>;
|
|
68
74
|
export type LockfileEntry = z.infer<typeof lockfileEntrySchema>;
|
|
@@ -96,6 +102,8 @@ export declare const lockfileDataSchema: z.ZodObject<{
|
|
|
96
102
|
* catalogue), not from Babelize: glossary edits don't overwrite it.
|
|
97
103
|
*/
|
|
98
104
|
external: z.ZodOptional<z.ZodBoolean>;
|
|
105
|
+
/** Edited in the dashboard (a translation override), not translated by Babelize. */
|
|
106
|
+
manual: z.ZodOptional<z.ZodBoolean>;
|
|
99
107
|
}, "strip", z.ZodTypeAny, {
|
|
100
108
|
sourceHash?: string | undefined;
|
|
101
109
|
fromMemory?: boolean | undefined;
|
|
@@ -103,6 +111,7 @@ export declare const lockfileDataSchema: z.ZodObject<{
|
|
|
103
111
|
pseudo?: boolean | undefined;
|
|
104
112
|
glossaryHash?: string | undefined;
|
|
105
113
|
external?: boolean | undefined;
|
|
114
|
+
manual?: boolean | undefined;
|
|
106
115
|
}, {
|
|
107
116
|
sourceHash?: string | undefined;
|
|
108
117
|
fromMemory?: boolean | undefined;
|
|
@@ -110,6 +119,7 @@ export declare const lockfileDataSchema: z.ZodObject<{
|
|
|
110
119
|
pseudo?: boolean | undefined;
|
|
111
120
|
glossaryHash?: string | undefined;
|
|
112
121
|
external?: boolean | undefined;
|
|
122
|
+
manual?: boolean | undefined;
|
|
113
123
|
}>>>;
|
|
114
124
|
}, "strip", z.ZodTypeAny, {
|
|
115
125
|
sourceHash: string;
|
|
@@ -122,6 +132,7 @@ export declare const lockfileDataSchema: z.ZodObject<{
|
|
|
122
132
|
pseudo?: boolean | undefined;
|
|
123
133
|
glossaryHash?: string | undefined;
|
|
124
134
|
external?: boolean | undefined;
|
|
135
|
+
manual?: boolean | undefined;
|
|
125
136
|
}> | undefined;
|
|
126
137
|
}, {
|
|
127
138
|
sourceHash: string;
|
|
@@ -134,6 +145,7 @@ export declare const lockfileDataSchema: z.ZodObject<{
|
|
|
134
145
|
pseudo?: boolean | undefined;
|
|
135
146
|
glossaryHash?: string | undefined;
|
|
136
147
|
external?: boolean | undefined;
|
|
148
|
+
manual?: boolean | undefined;
|
|
137
149
|
}> | undefined;
|
|
138
150
|
}>>;
|
|
139
151
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -148,6 +160,7 @@ export declare const lockfileDataSchema: z.ZodObject<{
|
|
|
148
160
|
pseudo?: boolean | undefined;
|
|
149
161
|
glossaryHash?: string | undefined;
|
|
150
162
|
external?: boolean | undefined;
|
|
163
|
+
manual?: boolean | undefined;
|
|
151
164
|
}> | undefined;
|
|
152
165
|
}>;
|
|
153
166
|
projectId: string;
|
|
@@ -165,6 +178,7 @@ export declare const lockfileDataSchema: z.ZodObject<{
|
|
|
165
178
|
pseudo?: boolean | undefined;
|
|
166
179
|
glossaryHash?: string | undefined;
|
|
167
180
|
external?: boolean | undefined;
|
|
181
|
+
manual?: boolean | undefined;
|
|
168
182
|
}> | undefined;
|
|
169
183
|
}>;
|
|
170
184
|
projectId: string;
|
|
@@ -32,6 +32,8 @@ exports.lockfileEntrySchema = zod_1.z.object({
|
|
|
32
32
|
* catalogue), not from Babelize: glossary edits don't overwrite it.
|
|
33
33
|
*/
|
|
34
34
|
external: zod_1.z.boolean().optional(),
|
|
35
|
+
/** Edited in the dashboard (a translation override), not translated by Babelize. */
|
|
36
|
+
manual: zod_1.z.boolean().optional(),
|
|
35
37
|
})).optional(),
|
|
36
38
|
});
|
|
37
39
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation overrides: translations a person edited in the dashboard.
|
|
3
|
+
*
|
|
4
|
+
* They win over the lockfile, translation memory and the translator, on every
|
|
5
|
+
* run, for as long as the source text is the one that was edited. When the
|
|
6
|
+
* source text changes the edit no longer fits: it is not applied, the key is
|
|
7
|
+
* translated as usual, and the run reports it as replaced.
|
|
8
|
+
*/
|
|
9
|
+
import type { LocaleData } from '../../runtime/types';
|
|
10
|
+
export interface TranslationOverride {
|
|
11
|
+
key: string;
|
|
12
|
+
locale: string;
|
|
13
|
+
text: string;
|
|
14
|
+
/** The source text the edit was made against. */
|
|
15
|
+
sourceText: string;
|
|
16
|
+
}
|
|
17
|
+
export interface OverrideReport {
|
|
18
|
+
/** Edits written into this run's locale files. */
|
|
19
|
+
applied: Array<{
|
|
20
|
+
key: string;
|
|
21
|
+
locale: string;
|
|
22
|
+
}>;
|
|
23
|
+
/** Edits whose source text has changed since: not applied, translated as usual. */
|
|
24
|
+
replaced: Array<{
|
|
25
|
+
key: string;
|
|
26
|
+
locale: string;
|
|
27
|
+
text: string;
|
|
28
|
+
sourceText: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
/** Set `key` in locale data: a literal flat key when the file has one, else a nested path. */
|
|
32
|
+
export declare function setLocaleValue(data: LocaleData, key: string, value: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Apply edits to the run's locale data in place. `source` is the flat source
|
|
35
|
+
* locale (key → text). Edits for a key the run no longer has, or a locale it
|
|
36
|
+
* doesn't write, are left alone (kept for when they come back).
|
|
37
|
+
*/
|
|
38
|
+
export declare function applyTranslationOverrides(locales: Array<{
|
|
39
|
+
locale: string;
|
|
40
|
+
data: LocaleData;
|
|
41
|
+
}>, source: Record<string, string>, overrides: TranslationOverride[]): OverrideReport;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Translation overrides: translations a person edited in the dashboard.
|
|
4
|
+
*
|
|
5
|
+
* They win over the lockfile, translation memory and the translator, on every
|
|
6
|
+
* run, for as long as the source text is the one that was edited. When the
|
|
7
|
+
* source text changes the edit no longer fits: it is not applied, the key is
|
|
8
|
+
* translated as usual, and the run reports it as replaced.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.setLocaleValue = setLocaleValue;
|
|
12
|
+
exports.applyTranslationOverrides = applyTranslationOverrides;
|
|
13
|
+
/** Set `key` in locale data: a literal flat key when the file has one, else a nested path. */
|
|
14
|
+
function setLocaleValue(data, key, value) {
|
|
15
|
+
if (typeof data[key] === 'string' || !key.includes('.')) {
|
|
16
|
+
data[key] = value;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const parts = key.split('.');
|
|
20
|
+
let current = data;
|
|
21
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
22
|
+
const part = parts[i];
|
|
23
|
+
if (typeof current[part] !== 'object' || current[part] === null)
|
|
24
|
+
current[part] = {};
|
|
25
|
+
current = current[part];
|
|
26
|
+
}
|
|
27
|
+
current[parts[parts.length - 1]] = value;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Apply edits to the run's locale data in place. `source` is the flat source
|
|
31
|
+
* locale (key → text). Edits for a key the run no longer has, or a locale it
|
|
32
|
+
* doesn't write, are left alone (kept for when they come back).
|
|
33
|
+
*/
|
|
34
|
+
function applyTranslationOverrides(locales, source, overrides) {
|
|
35
|
+
const report = { applied: [], replaced: [] };
|
|
36
|
+
const byLocale = new Map(locales.map((l) => [l.locale, l]));
|
|
37
|
+
for (const o of overrides) {
|
|
38
|
+
const target = byLocale.get(o.locale);
|
|
39
|
+
const current = source[o.key];
|
|
40
|
+
if (!target || current === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
if (current !== o.sourceText) {
|
|
43
|
+
report.replaced.push({ key: o.key, locale: o.locale, text: o.text, sourceText: o.sourceText });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
setLocaleValue(target.data, o.key, o.text);
|
|
47
|
+
report.applied.push({ key: o.key, locale: o.locale });
|
|
48
|
+
}
|
|
49
|
+
return report;
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A run's translation budget: the most (key, locale) pairs it may send to the
|
|
3
|
+
* translator (the caller's credit budget, see EngineConfigV2.maxTranslations).
|
|
4
|
+
*
|
|
5
|
+
* Locales are translated concurrently, so the budget is granted in slot order
|
|
6
|
+
* (the locale's position in the run) rather than first come, first served:
|
|
7
|
+
* each locale claims what it needs once its cache and translation-memory
|
|
8
|
+
* lookups are done, and a claim is only settled after every earlier slot has
|
|
9
|
+
* claimed. The same project and budget therefore always translate the same
|
|
10
|
+
* pairs: earlier languages are completed first, keys in source order.
|
|
11
|
+
*
|
|
12
|
+
* Every slot must claim exactly once (0 when it has nothing to send), or the
|
|
13
|
+
* slots after it wait forever.
|
|
14
|
+
*/
|
|
15
|
+
export declare class TranslationBudget {
|
|
16
|
+
private remaining;
|
|
17
|
+
private nextSlot;
|
|
18
|
+
private readonly pending;
|
|
19
|
+
constructor(limit: number);
|
|
20
|
+
/** Resolves with how many of `wanted` pairs slot `slot` may translate. */
|
|
21
|
+
claim(slot: number, wanted: number): Promise<number>;
|
|
22
|
+
private settle;
|
|
23
|
+
}
|
|
24
|
+
/** A `maxTranslations` value that caps the run: a finite number >= 0. */
|
|
25
|
+
export declare function isTranslationCap(max: number | undefined): max is number;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TranslationBudget = void 0;
|
|
4
|
+
exports.isTranslationCap = isTranslationCap;
|
|
5
|
+
/**
|
|
6
|
+
* A run's translation budget: the most (key, locale) pairs it may send to the
|
|
7
|
+
* translator (the caller's credit budget, see EngineConfigV2.maxTranslations).
|
|
8
|
+
*
|
|
9
|
+
* Locales are translated concurrently, so the budget is granted in slot order
|
|
10
|
+
* (the locale's position in the run) rather than first come, first served:
|
|
11
|
+
* each locale claims what it needs once its cache and translation-memory
|
|
12
|
+
* lookups are done, and a claim is only settled after every earlier slot has
|
|
13
|
+
* claimed. The same project and budget therefore always translate the same
|
|
14
|
+
* pairs: earlier languages are completed first, keys in source order.
|
|
15
|
+
*
|
|
16
|
+
* Every slot must claim exactly once (0 when it has nothing to send), or the
|
|
17
|
+
* slots after it wait forever.
|
|
18
|
+
*/
|
|
19
|
+
class TranslationBudget {
|
|
20
|
+
constructor(limit) {
|
|
21
|
+
this.nextSlot = 0;
|
|
22
|
+
this.pending = new Map();
|
|
23
|
+
this.remaining = Math.max(0, Math.floor(limit));
|
|
24
|
+
}
|
|
25
|
+
/** Resolves with how many of `wanted` pairs slot `slot` may translate. */
|
|
26
|
+
claim(slot, wanted) {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
this.pending.set(slot, { wanted: Math.max(0, Math.floor(wanted)), resolve });
|
|
29
|
+
this.settle();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
settle() {
|
|
33
|
+
for (let next = this.pending.get(this.nextSlot); next; next = this.pending.get(this.nextSlot)) {
|
|
34
|
+
this.pending.delete(this.nextSlot);
|
|
35
|
+
this.nextSlot++;
|
|
36
|
+
const granted = Math.min(next.wanted, this.remaining);
|
|
37
|
+
this.remaining -= granted;
|
|
38
|
+
next.resolve(granted);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.TranslationBudget = TranslationBudget;
|
|
43
|
+
/** A `maxTranslations` value that caps the run: a finite number >= 0. */
|
|
44
|
+
function isTranslationCap(max) {
|
|
45
|
+
return typeof max === 'number' && Number.isFinite(max) && max >= 0;
|
|
46
|
+
}
|
|
@@ -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) {
|
|
@@ -32,6 +32,13 @@ export interface TranslateOptions {
|
|
|
32
32
|
progress?: {
|
|
33
33
|
freshTranslations: number;
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Most (key, locale) pairs to send to the translator this call (the run's
|
|
37
|
+
* credit budget). Cache, TM and passthrough reuse don't count. Locales are
|
|
38
|
+
* filled in `targetLocales` order, keys in source order; the rest are
|
|
39
|
+
* returned as `skippedForBudget`. Undefined or negative: no cap.
|
|
40
|
+
*/
|
|
41
|
+
maxTranslations?: number;
|
|
35
42
|
}
|
|
36
43
|
export interface LoadedTranslationRules {
|
|
37
44
|
glossary: GlossaryRules;
|
|
@@ -59,6 +66,10 @@ export declare function translateLocales(workspace: WorkspaceInfo, sourceLocale:
|
|
|
59
66
|
error: string;
|
|
60
67
|
}[];
|
|
61
68
|
failures: TranslationFailure[];
|
|
69
|
+
skippedForBudget: {
|
|
70
|
+
key: string;
|
|
71
|
+
locale: string;
|
|
72
|
+
}[];
|
|
62
73
|
};
|
|
63
74
|
stats: {
|
|
64
75
|
tmHits: number;
|
|
@@ -24,6 +24,7 @@ const translation_sanitizer_1 = require("../../utils/translation-sanitizer");
|
|
|
24
24
|
const glossary_match_1 = require("../../utils/glossary-match");
|
|
25
25
|
const icu_spans_1 = require("../../utils/icu-spans");
|
|
26
26
|
const icu_branches_1 = require("./icu-branches");
|
|
27
|
+
const budget_1 = require("./budget");
|
|
27
28
|
const BATCH_SIZE = 15;
|
|
28
29
|
/** QA note on keys the backup (NLLB) translator produced. */
|
|
29
30
|
exports.FALLBACK_QA_WARNING = 'translated with the backup translator; will be retried with the AI translator next run';
|
|
@@ -214,6 +215,7 @@ async function translateLocales(workspace, sourceLocale, targetLocales, forceTra
|
|
|
214
215
|
const locales = [];
|
|
215
216
|
const errors = [];
|
|
216
217
|
const failures = [];
|
|
218
|
+
const skippedForBudget = [];
|
|
217
219
|
let tmHitsTotal = 0;
|
|
218
220
|
const perLocaleMetadata = {};
|
|
219
221
|
// Ensure locale output directories exist for all target locales
|
|
@@ -248,7 +250,23 @@ async function translateLocales(workspace, sourceLocale, targetLocales, forceTra
|
|
|
248
250
|
}
|
|
249
251
|
const localeTasks = targetLocales
|
|
250
252
|
.filter((l) => l !== sourceLocale.locale && (0, language_codes_1.isSupported)(l));
|
|
251
|
-
|
|
253
|
+
// The run's translation budget, granted in locale order (see TranslationBudget).
|
|
254
|
+
const budget = (0, budget_1.isTranslationCap)(options.maxTranslations) ? new budget_1.TranslationBudget(options.maxTranslations) : null;
|
|
255
|
+
// Every locale claims its share of the budget exactly once, even when it has
|
|
256
|
+
// nothing to send or fails, or the locales after it never get theirs.
|
|
257
|
+
const withBudget = (fn) => async (targetLocale, slot) => {
|
|
258
|
+
if (!budget)
|
|
259
|
+
return fn(targetLocale);
|
|
260
|
+
let claimed = false;
|
|
261
|
+
try {
|
|
262
|
+
return await fn(targetLocale, (wanted) => { claimed = true; return budget.claim(slot, wanted); });
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
if (!claimed)
|
|
266
|
+
void budget.claim(slot, 0);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const { results: localeResults, errors: localeErrors } = await (0, concurrent_map_1.concurrentMap)(localeTasks, withBudget(async (targetLocale, claimBudget) => {
|
|
252
270
|
const targetLangCode = (0, language_codes_1.toFlores)(targetLocale);
|
|
253
271
|
const localePaths = (0, file_utils_1.localeFilePath)(workspace.path, workspace.publicDir, workspace.localeFormat, targetLocale);
|
|
254
272
|
const targetPath = localePaths.file;
|
|
@@ -269,6 +287,7 @@ async function translateLocales(workspace, sourceLocale, targetLocales, forceTra
|
|
|
269
287
|
tmHits: 0,
|
|
270
288
|
metadata: {},
|
|
271
289
|
failures: {},
|
|
290
|
+
skippedForBudget: [],
|
|
272
291
|
error: null,
|
|
273
292
|
};
|
|
274
293
|
}
|
|
@@ -276,7 +295,7 @@ async function translateLocales(workspace, sourceLocale, targetLocales, forceTra
|
|
|
276
295
|
// LLM translators (BYOK, or an injected one such as Babelize-hosted)
|
|
277
296
|
// take the brand voice in the prompt; NLLB output is polished after.
|
|
278
297
|
const hasBrandVoiceInPrompt = !!((byokConfig || (providers.translator && !providers.translator.plainText)) && brandVoice);
|
|
279
|
-
const translationResult = await translateEntries(entriesToTranslate, sourceLangCode, targetLangCode, projectId, onProgress, glossary, targetLocale, cacheProvider, byokConfig, contextMap, extractionContextMap, hasBrandVoiceInPrompt ? (brandVoice || undefined) : undefined, dryRun, forceTranslation, providers, rulesComplete, options);
|
|
298
|
+
const translationResult = await translateEntries(entriesToTranslate, sourceLangCode, targetLangCode, projectId, onProgress, glossary, targetLocale, cacheProvider, byokConfig, contextMap, extractionContextMap, hasBrandVoiceInPrompt ? (brandVoice || undefined) : undefined, dryRun, forceTranslation, providers, rulesComplete, options, claimBudget);
|
|
280
299
|
const entriesResult = translationResult.result;
|
|
281
300
|
const batchStats = translationResult.stats;
|
|
282
301
|
const batchMetadata = translationResult.metadata;
|
|
@@ -284,10 +303,13 @@ async function translateLocales(workspace, sourceLocale, targetLocales, forceTra
|
|
|
284
303
|
if (Object.keys(entriesResult).length === 0 && Object.keys(entriesToTranslate).length > 0) {
|
|
285
304
|
// Nothing translated: write no file. Every key is in keyFailures with
|
|
286
305
|
// its reason, so the caller can report exactly what went wrong.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
306
|
+
// (Not an error when the run's budget simply didn't reach this locale.)
|
|
307
|
+
if (Object.keys(keyFailures).length > 0 || translationResult.skippedForBudget.length === 0) {
|
|
308
|
+
logger_1.engineLogger.error(`No strings could be translated for locale "${targetLocale}"`, {
|
|
309
|
+
locale: targetLocale, sourceKeys: Object.keys(entriesToTranslate).length,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return { locale: targetLocale, file: null, tmHits: batchStats.tmHits, metadata: batchMetadata, failures: keyFailures, skippedForBudget: translationResult.skippedForBudget, error: null };
|
|
291
313
|
}
|
|
292
314
|
let translatedEntries = entriesResult;
|
|
293
315
|
// Post-hoc brand voice polish only for the NLLB service; LLM
|
|
@@ -355,15 +377,18 @@ async function translateLocales(workspace, sourceLocale, targetLocales, forceTra
|
|
|
355
377
|
tmHits: batchStats.tmHits,
|
|
356
378
|
metadata: batchMetadata,
|
|
357
379
|
failures: keyFailures,
|
|
380
|
+
skippedForBudget: translationResult.skippedForBudget,
|
|
358
381
|
error: null,
|
|
359
382
|
};
|
|
360
|
-
}, TRANSLATE_CONCURRENCY);
|
|
383
|
+
}), TRANSLATE_CONCURRENCY);
|
|
361
384
|
for (const r of localeResults) {
|
|
362
385
|
if (!r)
|
|
363
386
|
continue;
|
|
364
387
|
for (const [key, f] of Object.entries(r.failures ?? {})) {
|
|
365
388
|
failures.push({ locale: r.locale, key, source: flatSource[key] ?? '', ...f });
|
|
366
389
|
}
|
|
390
|
+
for (const key of r.skippedForBudget ?? [])
|
|
391
|
+
skippedForBudget.push({ key, locale: r.locale });
|
|
367
392
|
if (r.error) {
|
|
368
393
|
errors.push({ locale: r.locale, error: r.error });
|
|
369
394
|
}
|
|
@@ -385,13 +410,19 @@ async function translateLocales(workspace, sourceLocale, targetLocales, forceTra
|
|
|
385
410
|
if (byokConfig && (errors.length > 0 || localeErrors.length > 0)) {
|
|
386
411
|
throw localeErrors.length > 0 ? localeErrors[0] : new Error(errors[0].error);
|
|
387
412
|
}
|
|
413
|
+
if (skippedForBudget.length > 0) {
|
|
414
|
+
logger_1.engineLogger.warn('Translation budget reached; remaining strings are left for the next run', {
|
|
415
|
+
maxTranslations: options.maxTranslations,
|
|
416
|
+
skipped: skippedForBudget.length,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
388
419
|
return {
|
|
389
|
-
translation: { locales, errors, failures },
|
|
420
|
+
translation: { locales, errors, failures, skippedForBudget },
|
|
390
421
|
stats: { tmHits: tmHitsTotal },
|
|
391
422
|
metadata: perLocaleMetadata
|
|
392
423
|
};
|
|
393
424
|
}
|
|
394
|
-
async function translateEntries(entries, sourceLang, targetLang, projectId, onProgress, glossary = glossary_1.EMPTY_GLOSSARY, targetLocale = '', cacheProvider, byokConfig, contextMap, extractionContextMap, brandVoice, dryRun = false, forceTranslation = false, providers = {}, rulesComplete = true, options = {}) {
|
|
425
|
+
async function translateEntries(entries, sourceLang, targetLang, projectId, onProgress, glossary = glossary_1.EMPTY_GLOSSARY, targetLocale = '', cacheProvider, byokConfig, contextMap, extractionContextMap, brandVoice, dryRun = false, forceTranslation = false, providers = {}, rulesComplete = true, options = {}, claimBudget) {
|
|
395
426
|
const result = {};
|
|
396
427
|
const metadata = {};
|
|
397
428
|
const keys = Object.keys(entries);
|
|
@@ -546,6 +577,7 @@ async function translateEntries(entries, sourceLang, targetLang, projectId, onPr
|
|
|
546
577
|
});
|
|
547
578
|
}
|
|
548
579
|
const failures = {};
|
|
580
|
+
const skippedForBudget = [];
|
|
549
581
|
// LLM providers get structure-aware prompts and repair attempts. The NLLB
|
|
550
582
|
// service translates plain text only: ICU messages go branch by branch
|
|
551
583
|
// (./icu-branches), and mixed-script text fails up front.
|
|
@@ -618,6 +650,14 @@ async function translateEntries(entries, sourceLang, targetLang, projectId, onPr
|
|
|
618
650
|
: undefined,
|
|
619
651
|
});
|
|
620
652
|
}
|
|
653
|
+
// 1b. The run's budget: only as many keys as it covers go to the
|
|
654
|
+
// translator (in source order). The rest get no value, like a failed
|
|
655
|
+
// key: not in the locale file or lockfile, so the next run retries them.
|
|
656
|
+
if (claimBudget) {
|
|
657
|
+
const granted = await claimBudget(prepared.length);
|
|
658
|
+
for (const entry of prepared.splice(granted))
|
|
659
|
+
skippedForBudget.push(entry.key);
|
|
660
|
+
}
|
|
621
661
|
// 2. Translate in batches; judge every key independently.
|
|
622
662
|
const outcomes = new Map();
|
|
623
663
|
const judge = (entry, keyResult, nllb = !isLlm) => {
|
|
@@ -970,7 +1010,7 @@ async function translateEntries(entries, sourceLang, targetLang, projectId, onPr
|
|
|
970
1010
|
meta.qa = [...(meta.qa ?? []), ...notes];
|
|
971
1011
|
metadata[key] = meta;
|
|
972
1012
|
}
|
|
973
|
-
return { result, stats: { tmHits }, metadata, failures };
|
|
1013
|
+
return { result, stats: { tmHits }, metadata, failures, skippedForBudget };
|
|
974
1014
|
}
|
|
975
1015
|
/**
|
|
976
1016
|
* Persist translations to the project owner's TM. Never throws: a TM failure
|