@katerynakhar/i18n-keeper 0.16.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 +21 -0
- package/README.md +666 -0
- package/dist/apply.d.ts +66 -0
- package/dist/apply.js +254 -0
- package/dist/apply.js.map +1 -0
- package/dist/check.d.ts +5 -0
- package/dist/check.js +326 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +525 -0
- package/dist/cli.js.map +1 -0
- package/dist/formats/error.d.ts +12 -0
- package/dist/formats/error.js +19 -0
- package/dist/formats/error.js.map +1 -0
- package/dist/formats/flatten.d.ts +6 -0
- package/dist/formats/flatten.js +26 -0
- package/dist/formats/flatten.js.map +1 -0
- package/dist/formats/json-write.d.ts +7 -0
- package/dist/formats/json-write.js +55 -0
- package/dist/formats/json-write.js.map +1 -0
- package/dist/formats/json.d.ts +6 -0
- package/dist/formats/json.js +21 -0
- package/dist/formats/json.js.map +1 -0
- package/dist/formats/php-write.d.ts +16 -0
- package/dist/formats/php-write.js +130 -0
- package/dist/formats/php-write.js.map +1 -0
- package/dist/formats/php.d.ts +44 -0
- package/dist/formats/php.js +356 -0
- package/dist/formats/php.js.map +1 -0
- package/dist/formats/po-write.d.ts +3 -0
- package/dist/formats/po-write.js +200 -0
- package/dist/formats/po-write.js.map +1 -0
- package/dist/formats/po.d.ts +30 -0
- package/dist/formats/po.js +202 -0
- package/dist/formats/po.js.map +1 -0
- package/dist/formats/write.d.ts +29 -0
- package/dist/formats/write.js +19 -0
- package/dist/formats/write.js.map +1 -0
- package/dist/formats/yaml-write.d.ts +12 -0
- package/dist/formats/yaml-write.js +42 -0
- package/dist/formats/yaml-write.js.map +1 -0
- package/dist/formats/yaml.d.ts +18 -0
- package/dist/formats/yaml.js +51 -0
- package/dist/formats/yaml.js.map +1 -0
- package/dist/glossary.d.ts +38 -0
- package/dist/glossary.js +140 -0
- package/dist/glossary.js.map +1 -0
- package/dist/lengths.d.ts +37 -0
- package/dist/lengths.js +195 -0
- package/dist/lengths.js.map +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +374 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory.d.ts +72 -0
- package/dist/memory.js +162 -0
- package/dist/memory.js.map +1 -0
- package/dist/placeholders.d.ts +15 -0
- package/dist/placeholders.js +103 -0
- package/dist/placeholders.js.map +1 -0
- package/dist/plurals.d.ts +59 -0
- package/dist/plurals.js +280 -0
- package/dist/plurals.js.map +1 -0
- package/dist/report.d.ts +2 -0
- package/dist/report.js +126 -0
- package/dist/report.js.map +1 -0
- package/dist/scan.d.ts +23 -0
- package/dist/scan.js +250 -0
- package/dist/scan.js.map +1 -0
- package/dist/translate.d.ts +93 -0
- package/dist/translate.js +369 -0
- package/dist/translate.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +50 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +28 -0
- package/dist/version.js.map +1 -0
- package/package.json +77 -0
- package/src/apply.ts +334 -0
- package/src/check.ts +468 -0
- package/src/cli.ts +637 -0
- package/src/formats/error.ts +19 -0
- package/src/formats/flatten.ts +38 -0
- package/src/formats/json-write.ts +66 -0
- package/src/formats/json.ts +27 -0
- package/src/formats/php-write.ts +146 -0
- package/src/formats/php.ts +414 -0
- package/src/formats/po-write.ts +230 -0
- package/src/formats/po.ts +233 -0
- package/src/formats/write.ts +41 -0
- package/src/formats/yaml-write.ts +53 -0
- package/src/formats/yaml.ts +62 -0
- package/src/glossary.ts +191 -0
- package/src/lengths.ts +214 -0
- package/src/mcp.ts +451 -0
- package/src/memory.ts +227 -0
- package/src/placeholders.ts +123 -0
- package/src/plurals.ts +320 -0
- package/src/report.ts +162 -0
- package/src/scan.ts +280 -0
- package/src/translate.ts +486 -0
- package/src/types.ts +136 -0
- package/src/version.ts +27 -0
package/dist/scan.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { FormatError } from './formats/error.js';
|
|
4
|
+
import { readJsonLocale } from './formats/json.js';
|
|
5
|
+
import { readPhpLocale } from './formats/php.js';
|
|
6
|
+
import { readPoLocale } from './formats/po.js';
|
|
7
|
+
import { readYamlLocale } from './formats/yaml.js';
|
|
8
|
+
import { DEFAULT_RULES, } from './types.js';
|
|
9
|
+
import { DEFAULT_SYNTAXES } from './placeholders.js';
|
|
10
|
+
const CANDIDATE_DIRS = [
|
|
11
|
+
'locales',
|
|
12
|
+
'src/locales',
|
|
13
|
+
'public/locales',
|
|
14
|
+
'app/locales',
|
|
15
|
+
'i18n',
|
|
16
|
+
'src/i18n',
|
|
17
|
+
'lang',
|
|
18
|
+
'resources/lang',
|
|
19
|
+
'translations',
|
|
20
|
+
'src/translations',
|
|
21
|
+
'config/locales',
|
|
22
|
+
'locale',
|
|
23
|
+
'po',
|
|
24
|
+
];
|
|
25
|
+
/** Formats we can read, longest extension first so stripping is unambiguous. */
|
|
26
|
+
const EXTENSIONS = ['.json', '.yaml', '.php', '.yml', '.pot', '.po'];
|
|
27
|
+
function readerFor(file) {
|
|
28
|
+
if (file.endsWith('.php'))
|
|
29
|
+
return readPhpLocale;
|
|
30
|
+
if (file.endsWith('.po') || file.endsWith('.pot'))
|
|
31
|
+
return readPoLocale;
|
|
32
|
+
if (file.endsWith('.yml') || file.endsWith('.yaml'))
|
|
33
|
+
return readYamlLocale;
|
|
34
|
+
return readJsonLocale;
|
|
35
|
+
}
|
|
36
|
+
function stripExtension(name) {
|
|
37
|
+
for (const ext of EXTENSIONS) {
|
|
38
|
+
if (name.endsWith(ext))
|
|
39
|
+
return name.slice(0, -ext.length);
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const LOCALE_CODE = /^[a-z]{2,3}(?:[-_][A-Za-z0-9]{2,8})*$/;
|
|
44
|
+
export class ScanError extends Error {
|
|
45
|
+
}
|
|
46
|
+
function isDir(p) {
|
|
47
|
+
try {
|
|
48
|
+
return statSync(p).isDirectory();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Finds the directory holding locale files, or throws with the paths it tried. */
|
|
55
|
+
export function findLocalesDir(root, explicit) {
|
|
56
|
+
if (explicit) {
|
|
57
|
+
const dir = resolve(root, explicit);
|
|
58
|
+
if (!isDir(dir))
|
|
59
|
+
throw new ScanError(`Locales directory not found: ${dir}`);
|
|
60
|
+
return dir;
|
|
61
|
+
}
|
|
62
|
+
for (const candidate of CANDIDATE_DIRS) {
|
|
63
|
+
const dir = resolve(root, candidate);
|
|
64
|
+
if (isDir(dir) && listLocales(dir).locales.length > 0)
|
|
65
|
+
return dir;
|
|
66
|
+
}
|
|
67
|
+
throw new ScanError(`No locales directory found under ${root}.\nTried: ${CANDIDATE_DIRS.join(', ')}\nPass one explicitly with --locales <path>.`);
|
|
68
|
+
}
|
|
69
|
+
/** `locales/en.json` is flat; `locales/en/common.json` is nested. */
|
|
70
|
+
export function listLocales(dir) {
|
|
71
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
72
|
+
const nested = entries
|
|
73
|
+
.filter((e) => e.isDirectory() && LOCALE_CODE.test(e.name))
|
|
74
|
+
.map((e) => e.name);
|
|
75
|
+
if (nested.length > 0)
|
|
76
|
+
return { layout: 'nested', locales: nested.sort() };
|
|
77
|
+
const flat = entries
|
|
78
|
+
.filter((e) => e.isFile())
|
|
79
|
+
.map((e) => stripExtension(e.name))
|
|
80
|
+
.filter((name) => name !== null && LOCALE_CODE.test(name));
|
|
81
|
+
return { layout: 'flat', locales: [...new Set(flat)].sort() };
|
|
82
|
+
}
|
|
83
|
+
function collectLocaleFiles(dir, acc = []) {
|
|
84
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
85
|
+
const full = join(dir, entry.name);
|
|
86
|
+
if (entry.isDirectory())
|
|
87
|
+
collectLocaleFiles(full, acc);
|
|
88
|
+
else if (entry.isFile() && stripExtension(entry.name) !== null)
|
|
89
|
+
acc.push(full);
|
|
90
|
+
}
|
|
91
|
+
return acc;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether a bundle's source text comes from msgid rather than from the locale
|
|
95
|
+
* itself. In gettext every catalogue carries the untranslated string, so the
|
|
96
|
+
* locale named as the source only supplies the keys.
|
|
97
|
+
*/
|
|
98
|
+
export function sourceIsMsgid(bundle) {
|
|
99
|
+
return bundle.files.length > 0 && bundle.files.every((f) => f.endsWith('.po') || f.endsWith('.pot'));
|
|
100
|
+
}
|
|
101
|
+
export function loadBundle(config, locale) {
|
|
102
|
+
const target = {
|
|
103
|
+
skipped: [],
|
|
104
|
+
leaves: new Map(),
|
|
105
|
+
containers: new Set(),
|
|
106
|
+
plurals: new Map(),
|
|
107
|
+
nplurals: null,
|
|
108
|
+
};
|
|
109
|
+
const files = [];
|
|
110
|
+
const unreadable = [];
|
|
111
|
+
const isSource = locale === config.sourceLocale;
|
|
112
|
+
/**
|
|
113
|
+
* A malformed file is reported, not thrown. A project with a hundred
|
|
114
|
+
* locales and one bad file should still be checked; refusing to read any
|
|
115
|
+
* of it is how a linter becomes something people stop running.
|
|
116
|
+
*/
|
|
117
|
+
const read = (file, namespace) => {
|
|
118
|
+
try {
|
|
119
|
+
readerFor(file)(file, namespace, locale, isSource, target);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (!(err instanceof FormatError))
|
|
123
|
+
throw err;
|
|
124
|
+
unreadable.push({ file, message: err.message });
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
if (config.layout === 'flat') {
|
|
128
|
+
// A locale may be en.json or en.php; both are read when both exist.
|
|
129
|
+
for (const ext of EXTENSIONS) {
|
|
130
|
+
const file = join(config.localesDir, `${locale}${ext}`);
|
|
131
|
+
if (!existsSync(file))
|
|
132
|
+
continue;
|
|
133
|
+
files.push(file);
|
|
134
|
+
read(file, '');
|
|
135
|
+
}
|
|
136
|
+
if (files.length === 0) {
|
|
137
|
+
throw new ScanError(`No locale file for "${locale}" in ${config.localesDir}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const localeRoot = join(config.localesDir, locale);
|
|
142
|
+
for (const file of collectLocaleFiles(localeRoot).sort()) {
|
|
143
|
+
// lang/en/shop/pricing.php -> "shop.pricing";
|
|
144
|
+
// locale/fr/LC_MESSAGES/messages.po -> "messages", since the gettext
|
|
145
|
+
// directory is layout, not namespace.
|
|
146
|
+
const relativePath = relative(localeRoot, file);
|
|
147
|
+
const namespace = (stripExtension(relativePath) ?? relativePath)
|
|
148
|
+
.split(sep)
|
|
149
|
+
.filter((part) => part !== 'LC_MESSAGES')
|
|
150
|
+
.join('.');
|
|
151
|
+
files.push(file);
|
|
152
|
+
read(file, namespace);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
locale,
|
|
157
|
+
files,
|
|
158
|
+
skipped: target.skipped,
|
|
159
|
+
unreadable,
|
|
160
|
+
leaves: target.leaves,
|
|
161
|
+
containers: target.containers,
|
|
162
|
+
plurals: target.plurals,
|
|
163
|
+
nplurals: target.nplurals,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export function detectProject(root, opts = {}) {
|
|
167
|
+
const resolvedRoot = resolve(root);
|
|
168
|
+
if (!existsSync(resolvedRoot))
|
|
169
|
+
throw new ScanError(`Path not found: ${resolvedRoot}`);
|
|
170
|
+
const localesDir = findLocalesDir(resolvedRoot, opts.localesDir);
|
|
171
|
+
const { layout, locales } = listLocales(localesDir);
|
|
172
|
+
if (locales.length === 0)
|
|
173
|
+
throw new ScanError(`No locale files in ${localesDir}`);
|
|
174
|
+
let sourceLocale = opts.sourceLocale ?? '';
|
|
175
|
+
if (sourceLocale && !locales.includes(sourceLocale)) {
|
|
176
|
+
throw new ScanError(`Source locale "${sourceLocale}" not among: ${locales.join(', ')}`);
|
|
177
|
+
}
|
|
178
|
+
// Laravel's :name is off by default because it false-positives on prose,
|
|
179
|
+
// but in a PHP project it is the interpolation syntax actually in use — and
|
|
180
|
+
// Laravel keeps string-keyed translations in JSON, so the file extension
|
|
181
|
+
// alone is not the signal. A composer.json or a lang/ directory is.
|
|
182
|
+
const syntaxes = [...DEFAULT_SYNTAXES];
|
|
183
|
+
if (usesLaravelPlaceholders(resolvedRoot, localesDir, layout, locales))
|
|
184
|
+
syntaxes.push('laravel');
|
|
185
|
+
const config = {
|
|
186
|
+
root: resolvedRoot,
|
|
187
|
+
localesDir,
|
|
188
|
+
sourceLocale: sourceLocale || locales[0],
|
|
189
|
+
locales,
|
|
190
|
+
layout,
|
|
191
|
+
placeholderSyntaxes: syntaxes,
|
|
192
|
+
ignoreIdentical: [],
|
|
193
|
+
rules: { ...DEFAULT_RULES },
|
|
194
|
+
};
|
|
195
|
+
if (!sourceLocale) {
|
|
196
|
+
// Plain `en` first; then a single regional English, which is the source in
|
|
197
|
+
// a project that spells it en-US. Several of them means none is: Sphinx
|
|
198
|
+
// carries en_DE and en_GB purely for date formats, so there the most
|
|
199
|
+
// complete catalogue is the better guess than whichever sorts first.
|
|
200
|
+
const english = locales.filter((l) => /^en([-_]|$)/.test(l));
|
|
201
|
+
config.sourceLocale =
|
|
202
|
+
locales.find((l) => l === 'en') ??
|
|
203
|
+
(english.length === 1 ? english[0] : mostComplete(config, locales));
|
|
204
|
+
}
|
|
205
|
+
return config;
|
|
206
|
+
}
|
|
207
|
+
/** The locale with the most keys, which in practice is the one others follow. */
|
|
208
|
+
function mostComplete(config, locales) {
|
|
209
|
+
let best = locales[0];
|
|
210
|
+
let bestCount = -1;
|
|
211
|
+
for (const locale of locales) {
|
|
212
|
+
let count = 0;
|
|
213
|
+
try {
|
|
214
|
+
count = loadBundle({ ...config, sourceLocale: locale }, locale).leaves.size;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
continue; // an unreadable locale simply cannot be the source
|
|
218
|
+
}
|
|
219
|
+
if (count > bestCount) {
|
|
220
|
+
best = locale;
|
|
221
|
+
bestCount = count;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return best;
|
|
225
|
+
}
|
|
226
|
+
function hasPhpLocales(localesDir, layout, locales) {
|
|
227
|
+
if (layout === 'flat') {
|
|
228
|
+
return locales.some((locale) => existsSync(join(localesDir, `${locale}.php`)));
|
|
229
|
+
}
|
|
230
|
+
return locales.some((locale) => {
|
|
231
|
+
const dir = join(localesDir, locale);
|
|
232
|
+
return isDir(dir) && collectLocaleFiles(dir).some((file) => file.endsWith('.php'));
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Whether this project interpolates the PHP way.
|
|
237
|
+
*
|
|
238
|
+
* Tying it to the .php extension missed every Laravel app that keeps its
|
|
239
|
+
* string-keyed translations in lang/xx.json — which is Laravel's own
|
|
240
|
+
* convention, so the placeholders went unchecked in exactly the projects the
|
|
241
|
+
* syntax exists for.
|
|
242
|
+
*/
|
|
243
|
+
function usesLaravelPlaceholders(root, localesDir, layout, locales) {
|
|
244
|
+
if (hasPhpLocales(localesDir, layout, locales))
|
|
245
|
+
return true;
|
|
246
|
+
if (existsSync(join(root, 'composer.json')))
|
|
247
|
+
return true;
|
|
248
|
+
return basename(localesDir) === 'lang';
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=scan.js.map
|
package/dist/scan.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import { type Glossary } from './glossary.js';
|
|
3
|
+
import { type Limits } from './lengths.js';
|
|
4
|
+
import type { Config, Finding, LocaleBundle } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* The one part of this tool that needs a network and cannot be verified by
|
|
7
|
+
* reading the output. So it is not trusted: every proposal is put back through
|
|
8
|
+
* the same deterministic checks the linter already applies, and anything that
|
|
9
|
+
* fails is rejected rather than written.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_MODEL = "claude-opus-5";
|
|
12
|
+
export declare const DEFAULT_BATCH = 20;
|
|
13
|
+
export declare const DEFAULT_CAP = 50;
|
|
14
|
+
export type JobKind = 'fill' | 'repair' | 'refresh';
|
|
15
|
+
export declare const JOB_KINDS: readonly JobKind[];
|
|
16
|
+
/**
|
|
17
|
+
* Defects worth handing back to the model.
|
|
18
|
+
*
|
|
19
|
+
* A rule belongs here only if `validate` can confirm the repair afterwards.
|
|
20
|
+
* That is the whole selection principle: a fix the gate cannot check is a fix
|
|
21
|
+
* nobody can trust, so those findings are left for a human.
|
|
22
|
+
*
|
|
23
|
+
* It excludes `identical_to_source` (often correct — "Email" is "Email" in
|
|
24
|
+
* French, and forcing a change would make it worse), `plural_extra_category`
|
|
25
|
+
* and `plural_selector_lost` (the single-string validator does not check for
|
|
26
|
+
* either, so a repair could not be verified).
|
|
27
|
+
*/
|
|
28
|
+
export declare const REPAIRABLE_RULES: Set<string>;
|
|
29
|
+
export interface Job {
|
|
30
|
+
locale: string;
|
|
31
|
+
key: string;
|
|
32
|
+
source: string;
|
|
33
|
+
kind: JobKind;
|
|
34
|
+
/** The translation being replaced: broken for a repair, outdated for a refresh. */
|
|
35
|
+
previous: string | null;
|
|
36
|
+
/** For a repair, exactly what the linter found wrong. */
|
|
37
|
+
problems: string[];
|
|
38
|
+
placeholders: string[];
|
|
39
|
+
glossary: Array<{
|
|
40
|
+
term: string;
|
|
41
|
+
accepted: string[];
|
|
42
|
+
}>;
|
|
43
|
+
doNotTranslate: string[];
|
|
44
|
+
maxWidth: number | null;
|
|
45
|
+
isIcuPlural: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface Proposal {
|
|
48
|
+
locale: string;
|
|
49
|
+
key: string;
|
|
50
|
+
kind: JobKind;
|
|
51
|
+
source: string;
|
|
52
|
+
value: string;
|
|
53
|
+
accepted: boolean;
|
|
54
|
+
/** Deterministic checks the proposal failed, if any. */
|
|
55
|
+
rejections: string[];
|
|
56
|
+
attempts: number;
|
|
57
|
+
}
|
|
58
|
+
/** Turns a lint report into a work list. */
|
|
59
|
+
export declare function collectJobs(config: Config, findings: Finding[], source: LocaleBundle, glossary: Glossary | null, limits: Limits | null, locales?: string[], kinds?: JobKind[]): Job[];
|
|
60
|
+
/** What `validate` needs to judge a string; a full Job satisfies it. */
|
|
61
|
+
export type Constraints = Pick<Job, 'source' | 'locale' | 'maxWidth'>;
|
|
62
|
+
/** The same rules the linter enforces, applied to one proposed string. */
|
|
63
|
+
export declare function validate(config: Config, job: Constraints, proposed: string, glossary: Glossary | null): string[];
|
|
64
|
+
export interface TranslateOptions {
|
|
65
|
+
model: string;
|
|
66
|
+
effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
67
|
+
client?: Anthropic;
|
|
68
|
+
}
|
|
69
|
+
/** The model declined one batch on content grounds; other batches may still run. */
|
|
70
|
+
export declare class TranslationRefused extends Error {
|
|
71
|
+
}
|
|
72
|
+
/** One request for one locale's batch. Returns text keyed by job index. */
|
|
73
|
+
export declare function requestBatch(jobs: Job[], locale: string, options: TranslateOptions, feedback?: Map<number, string[]>): Promise<Map<number, string>>;
|
|
74
|
+
export interface TranslationRun {
|
|
75
|
+
proposals: Proposal[];
|
|
76
|
+
/**
|
|
77
|
+
* Why the run stopped early, if it did. Anything that is not a content
|
|
78
|
+
* refusal — no credentials, a rate limit, a network failure — will hit every
|
|
79
|
+
* remaining batch the same way, so the run stops instead of reporting the
|
|
80
|
+
* same infrastructure error once per string.
|
|
81
|
+
*/
|
|
82
|
+
aborted: string | null;
|
|
83
|
+
}
|
|
84
|
+
export interface RunOptions extends TranslateOptions {
|
|
85
|
+
batchSize: number;
|
|
86
|
+
/** Called before each request so the caller can report progress. */
|
|
87
|
+
onBatch?: (locale: string, size: number, attempt: number) => void;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Translates a work list, validating every proposal and retrying once with the
|
|
91
|
+
* specific failures handed back to the model.
|
|
92
|
+
*/
|
|
93
|
+
export declare function runTranslation(config: Config, jobs: Job[], glossary: Glossary | null, options: RunOptions): Promise<TranslationRun>;
|