@verbatra/sdk 0.5.0-next.1 → 0.5.0-next.2
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 +1 -1
- package/dist/index.cjs +112 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -28
- package/dist/index.d.ts +38 -28
- package/dist/index.js +112 -64
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ export default defineConfig({
|
|
|
71
71
|
});
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
`files.pattern` must contain the `{locale}` token, and `targetLocales` must not include `sourceLocale`; both are enforced when the config is validated. The supported `format` values are `i18next-json`, `vue-i18n-json`, `next-intl-json`, `ngx-translate-json`, `xliff`, `yaml`, and `arb`. The optional `glossary` (a term map) and `tone` (`"formal"`, `"informal"`, or `"neutral"`) refine the output. The optional `prune` boolean (off by default) opts in to removing orphaned keys (present in a target file but absent from the source) from the written target files and the lock; the `translate --prune` flag overrides it per run. The optional `generatePlurals` boolean (off by default) opts in to synthesizing the CLDR plural forms a richer target language requires but the source lacks (i18next-JSON projects translated by an LLM provider only; DeepL, non-i18next formats, and unknown languages fall back to the per-locale plural warning and never fail); a per-run `generatePlurals` override on `translate` takes precedence, and generated keys are reported separately from translated keys on the summary. OpenAI and Gemini take `{ model, maxOutputTokens }`; DeepL takes `{}` (with an optional `glossaryId`). API keys are never part of the config. Each provider reads its own environment variable (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `DEEPL_API_KEY`).
|
|
74
|
+
`files.pattern` must contain the `{locale}` token, and `targetLocales` must not include `sourceLocale`; both are enforced when the config is validated. The supported `format` values are `i18next-json`, `vue-i18n-json`, `next-intl-json`, `ngx-translate-json`, `xliff`, `yaml`, and `arb`. The optional `glossary` (a term map) and `tone` (`"formal"`, `"informal"`, or `"neutral"`) refine the output. The optional `prune` boolean (off by default) opts in to removing orphaned keys (present in a target file but absent from the source) from the written target files and the lock; the `translate --prune` flag overrides it per run. The optional `generatePlurals` boolean (off by default) opts in to synthesizing the CLDR plural forms a richer target language requires but the source lacks (i18next-JSON projects translated by an LLM provider only; DeepL, non-i18next formats, and unknown languages fall back to the per-locale plural warning and never fail); a per-run `generatePlurals` override on `translate` takes precedence, and generated keys are reported separately from translated keys on the summary. OpenAI and Gemini take `{ model, maxOutputTokens }`; `openai-compatible` additionally takes a `baseUrl` (for a local or self-hosted server such as LM Studio, Ollama, or vLLM) and an optional `apiKeyEnvVar`; DeepL takes `{}` (with an optional `glossaryId`). API keys are never part of the config. Each provider reads its own environment variable (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `DEEPL_API_KEY`; `openai-compatible` resolves its key from `apiKeyEnvVar`, then `OPENAI_COMPATIBLE_API_KEY`, then falls back to a keyless placeholder).
|
|
75
75
|
|
|
76
76
|
## API reference
|
|
77
77
|
|
package/dist/index.cjs
CHANGED
|
@@ -503,16 +503,14 @@ function validateRequest(request) {
|
|
|
503
503
|
return parsed.data;
|
|
504
504
|
}
|
|
505
505
|
function toIntegrityInputs(entries, values) {
|
|
506
|
-
|
|
506
|
+
const inputs = [];
|
|
507
|
+
for (const entry of entries) {
|
|
507
508
|
const translatedValue = values.get(entry.key);
|
|
508
|
-
if (translatedValue
|
|
509
|
-
|
|
510
|
-
"INVALID_RESPONSE",
|
|
511
|
-
"The provider response is missing one or more keys."
|
|
512
|
-
);
|
|
509
|
+
if (translatedValue !== void 0) {
|
|
510
|
+
inputs.push({ key: entry.key, sourceValue: entry.value, translatedValue });
|
|
513
511
|
}
|
|
514
|
-
|
|
515
|
-
|
|
512
|
+
}
|
|
513
|
+
return inputs;
|
|
516
514
|
}
|
|
517
515
|
function toItem(entry) {
|
|
518
516
|
return {
|
|
@@ -544,25 +542,29 @@ function deriveJsonSchema(schema) {
|
|
|
544
542
|
}
|
|
545
543
|
return result;
|
|
546
544
|
}
|
|
547
|
-
function
|
|
548
|
-
const
|
|
549
|
-
const
|
|
545
|
+
function partitionTranslations(translations, requested) {
|
|
546
|
+
const firstSeen = /* @__PURE__ */ new Map();
|
|
547
|
+
const duplicated = /* @__PURE__ */ new Set();
|
|
550
548
|
for (const { key, value } of translations) {
|
|
551
|
-
if (!requested.has(key)
|
|
552
|
-
throw new ProviderError(
|
|
553
|
-
"INVALID_RESPONSE",
|
|
554
|
-
"The provider returned an unexpected or duplicate key."
|
|
555
|
-
);
|
|
549
|
+
if (!requested.has(key)) {
|
|
550
|
+
throw new ProviderError("INVALID_RESPONSE", "The provider returned an unexpected key.");
|
|
556
551
|
}
|
|
557
|
-
|
|
552
|
+
if (firstSeen.has(key)) {
|
|
553
|
+
duplicated.add(key);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
firstSeen.set(key, value);
|
|
558
557
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
558
|
+
return { firstSeen, duplicated };
|
|
559
|
+
}
|
|
560
|
+
function reconcile(translations, requestedKeys) {
|
|
561
|
+
const requested = new Set(requestedKeys);
|
|
562
|
+
const { firstSeen, duplicated } = partitionTranslations(translations, requested);
|
|
563
|
+
const missingKeys = requestedKeys.filter((key) => duplicated.has(key) || !firstSeen.has(key));
|
|
564
|
+
for (const key of duplicated) {
|
|
565
|
+
firstSeen.delete(key);
|
|
564
566
|
}
|
|
565
|
-
return
|
|
567
|
+
return { accepted: firstSeen, missingKeys };
|
|
566
568
|
}
|
|
567
569
|
function reconcileResult(raw, requestedKeys) {
|
|
568
570
|
const parsed = translationsResultSchema.safeParse(raw);
|
|
@@ -574,22 +576,54 @@ function reconcileResult(raw, requestedKeys) {
|
|
|
574
576
|
}
|
|
575
577
|
return reconcile(parsed.data.translations, requestedKeys);
|
|
576
578
|
}
|
|
579
|
+
var MAX_REPAIR_ROUNDS = 1;
|
|
577
580
|
async function runLlmTranslation(request, mechanism) {
|
|
578
581
|
const data = validateRequest(request);
|
|
582
|
+
const signal = request.signal;
|
|
583
|
+
const first = await requestTranslations(mechanism, data, signal);
|
|
584
|
+
const values = first.outcome.accepted;
|
|
585
|
+
let usage = first.completion.usage;
|
|
586
|
+
let toRepair = entriesFor(data.entries, first.outcome.missingKeys);
|
|
587
|
+
for (let round = 0; round < MAX_REPAIR_ROUNDS && toRepair.length > 0; round += 1) {
|
|
588
|
+
const repair = await requestTranslations(mechanism, { ...data, entries: toRepair }, signal);
|
|
589
|
+
for (const [key, value] of repair.outcome.accepted) {
|
|
590
|
+
values.set(key, value);
|
|
591
|
+
}
|
|
592
|
+
usage = mergeUsage(usage, repair.completion.usage);
|
|
593
|
+
toRepair = entriesFor(data.entries, repair.outcome.missingKeys);
|
|
594
|
+
}
|
|
595
|
+
const integrity = checkBatchIntegrity(
|
|
596
|
+
toIntegrityInputs(data.entries, values),
|
|
597
|
+
request.extractPlaceholders,
|
|
598
|
+
request.comparePlaceholders
|
|
599
|
+
);
|
|
600
|
+
return usage === void 0 ? { values, integrity, notices: [] } : { values, integrity, usage, notices: [] };
|
|
601
|
+
}
|
|
602
|
+
async function requestTranslations(mechanism, data, signal) {
|
|
579
603
|
const payloadJson = JSON.stringify(buildDataPayload(data));
|
|
580
604
|
const requestedKeys = data.entries.map((entry) => entry.key);
|
|
581
605
|
const completion = await mechanism.translate({
|
|
582
606
|
payloadJson,
|
|
583
607
|
requestedKeys,
|
|
584
|
-
...
|
|
608
|
+
...signal !== void 0 ? { signal } : {}
|
|
585
609
|
});
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
610
|
+
return { completion, outcome: reconcileResult(completion.raw, requestedKeys) };
|
|
611
|
+
}
|
|
612
|
+
function entriesFor(entries, keys) {
|
|
613
|
+
const wanted = new Set(keys);
|
|
614
|
+
return entries.filter((entry) => wanted.has(entry.key));
|
|
615
|
+
}
|
|
616
|
+
function mergeUsage(first, second) {
|
|
617
|
+
if (first === void 0) {
|
|
618
|
+
return second;
|
|
619
|
+
}
|
|
620
|
+
if (second === void 0) {
|
|
621
|
+
return first;
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
inputTokens: first.inputTokens + second.inputTokens,
|
|
625
|
+
outputTokens: first.outputTokens + second.outputTokens
|
|
626
|
+
};
|
|
593
627
|
}
|
|
594
628
|
var OUTPUT_TRUNCATED_MESSAGE = "The provider stopped because the output-token limit was reached. Reduce the batch size or raise the configured max output tokens.";
|
|
595
629
|
function assertNotTruncated(truncated) {
|
|
@@ -837,7 +871,10 @@ function createDeepLProvider(config, deps = {}) {
|
|
|
837
871
|
return {
|
|
838
872
|
id: PROVIDER_ID2,
|
|
839
873
|
kind: "machine-translation",
|
|
840
|
-
|
|
874
|
+
// Honest capability signal: DeepL only ever applies a pre-created native glossary id, never a
|
|
875
|
+
// generic term map (see buildTranslateOptions). Without one configured, a supplied term map is
|
|
876
|
+
// silently ignored and surfaces only as a GLOSSARY_IGNORED notice.
|
|
877
|
+
supportsGlossary: validConfig.glossaryId !== void 0,
|
|
841
878
|
translateBatch: (request) => translate(bundle, validConfig, request)
|
|
842
879
|
};
|
|
843
880
|
}
|
|
@@ -1141,16 +1178,19 @@ function buildResponseFormat(mode) {
|
|
|
1141
1178
|
}
|
|
1142
1179
|
};
|
|
1143
1180
|
}
|
|
1144
|
-
function buildOpenAiRequest(config, payloadJson, mode = "strict-schema") {
|
|
1145
|
-
|
|
1146
|
-
model: config.model,
|
|
1147
|
-
max_completion_tokens: config.maxOutputTokens,
|
|
1181
|
+
function buildOpenAiRequest(config, payloadJson, mode = "strict-schema", tokenLimitField = "max_completion_tokens") {
|
|
1182
|
+
const base = {
|
|
1148
1183
|
messages: [
|
|
1149
1184
|
{ role: "system", content: OPENAI_SYSTEM_RULES },
|
|
1150
1185
|
{ role: "user", content: payloadJson }
|
|
1151
1186
|
],
|
|
1187
|
+
model: config.model,
|
|
1152
1188
|
response_format: buildResponseFormat(mode)
|
|
1153
1189
|
};
|
|
1190
|
+
if (tokenLimitField === "max_tokens") {
|
|
1191
|
+
return { ...base, max_tokens: config.maxOutputTokens };
|
|
1192
|
+
}
|
|
1193
|
+
return { ...base, max_completion_tokens: config.maxOutputTokens };
|
|
1154
1194
|
}
|
|
1155
1195
|
function advanceStringScan(char, escaped) {
|
|
1156
1196
|
if (escaped) {
|
|
@@ -1321,7 +1361,7 @@ function createOpenAiCompatibleProvider(config, deps = {}) {
|
|
|
1321
1361
|
function createMechanism4(client, config) {
|
|
1322
1362
|
return {
|
|
1323
1363
|
translate: async ({ payloadJson, signal }) => {
|
|
1324
|
-
const body = buildOpenAiRequest(config, payloadJson, "strict-schema");
|
|
1364
|
+
const body = buildOpenAiRequest(config, payloadJson, "strict-schema", "max_tokens");
|
|
1325
1365
|
const completion = await callClient5(client, body, signal);
|
|
1326
1366
|
return extractOpenAiResult(completion, true);
|
|
1327
1367
|
}
|
|
@@ -1922,6 +1962,9 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
|
1922
1962
|
throw error;
|
|
1923
1963
|
}
|
|
1924
1964
|
}
|
|
1965
|
+
function stripLeadingBom(content) {
|
|
1966
|
+
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
1967
|
+
}
|
|
1925
1968
|
async function readBoundedUtf82(handle, size) {
|
|
1926
1969
|
const buffer = Buffer.allocUnsafe(size);
|
|
1927
1970
|
let offset = 0;
|
|
@@ -1932,7 +1975,7 @@ async function readBoundedUtf82(handle, size) {
|
|
|
1932
1975
|
}
|
|
1933
1976
|
offset += bytesRead;
|
|
1934
1977
|
}
|
|
1935
|
-
return buffer.toString("utf8", 0, offset);
|
|
1978
|
+
return stripLeadingBom(buffer.toString("utf8", 0, offset));
|
|
1936
1979
|
}
|
|
1937
1980
|
async function readBounded2(filePath) {
|
|
1938
1981
|
const handle = await promises.open(filePath, "r");
|
|
@@ -2975,15 +3018,8 @@ function subBatchFailedNotice(count, error) {
|
|
|
2975
3018
|
}
|
|
2976
3019
|
|
|
2977
3020
|
// src/flow/notices.ts
|
|
2978
|
-
function isNotice(value) {
|
|
2979
|
-
return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.message === "string";
|
|
2980
|
-
}
|
|
2981
3021
|
function readNotices(result) {
|
|
2982
|
-
|
|
2983
|
-
if (!Array.isArray(candidate)) {
|
|
2984
|
-
return [];
|
|
2985
|
-
}
|
|
2986
|
-
return candidate.filter(isNotice);
|
|
3022
|
+
return result.notices ?? [];
|
|
2987
3023
|
}
|
|
2988
3024
|
|
|
2989
3025
|
// src/flow/plural-categories.ts
|
|
@@ -3187,20 +3223,26 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
|
|
|
3187
3223
|
return [subBatchFailedNotice(batch.length, error)];
|
|
3188
3224
|
}
|
|
3189
3225
|
for (const item of batch) {
|
|
3190
|
-
|
|
3191
|
-
const integrity = result.integrity.get(item.targetKey);
|
|
3192
|
-
if (value !== void 0 && integrity?.matches === true) {
|
|
3193
|
-
accepted.push({
|
|
3194
|
-
targetKey: item.targetKey,
|
|
3195
|
-
entry: { ...syntheticEntry(item), value },
|
|
3196
|
-
lockHash: generatedLockHash(item.governingEntries, item.category)
|
|
3197
|
-
});
|
|
3198
|
-
} else {
|
|
3199
|
-
withheld.push(item.targetKey);
|
|
3200
|
-
}
|
|
3226
|
+
foldGenerationItem(item, result, accepted, withheld, providerFailures);
|
|
3201
3227
|
}
|
|
3202
3228
|
return readNotices(result);
|
|
3203
3229
|
}
|
|
3230
|
+
function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
|
|
3231
|
+
const value = result.values.get(item.targetKey);
|
|
3232
|
+
if (value === void 0) {
|
|
3233
|
+
providerFailures.push(item.targetKey);
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
if (result.integrity.get(item.targetKey)?.matches === true) {
|
|
3237
|
+
accepted.push({
|
|
3238
|
+
targetKey: item.targetKey,
|
|
3239
|
+
entry: { ...syntheticEntry(item), value },
|
|
3240
|
+
lockHash: generatedLockHash(item.governingEntries, item.category)
|
|
3241
|
+
});
|
|
3242
|
+
} else {
|
|
3243
|
+
withheld.push(item.targetKey);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3204
3246
|
|
|
3205
3247
|
// src/flow/locale-run.ts
|
|
3206
3248
|
function emptyResource(locale, format) {
|
|
@@ -3388,16 +3430,22 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
|
|
|
3388
3430
|
return [subBatchFailedNotice(batch.length, error)];
|
|
3389
3431
|
}
|
|
3390
3432
|
for (const entry of batch) {
|
|
3391
|
-
|
|
3392
|
-
const integrity = result.integrity.get(entry.key);
|
|
3393
|
-
if (value !== void 0 && integrity?.matches === true) {
|
|
3394
|
-
accepted.set(entry.key, { value, source: entry });
|
|
3395
|
-
} else {
|
|
3396
|
-
integrityMismatches.push(entry.key);
|
|
3397
|
-
}
|
|
3433
|
+
foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
|
|
3398
3434
|
}
|
|
3399
3435
|
return readNotices(result);
|
|
3400
3436
|
}
|
|
3437
|
+
function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
|
|
3438
|
+
const value = result.values.get(entry.key);
|
|
3439
|
+
if (value === void 0) {
|
|
3440
|
+
providerFailures.push(entry.key);
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
if (result.integrity.get(entry.key)?.matches === true) {
|
|
3444
|
+
accepted.set(entry.key, { value, source: entry });
|
|
3445
|
+
} else {
|
|
3446
|
+
integrityMismatches.push(entry.key);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3401
3449
|
function computeLockEntries(params, merged, withheld, generated) {
|
|
3402
3450
|
const lockEntries = {};
|
|
3403
3451
|
const sourceBaseKeys = sourcePluralBaseKeys(params.source);
|