solid-translate 1.4.5 → 1.5.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/README.md +15 -0
- package/dist/{chunk-36PYT2MX.js → chunk-VAKU3D24.js} +83 -44
- package/dist/cli.js +11 -6
- package/dist/{translate-PTFH3PFU.js → translate-FXWVMHH2.js} +5 -1
- package/dist/vite.js +81 -44
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -409,6 +409,7 @@ on every pull request.
|
|
|
409
409
|
"provider": "openrouter",
|
|
410
410
|
"model": "openai/gpt-4o-mini",
|
|
411
411
|
"batchSize": 50,
|
|
412
|
+
"maxAttempts": 3,
|
|
412
413
|
"include": ["src/**/*.tsx", "src/**/*.ts"],
|
|
413
414
|
"files": {
|
|
414
415
|
"json": {
|
|
@@ -424,6 +425,20 @@ on every pull request.
|
|
|
424
425
|
}
|
|
425
426
|
```
|
|
426
427
|
|
|
428
|
+
### Reliability: retries, splitting and batch size
|
|
429
|
+
|
|
430
|
+
Every batch is retried with exponential backoff (default `maxAttempts: 3`), and each
|
|
431
|
+
retry asks only for the keys the model has not translated yet. A batch that comes back
|
|
432
|
+
with nothing usable — typically a response truncated mid-JSON — is split in half and
|
|
433
|
+
each half recovers on its own, down to single keys. Only keys that fail every attempt
|
|
434
|
+
are reported, and they are left out of the lock file so the next `translate` run
|
|
435
|
+
retries exactly those.
|
|
436
|
+
|
|
437
|
+
The output-token ceiling scales with both the number of keys and the amount of source
|
|
438
|
+
text in the batch. If your strings are long paragraphs, a smaller `batchSize` (8–10)
|
|
439
|
+
keeps each response well inside model limits and makes a single bad response cheap to
|
|
440
|
+
retry.
|
|
441
|
+
|
|
427
442
|
The `[locale]` placeholder is replaced with each target locale. Source files are found by replacing `[locale]` with the source locale.
|
|
428
443
|
|
|
429
444
|
### Environment Variables
|
|
@@ -41,9 +41,31 @@ function collectBatchTranslations(parsed, requestedKeys) {
|
|
|
41
41
|
}
|
|
42
42
|
return { translations, missing };
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
45
|
+
var DEFAULT_BASE_DELAY_MS = 500;
|
|
46
|
+
function batchMaxTokens(entries) {
|
|
47
|
+
const keyCount = Object.keys(entries).length;
|
|
48
|
+
let sourceChars = 0;
|
|
49
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
50
|
+
sourceChars += key.length + value.length;
|
|
51
|
+
}
|
|
52
|
+
return Math.min(32e3, 2e3 + 400 * keyCount + Math.ceil(sourceChars * 1.5));
|
|
53
|
+
}
|
|
54
|
+
function backoffDelay(attempt, baseDelayMs) {
|
|
55
|
+
const exp = Math.min(3e4, baseDelayMs * 2 ** Math.max(0, attempt - 1));
|
|
56
|
+
return Math.round(exp * (0.75 + Math.random() * 0.5));
|
|
57
|
+
}
|
|
58
|
+
function defaultSleep(ms) {
|
|
59
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
60
|
+
}
|
|
61
|
+
async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts, options = {}) {
|
|
45
62
|
const keys = Object.keys(entries);
|
|
46
63
|
if (keys.length === 0) return {};
|
|
64
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
|
|
65
|
+
const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
66
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
67
|
+
const log = options.log ?? (() => {
|
|
68
|
+
});
|
|
47
69
|
const defaultSystem = [
|
|
48
70
|
`You are a professional translator specializing in software localization.`,
|
|
49
71
|
`Translate text from "${sourceLocale}" to "${targetLocale}".`,
|
|
@@ -54,57 +76,72 @@ async function translateBatch(model, entries, targetLocale, sourceLocale, system
|
|
|
54
76
|
`- Do not add or remove content`,
|
|
55
77
|
`- Return natural, idiomatic translations`
|
|
56
78
|
].join("\n");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const contextLines = Object.entries(contexts).filter(([key]) => key in
|
|
60
|
-
if (contextLines.length
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
``
|
|
66
|
-
].join("\n");
|
|
67
|
-
}
|
|
68
|
-
}
|
|
79
|
+
const contextSectionFor = (subset) => {
|
|
80
|
+
if (!contexts || Object.keys(contexts).length === 0) return "";
|
|
81
|
+
const contextLines = Object.entries(contexts).filter(([key]) => key in subset).map(([key, ctx]) => ` "${key}": ${ctx}`);
|
|
82
|
+
if (contextLines.length === 0) return "";
|
|
83
|
+
return [``, `Context hints for disambiguation:`, ...contextLines, ``].join(
|
|
84
|
+
"\n"
|
|
85
|
+
);
|
|
86
|
+
};
|
|
69
87
|
const generateText = await loadGenerateText();
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
`Respond with ONLY a JSON object \u2014 no prose, no code fences \u2014 containing the exact same keys and the translated values.`,
|
|
73
|
-
contextSection,
|
|
74
|
-
JSON.stringify(entries, null, 2)
|
|
75
|
-
].join("\n");
|
|
76
|
-
const attempt = async (prompt, keyCount) => {
|
|
88
|
+
const attempt = async (subset) => {
|
|
89
|
+
const subsetKeys = Object.keys(subset);
|
|
77
90
|
const { text } = await generateText({
|
|
78
91
|
model,
|
|
79
92
|
system: systemPrompt || defaultSystem,
|
|
80
|
-
prompt
|
|
81
|
-
// Provider-default output ceilings (often 4k tokens) truncate large
|
|
82
|
-
// batches mid-JSON ("Unterminated string in JSON"). Scale the ceiling
|
|
83
|
-
// with batch size so the full object always fits.
|
|
84
|
-
maxTokens: Math.min(32e3, 2e3 + 400 * keyCount)
|
|
85
|
-
});
|
|
86
|
-
return collectBatchTranslations(extractJsonObject(text), keys);
|
|
87
|
-
};
|
|
88
|
-
let { translations, missing } = await attempt(basePrompt, keys.length);
|
|
89
|
-
if (missing.length > 0) {
|
|
90
|
-
const retryEntries = {};
|
|
91
|
-
for (const key of missing) retryEntries[key] = entries[key];
|
|
92
|
-
const retry = await attempt(
|
|
93
|
-
[
|
|
93
|
+
prompt: [
|
|
94
94
|
`Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
|
|
95
95
|
`Respond with ONLY a JSON object \u2014 no prose, no code fences \u2014 containing the exact same keys and the translated values.`,
|
|
96
|
-
|
|
97
|
-
JSON.stringify(
|
|
96
|
+
contextSectionFor(subset),
|
|
97
|
+
JSON.stringify(subset, null, 2)
|
|
98
98
|
].join("\n"),
|
|
99
|
-
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
maxTokens: batchMaxTokens(subset)
|
|
100
|
+
});
|
|
101
|
+
return collectBatchTranslations(extractJsonObject(text), subsetKeys);
|
|
102
|
+
};
|
|
103
|
+
const translations = {};
|
|
104
|
+
const failed = [];
|
|
105
|
+
let lastError;
|
|
106
|
+
const recover = async (pending, depth) => {
|
|
107
|
+
let remaining = pending;
|
|
108
|
+
for (let n = 1; n <= maxAttempts && remaining.length > 0; n++) {
|
|
109
|
+
const subset = {};
|
|
110
|
+
for (const key of remaining) subset[key] = entries[key];
|
|
111
|
+
let missing;
|
|
112
|
+
try {
|
|
113
|
+
const result = await attempt(subset);
|
|
114
|
+
Object.assign(translations, result.translations);
|
|
115
|
+
missing = result.missing;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
lastError = err;
|
|
118
|
+
missing = remaining;
|
|
119
|
+
log(
|
|
120
|
+
`translate ${targetLocale}: attempt ${n}/${maxAttempts} for ${remaining.length} key(s) failed: ${err instanceof Error ? err.message : String(err)}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (missing.length === 0) return;
|
|
124
|
+
const progressed = missing.length < remaining.length;
|
|
125
|
+
remaining = missing;
|
|
126
|
+
if (!progressed && remaining.length > 1) {
|
|
127
|
+
const mid = Math.ceil(remaining.length / 2);
|
|
128
|
+
log(
|
|
129
|
+
`translate ${targetLocale}: splitting ${remaining.length} key(s) into ${mid} + ${remaining.length - mid}`
|
|
130
|
+
);
|
|
131
|
+
await recover(remaining.slice(0, mid), depth + 1);
|
|
132
|
+
await recover(remaining.slice(mid), depth + 1);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (n < maxAttempts) await sleep(backoffDelay(n, baseDelayMs));
|
|
136
|
+
}
|
|
137
|
+
failed.push(...remaining);
|
|
138
|
+
};
|
|
139
|
+
await recover(keys, 0);
|
|
140
|
+
if (failed.length > 0) {
|
|
141
|
+
const sample = failed.slice(0, 3).join('", "');
|
|
142
|
+
const cause = lastError instanceof Error ? ` (last error: ${lastError.message})` : "";
|
|
106
143
|
throw new Error(
|
|
107
|
-
`model returned no translation for ${
|
|
144
|
+
`model returned no translation for ${failed.length} of ${keys.length} keys after ${maxAttempts} attempts (e.g. "${sample}")${cause}`
|
|
108
145
|
);
|
|
109
146
|
}
|
|
110
147
|
return translations;
|
|
@@ -144,6 +181,8 @@ async function translateMarkdown(model, content, targetLocale, sourceLocale, sys
|
|
|
144
181
|
export {
|
|
145
182
|
extractJsonObject,
|
|
146
183
|
collectBatchTranslations,
|
|
184
|
+
batchMaxTokens,
|
|
185
|
+
backoffDelay,
|
|
147
186
|
translateBatch,
|
|
148
187
|
translateMarkdown
|
|
149
188
|
};
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
translateBatch
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-VAKU3D24.js";
|
|
5
5
|
import {
|
|
6
6
|
__commonJS,
|
|
7
7
|
__toESM
|
|
@@ -15570,13 +15570,14 @@ Run \`solid-translate translate\` to refresh ${sourceLocale} \u2192 targets.`
|
|
|
15570
15570
|
);
|
|
15571
15571
|
}
|
|
15572
15572
|
async function runTranslate() {
|
|
15573
|
-
const { translateBatch: translateBatch2, translateMarkdown: translateMarkdown2 } = await import("./translate-
|
|
15573
|
+
const { translateBatch: translateBatch2, translateMarkdown: translateMarkdown2 } = await import("./translate-FXWVMHH2.js");
|
|
15574
15574
|
const config = await loadConfig();
|
|
15575
15575
|
const root = process.cwd();
|
|
15576
15576
|
const sourceLocale = config.sourceLocale || "en";
|
|
15577
15577
|
const targetLocales = config.targetLocales;
|
|
15578
15578
|
const localesDir = resolve(config.localesDir || "./src/locales");
|
|
15579
15579
|
const batchSize = config.batchSize || 50;
|
|
15580
|
+
const maxAttempts = config.maxAttempts || 3;
|
|
15580
15581
|
if (!targetLocales || targetLocales.length === 0) {
|
|
15581
15582
|
console.error("No targetLocales configured.");
|
|
15582
15583
|
process.exit(1);
|
|
@@ -15588,7 +15589,8 @@ async function runTranslate() {
|
|
|
15588
15589
|
sourceLocale,
|
|
15589
15590
|
targetLocales,
|
|
15590
15591
|
batchSize,
|
|
15591
|
-
config.systemPrompt
|
|
15592
|
+
config.systemPrompt,
|
|
15593
|
+
maxAttempts
|
|
15592
15594
|
);
|
|
15593
15595
|
if (config.files) {
|
|
15594
15596
|
const { glob } = await import("glob");
|
|
@@ -15628,7 +15630,9 @@ async function runTranslate() {
|
|
|
15628
15630
|
sourceDict,
|
|
15629
15631
|
targetLocale,
|
|
15630
15632
|
sourceLocale,
|
|
15631
|
-
config.systemPrompt
|
|
15633
|
+
config.systemPrompt,
|
|
15634
|
+
void 0,
|
|
15635
|
+
{ maxAttempts, log: (message) => console.log(message) }
|
|
15632
15636
|
);
|
|
15633
15637
|
mkdirSync(dirname(actualTarget), { recursive: true });
|
|
15634
15638
|
writeFileSync2(
|
|
@@ -15672,7 +15676,7 @@ async function runTranslate() {
|
|
|
15672
15676
|
}
|
|
15673
15677
|
console.log("\nTranslation complete.");
|
|
15674
15678
|
}
|
|
15675
|
-
async function translateLocaleFiles(model, localesDir, sourceLocale, targetLocales, batchSize, systemPrompt) {
|
|
15679
|
+
async function translateLocaleFiles(model, localesDir, sourceLocale, targetLocales, batchSize, systemPrompt, maxAttempts) {
|
|
15676
15680
|
const result = await syncLocaleFiles({
|
|
15677
15681
|
localesDir,
|
|
15678
15682
|
sourceLocale,
|
|
@@ -15684,7 +15688,8 @@ async function translateLocaleFiles(model, localesDir, sourceLocale, targetLocal
|
|
|
15684
15688
|
targetLocale,
|
|
15685
15689
|
sourceLocale,
|
|
15686
15690
|
systemPrompt,
|
|
15687
|
-
contexts
|
|
15691
|
+
contexts,
|
|
15692
|
+
{ maxAttempts, log: (message) => console.log(` ${message}`) }
|
|
15688
15693
|
),
|
|
15689
15694
|
log: (message) => console.log(message)
|
|
15690
15695
|
});
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
backoffDelay,
|
|
4
|
+
batchMaxTokens,
|
|
3
5
|
collectBatchTranslations,
|
|
4
6
|
extractJsonObject,
|
|
5
7
|
translateBatch,
|
|
6
8
|
translateMarkdown
|
|
7
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-VAKU3D24.js";
|
|
8
10
|
import "./chunk-FYS2JH42.js";
|
|
9
11
|
export {
|
|
12
|
+
backoffDelay,
|
|
13
|
+
batchMaxTokens,
|
|
10
14
|
collectBatchTranslations,
|
|
11
15
|
extractJsonObject,
|
|
12
16
|
translateBatch,
|
package/dist/vite.js
CHANGED
|
@@ -14652,9 +14652,31 @@ function collectBatchTranslations(parsed, requestedKeys) {
|
|
|
14652
14652
|
}
|
|
14653
14653
|
return { translations, missing };
|
|
14654
14654
|
}
|
|
14655
|
-
|
|
14655
|
+
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
14656
|
+
var DEFAULT_BASE_DELAY_MS = 500;
|
|
14657
|
+
function batchMaxTokens(entries) {
|
|
14658
|
+
const keyCount = Object.keys(entries).length;
|
|
14659
|
+
let sourceChars = 0;
|
|
14660
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
14661
|
+
sourceChars += key.length + value.length;
|
|
14662
|
+
}
|
|
14663
|
+
return Math.min(32e3, 2e3 + 400 * keyCount + Math.ceil(sourceChars * 1.5));
|
|
14664
|
+
}
|
|
14665
|
+
function backoffDelay(attempt, baseDelayMs) {
|
|
14666
|
+
const exp = Math.min(3e4, baseDelayMs * 2 ** Math.max(0, attempt - 1));
|
|
14667
|
+
return Math.round(exp * (0.75 + Math.random() * 0.5));
|
|
14668
|
+
}
|
|
14669
|
+
function defaultSleep(ms) {
|
|
14670
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
14671
|
+
}
|
|
14672
|
+
async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts, options = {}) {
|
|
14656
14673
|
const keys = Object.keys(entries);
|
|
14657
14674
|
if (keys.length === 0) return {};
|
|
14675
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
|
|
14676
|
+
const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
14677
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
14678
|
+
const log = options.log ?? (() => {
|
|
14679
|
+
});
|
|
14658
14680
|
const defaultSystem = [
|
|
14659
14681
|
`You are a professional translator specializing in software localization.`,
|
|
14660
14682
|
`Translate text from "${sourceLocale}" to "${targetLocale}".`,
|
|
@@ -14665,57 +14687,72 @@ async function translateBatch(model, entries, targetLocale, sourceLocale, system
|
|
|
14665
14687
|
`- Do not add or remove content`,
|
|
14666
14688
|
`- Return natural, idiomatic translations`
|
|
14667
14689
|
].join("\n");
|
|
14668
|
-
|
|
14669
|
-
|
|
14670
|
-
const contextLines = Object.entries(contexts).filter(([key]) => key in
|
|
14671
|
-
if (contextLines.length
|
|
14672
|
-
|
|
14673
|
-
|
|
14674
|
-
|
|
14675
|
-
|
|
14676
|
-
``
|
|
14677
|
-
].join("\n");
|
|
14678
|
-
}
|
|
14679
|
-
}
|
|
14690
|
+
const contextSectionFor = (subset) => {
|
|
14691
|
+
if (!contexts || Object.keys(contexts).length === 0) return "";
|
|
14692
|
+
const contextLines = Object.entries(contexts).filter(([key]) => key in subset).map(([key, ctx]) => ` "${key}": ${ctx}`);
|
|
14693
|
+
if (contextLines.length === 0) return "";
|
|
14694
|
+
return [``, `Context hints for disambiguation:`, ...contextLines, ``].join(
|
|
14695
|
+
"\n"
|
|
14696
|
+
);
|
|
14697
|
+
};
|
|
14680
14698
|
const generateText = await loadGenerateText();
|
|
14681
|
-
const
|
|
14682
|
-
|
|
14683
|
-
`Respond with ONLY a JSON object \u2014 no prose, no code fences \u2014 containing the exact same keys and the translated values.`,
|
|
14684
|
-
contextSection,
|
|
14685
|
-
JSON.stringify(entries, null, 2)
|
|
14686
|
-
].join("\n");
|
|
14687
|
-
const attempt = async (prompt, keyCount) => {
|
|
14699
|
+
const attempt = async (subset) => {
|
|
14700
|
+
const subsetKeys = Object.keys(subset);
|
|
14688
14701
|
const { text } = await generateText({
|
|
14689
14702
|
model,
|
|
14690
14703
|
system: systemPrompt || defaultSystem,
|
|
14691
|
-
prompt
|
|
14692
|
-
// Provider-default output ceilings (often 4k tokens) truncate large
|
|
14693
|
-
// batches mid-JSON ("Unterminated string in JSON"). Scale the ceiling
|
|
14694
|
-
// with batch size so the full object always fits.
|
|
14695
|
-
maxTokens: Math.min(32e3, 2e3 + 400 * keyCount)
|
|
14696
|
-
});
|
|
14697
|
-
return collectBatchTranslations(extractJsonObject(text), keys);
|
|
14698
|
-
};
|
|
14699
|
-
let { translations, missing } = await attempt(basePrompt, keys.length);
|
|
14700
|
-
if (missing.length > 0) {
|
|
14701
|
-
const retryEntries = {};
|
|
14702
|
-
for (const key of missing) retryEntries[key] = entries[key];
|
|
14703
|
-
const retry = await attempt(
|
|
14704
|
-
[
|
|
14704
|
+
prompt: [
|
|
14705
14705
|
`Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
|
|
14706
14706
|
`Respond with ONLY a JSON object \u2014 no prose, no code fences \u2014 containing the exact same keys and the translated values.`,
|
|
14707
|
-
|
|
14708
|
-
JSON.stringify(
|
|
14707
|
+
contextSectionFor(subset),
|
|
14708
|
+
JSON.stringify(subset, null, 2)
|
|
14709
14709
|
].join("\n"),
|
|
14710
|
-
|
|
14711
|
-
);
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
}
|
|
14715
|
-
|
|
14716
|
-
|
|
14710
|
+
maxTokens: batchMaxTokens(subset)
|
|
14711
|
+
});
|
|
14712
|
+
return collectBatchTranslations(extractJsonObject(text), subsetKeys);
|
|
14713
|
+
};
|
|
14714
|
+
const translations = {};
|
|
14715
|
+
const failed = [];
|
|
14716
|
+
let lastError;
|
|
14717
|
+
const recover = async (pending, depth) => {
|
|
14718
|
+
let remaining = pending;
|
|
14719
|
+
for (let n = 1; n <= maxAttempts && remaining.length > 0; n++) {
|
|
14720
|
+
const subset = {};
|
|
14721
|
+
for (const key of remaining) subset[key] = entries[key];
|
|
14722
|
+
let missing;
|
|
14723
|
+
try {
|
|
14724
|
+
const result = await attempt(subset);
|
|
14725
|
+
Object.assign(translations, result.translations);
|
|
14726
|
+
missing = result.missing;
|
|
14727
|
+
} catch (err) {
|
|
14728
|
+
lastError = err;
|
|
14729
|
+
missing = remaining;
|
|
14730
|
+
log(
|
|
14731
|
+
`translate ${targetLocale}: attempt ${n}/${maxAttempts} for ${remaining.length} key(s) failed: ${err instanceof Error ? err.message : String(err)}`
|
|
14732
|
+
);
|
|
14733
|
+
}
|
|
14734
|
+
if (missing.length === 0) return;
|
|
14735
|
+
const progressed = missing.length < remaining.length;
|
|
14736
|
+
remaining = missing;
|
|
14737
|
+
if (!progressed && remaining.length > 1) {
|
|
14738
|
+
const mid = Math.ceil(remaining.length / 2);
|
|
14739
|
+
log(
|
|
14740
|
+
`translate ${targetLocale}: splitting ${remaining.length} key(s) into ${mid} + ${remaining.length - mid}`
|
|
14741
|
+
);
|
|
14742
|
+
await recover(remaining.slice(0, mid), depth + 1);
|
|
14743
|
+
await recover(remaining.slice(mid), depth + 1);
|
|
14744
|
+
return;
|
|
14745
|
+
}
|
|
14746
|
+
if (n < maxAttempts) await sleep(backoffDelay(n, baseDelayMs));
|
|
14747
|
+
}
|
|
14748
|
+
failed.push(...remaining);
|
|
14749
|
+
};
|
|
14750
|
+
await recover(keys, 0);
|
|
14751
|
+
if (failed.length > 0) {
|
|
14752
|
+
const sample = failed.slice(0, 3).join('", "');
|
|
14753
|
+
const cause = lastError instanceof Error ? ` (last error: ${lastError.message})` : "";
|
|
14717
14754
|
throw new Error(
|
|
14718
|
-
`model returned no translation for ${
|
|
14755
|
+
`model returned no translation for ${failed.length} of ${keys.length} keys after ${maxAttempts} attempts (e.g. "${sample}")${cause}`
|
|
14719
14756
|
);
|
|
14720
14757
|
}
|
|
14721
14758
|
return translations;
|