@verbatra/sdk 0.5.0-next.1 → 0.5.0-next.3
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 +308 -81
- 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 +308 -81
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -473,16 +473,14 @@ function validateRequest(request) {
|
|
|
473
473
|
return parsed.data;
|
|
474
474
|
}
|
|
475
475
|
function toIntegrityInputs(entries, values) {
|
|
476
|
-
|
|
476
|
+
const inputs = [];
|
|
477
|
+
for (const entry of entries) {
|
|
477
478
|
const translatedValue = values.get(entry.key);
|
|
478
|
-
if (translatedValue
|
|
479
|
-
|
|
480
|
-
"INVALID_RESPONSE",
|
|
481
|
-
"The provider response is missing one or more keys."
|
|
482
|
-
);
|
|
479
|
+
if (translatedValue !== void 0) {
|
|
480
|
+
inputs.push({ key: entry.key, sourceValue: entry.value, translatedValue });
|
|
483
481
|
}
|
|
484
|
-
|
|
485
|
-
|
|
482
|
+
}
|
|
483
|
+
return inputs;
|
|
486
484
|
}
|
|
487
485
|
function toItem(entry) {
|
|
488
486
|
return {
|
|
@@ -514,25 +512,29 @@ function deriveJsonSchema(schema) {
|
|
|
514
512
|
}
|
|
515
513
|
return result;
|
|
516
514
|
}
|
|
517
|
-
function
|
|
518
|
-
const
|
|
519
|
-
const
|
|
515
|
+
function partitionTranslations(translations, requested) {
|
|
516
|
+
const firstSeen = /* @__PURE__ */ new Map();
|
|
517
|
+
const duplicated = /* @__PURE__ */ new Set();
|
|
520
518
|
for (const { key, value } of translations) {
|
|
521
|
-
if (!requested.has(key)
|
|
522
|
-
throw new ProviderError(
|
|
523
|
-
"INVALID_RESPONSE",
|
|
524
|
-
"The provider returned an unexpected or duplicate key."
|
|
525
|
-
);
|
|
519
|
+
if (!requested.has(key)) {
|
|
520
|
+
throw new ProviderError("INVALID_RESPONSE", "The provider returned an unexpected key.");
|
|
526
521
|
}
|
|
527
|
-
|
|
522
|
+
if (firstSeen.has(key)) {
|
|
523
|
+
duplicated.add(key);
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
firstSeen.set(key, value);
|
|
528
527
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
528
|
+
return { firstSeen, duplicated };
|
|
529
|
+
}
|
|
530
|
+
function reconcile(translations, requestedKeys) {
|
|
531
|
+
const requested = new Set(requestedKeys);
|
|
532
|
+
const { firstSeen, duplicated } = partitionTranslations(translations, requested);
|
|
533
|
+
const missingKeys = requestedKeys.filter((key) => duplicated.has(key) || !firstSeen.has(key));
|
|
534
|
+
for (const key of duplicated) {
|
|
535
|
+
firstSeen.delete(key);
|
|
534
536
|
}
|
|
535
|
-
return
|
|
537
|
+
return { accepted: firstSeen, missingKeys };
|
|
536
538
|
}
|
|
537
539
|
function reconcileResult(raw, requestedKeys) {
|
|
538
540
|
const parsed = translationsResultSchema.safeParse(raw);
|
|
@@ -544,22 +546,54 @@ function reconcileResult(raw, requestedKeys) {
|
|
|
544
546
|
}
|
|
545
547
|
return reconcile(parsed.data.translations, requestedKeys);
|
|
546
548
|
}
|
|
549
|
+
var MAX_REPAIR_ROUNDS = 1;
|
|
547
550
|
async function runLlmTranslation(request, mechanism) {
|
|
548
551
|
const data = validateRequest(request);
|
|
552
|
+
const signal = request.signal;
|
|
553
|
+
const first = await requestTranslations(mechanism, data, signal);
|
|
554
|
+
const values = first.outcome.accepted;
|
|
555
|
+
let usage = first.completion.usage;
|
|
556
|
+
let toRepair = entriesFor(data.entries, first.outcome.missingKeys);
|
|
557
|
+
for (let round = 0; round < MAX_REPAIR_ROUNDS && toRepair.length > 0; round += 1) {
|
|
558
|
+
const repair = await requestTranslations(mechanism, { ...data, entries: toRepair }, signal);
|
|
559
|
+
for (const [key, value] of repair.outcome.accepted) {
|
|
560
|
+
values.set(key, value);
|
|
561
|
+
}
|
|
562
|
+
usage = mergeUsage(usage, repair.completion.usage);
|
|
563
|
+
toRepair = entriesFor(data.entries, repair.outcome.missingKeys);
|
|
564
|
+
}
|
|
565
|
+
const integrity = checkBatchIntegrity(
|
|
566
|
+
toIntegrityInputs(data.entries, values),
|
|
567
|
+
request.extractPlaceholders,
|
|
568
|
+
request.comparePlaceholders
|
|
569
|
+
);
|
|
570
|
+
return usage === void 0 ? { values, integrity, notices: [] } : { values, integrity, usage, notices: [] };
|
|
571
|
+
}
|
|
572
|
+
async function requestTranslations(mechanism, data, signal) {
|
|
549
573
|
const payloadJson = JSON.stringify(buildDataPayload(data));
|
|
550
574
|
const requestedKeys = data.entries.map((entry) => entry.key);
|
|
551
575
|
const completion = await mechanism.translate({
|
|
552
576
|
payloadJson,
|
|
553
577
|
requestedKeys,
|
|
554
|
-
...
|
|
578
|
+
...signal !== void 0 ? { signal } : {}
|
|
555
579
|
});
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
580
|
+
return { completion, outcome: reconcileResult(completion.raw, requestedKeys) };
|
|
581
|
+
}
|
|
582
|
+
function entriesFor(entries, keys) {
|
|
583
|
+
const wanted = new Set(keys);
|
|
584
|
+
return entries.filter((entry) => wanted.has(entry.key));
|
|
585
|
+
}
|
|
586
|
+
function mergeUsage(first, second) {
|
|
587
|
+
if (first === void 0) {
|
|
588
|
+
return second;
|
|
589
|
+
}
|
|
590
|
+
if (second === void 0) {
|
|
591
|
+
return first;
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
inputTokens: first.inputTokens + second.inputTokens,
|
|
595
|
+
outputTokens: first.outputTokens + second.outputTokens
|
|
596
|
+
};
|
|
563
597
|
}
|
|
564
598
|
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.";
|
|
565
599
|
function assertNotTruncated(truncated) {
|
|
@@ -743,6 +777,50 @@ function createDefaultClient2() {
|
|
|
743
777
|
};
|
|
744
778
|
return { client, freeAccount };
|
|
745
779
|
}
|
|
780
|
+
var DEEPL_MAX_TEXTS_PER_REQUEST = 50;
|
|
781
|
+
var DEEPL_MAX_PAYLOAD_BYTES = 128 * 1024;
|
|
782
|
+
var DEEPL_PAYLOAD_OVERHEAD_RESERVE_BYTES = 4 * 1024;
|
|
783
|
+
var DEEPL_MAX_TEXT_PAYLOAD_BYTES = DEEPL_MAX_PAYLOAD_BYTES - DEEPL_PAYLOAD_OVERHEAD_RESERVE_BYTES;
|
|
784
|
+
function estimateWireBytes(text) {
|
|
785
|
+
return encodeURIComponent(text).length;
|
|
786
|
+
}
|
|
787
|
+
function chunkTextsForDeepL(texts) {
|
|
788
|
+
const chunks = [];
|
|
789
|
+
let current = [];
|
|
790
|
+
let currentBytes = 0;
|
|
791
|
+
for (const text of texts) {
|
|
792
|
+
const textBytes = estimateWireBytes(text);
|
|
793
|
+
const startsNewChunk = current.length > 0 && (current.length >= DEEPL_MAX_TEXTS_PER_REQUEST || currentBytes + textBytes > DEEPL_MAX_TEXT_PAYLOAD_BYTES);
|
|
794
|
+
if (startsNewChunk) {
|
|
795
|
+
chunks.push(current);
|
|
796
|
+
current = [];
|
|
797
|
+
currentBytes = 0;
|
|
798
|
+
}
|
|
799
|
+
current.push(text);
|
|
800
|
+
currentBytes += textBytes;
|
|
801
|
+
}
|
|
802
|
+
if (current.length > 0) {
|
|
803
|
+
chunks.push(current);
|
|
804
|
+
}
|
|
805
|
+
return chunks;
|
|
806
|
+
}
|
|
807
|
+
var DEPRECATED_BARE_TARGET_CODES = /* @__PURE__ */ new Set(["en", "pt"]);
|
|
808
|
+
function assertValidDeepLSourceLocale(locale) {
|
|
809
|
+
if (locale.includes("-")) {
|
|
810
|
+
throw new ProviderError(
|
|
811
|
+
"INVALID_REQUEST",
|
|
812
|
+
`DeepL does not accept a regional or script source locale code: "${locale}". Only the base language code is valid as a DeepL source (for example, use "en" instead of "en-US").`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function assertValidDeepLTargetLocale(locale) {
|
|
817
|
+
if (DEPRECATED_BARE_TARGET_CODES.has(locale.toLowerCase())) {
|
|
818
|
+
throw new ProviderError(
|
|
819
|
+
"INVALID_REQUEST",
|
|
820
|
+
`DeepL requires a disambiguated target locale code instead of "${locale}" (for example, "en-GB" or "en-US" for English, "pt-PT" or "pt-BR" for Portuguese).`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
746
824
|
var PLACEHOLDER_UNSUPPORTED_MESSAGE = "Some entries contain placeholders or ICU syntax that DeepL cannot preserve; they were left untranslated. Use an LLM provider to translate placeholder-bearing strings.";
|
|
747
825
|
function partitionByPlaceholders(entries) {
|
|
748
826
|
const protectable = [];
|
|
@@ -807,7 +885,10 @@ function createDeepLProvider(config, deps = {}) {
|
|
|
807
885
|
return {
|
|
808
886
|
id: PROVIDER_ID2,
|
|
809
887
|
kind: "machine-translation",
|
|
810
|
-
|
|
888
|
+
// Honest capability signal: DeepL only ever applies a pre-created native glossary id, never a
|
|
889
|
+
// generic term map (see buildTranslateOptions). Without one configured, a supplied term map is
|
|
890
|
+
// silently ignored and surfaces only as a GLOSSARY_IGNORED notice.
|
|
891
|
+
supportsGlossary: validConfig.glossaryId !== void 0,
|
|
811
892
|
translateBatch: (request) => translate(bundle, validConfig, request)
|
|
812
893
|
};
|
|
813
894
|
}
|
|
@@ -819,6 +900,8 @@ function resolveClient(deps) {
|
|
|
819
900
|
}
|
|
820
901
|
async function translate(bundle, config, request) {
|
|
821
902
|
const data = validateRequest(request);
|
|
903
|
+
assertValidDeepLSourceLocale(data.sourceLocale);
|
|
904
|
+
assertValidDeepLTargetLocale(data.targetLocale);
|
|
822
905
|
const { protectable, unprotectable } = partitionByPlaceholders(data.entries);
|
|
823
906
|
const genericGlossarySupplied = request.glossary !== void 0 && Object.keys(request.glossary).length > 0;
|
|
824
907
|
const { options, notices } = buildTranslateOptions({
|
|
@@ -846,7 +929,7 @@ async function translateProtectable(client, data, protectable, options, extract,
|
|
|
846
929
|
return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
|
|
847
930
|
}
|
|
848
931
|
const texts = protectable.map((entry) => entry.value);
|
|
849
|
-
const results = await
|
|
932
|
+
const results = await callClientChunked(
|
|
850
933
|
client,
|
|
851
934
|
texts,
|
|
852
935
|
data.sourceLocale,
|
|
@@ -864,6 +947,14 @@ function callClient2(client, texts, sourceLang, targetLang, options, signal) {
|
|
|
864
947
|
signal
|
|
865
948
|
);
|
|
866
949
|
}
|
|
950
|
+
async function callClientChunked(client, texts, sourceLang, targetLang, options, signal) {
|
|
951
|
+
const results = [];
|
|
952
|
+
for (const chunk2 of chunkTextsForDeepL(texts)) {
|
|
953
|
+
const chunkResults = await callClient2(client, chunk2, sourceLang, targetLang, options, signal);
|
|
954
|
+
results.push(...chunkResults);
|
|
955
|
+
}
|
|
956
|
+
return results;
|
|
957
|
+
}
|
|
867
958
|
var geminiConfigSchema = z.object({
|
|
868
959
|
model: z.string().min(1),
|
|
869
960
|
maxOutputTokens: z.number().int().positive()
|
|
@@ -1111,16 +1202,19 @@ function buildResponseFormat(mode) {
|
|
|
1111
1202
|
}
|
|
1112
1203
|
};
|
|
1113
1204
|
}
|
|
1114
|
-
function buildOpenAiRequest(config, payloadJson, mode = "strict-schema") {
|
|
1115
|
-
|
|
1116
|
-
model: config.model,
|
|
1117
|
-
max_completion_tokens: config.maxOutputTokens,
|
|
1205
|
+
function buildOpenAiRequest(config, payloadJson, mode = "strict-schema", tokenLimitField = "max_completion_tokens") {
|
|
1206
|
+
const base = {
|
|
1118
1207
|
messages: [
|
|
1119
1208
|
{ role: "system", content: OPENAI_SYSTEM_RULES },
|
|
1120
1209
|
{ role: "user", content: payloadJson }
|
|
1121
1210
|
],
|
|
1211
|
+
model: config.model,
|
|
1122
1212
|
response_format: buildResponseFormat(mode)
|
|
1123
1213
|
};
|
|
1214
|
+
if (tokenLimitField === "max_tokens") {
|
|
1215
|
+
return { ...base, max_tokens: config.maxOutputTokens };
|
|
1216
|
+
}
|
|
1217
|
+
return { ...base, max_completion_tokens: config.maxOutputTokens };
|
|
1124
1218
|
}
|
|
1125
1219
|
function advanceStringScan(char, escaped) {
|
|
1126
1220
|
if (escaped) {
|
|
@@ -1291,7 +1385,7 @@ function createOpenAiCompatibleProvider(config, deps = {}) {
|
|
|
1291
1385
|
function createMechanism4(client, config) {
|
|
1292
1386
|
return {
|
|
1293
1387
|
translate: async ({ payloadJson, signal }) => {
|
|
1294
|
-
const body = buildOpenAiRequest(config, payloadJson, "strict-schema");
|
|
1388
|
+
const body = buildOpenAiRequest(config, payloadJson, "strict-schema", "max_tokens");
|
|
1295
1389
|
const completion = await callClient5(client, body, signal);
|
|
1296
1390
|
return extractOpenAiResult(completion, true);
|
|
1297
1391
|
}
|
|
@@ -1332,6 +1426,17 @@ function buildProvider(config) {
|
|
|
1332
1426
|
|
|
1333
1427
|
// src/config/schema.ts
|
|
1334
1428
|
var DEFAULT_MAX_BATCH_SIZE = 50;
|
|
1429
|
+
function findCaseInsensitiveDuplicate(locales) {
|
|
1430
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1431
|
+
for (const locale of locales) {
|
|
1432
|
+
const key = locale.toLowerCase();
|
|
1433
|
+
if (seen.has(key)) {
|
|
1434
|
+
return locale;
|
|
1435
|
+
}
|
|
1436
|
+
seen.add(key);
|
|
1437
|
+
}
|
|
1438
|
+
return void 0;
|
|
1439
|
+
}
|
|
1335
1440
|
var verbatraConfigSchema = z.strictObject({
|
|
1336
1441
|
sourceLocale: z.string().min(1),
|
|
1337
1442
|
targetLocales: z.array(z.string().min(1)).min(1),
|
|
@@ -1373,6 +1478,14 @@ var verbatraConfigSchema = z.strictObject({
|
|
|
1373
1478
|
}).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
|
|
1374
1479
|
message: "targetLocales must not include the source locale",
|
|
1375
1480
|
path: ["targetLocales"]
|
|
1481
|
+
}).refine((config) => findCaseInsensitiveDuplicate(config.targetLocales) === void 0, {
|
|
1482
|
+
error: (issue) => {
|
|
1483
|
+
const duplicate = findCaseInsensitiveDuplicate(
|
|
1484
|
+
issue.input.targetLocales
|
|
1485
|
+
);
|
|
1486
|
+
return `targetLocales must not contain case-insensitively duplicate locales: "${duplicate}"`;
|
|
1487
|
+
},
|
|
1488
|
+
path: ["targetLocales"]
|
|
1376
1489
|
}).refine((config) => config.files.pattern.includes(LOCALE_TOKEN), {
|
|
1377
1490
|
message: `files.pattern must contain the ${LOCALE_TOKEN} token`,
|
|
1378
1491
|
path: ["files", "pattern"]
|
|
@@ -1892,6 +2005,9 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
|
1892
2005
|
throw error;
|
|
1893
2006
|
}
|
|
1894
2007
|
}
|
|
2008
|
+
function stripLeadingBom(content) {
|
|
2009
|
+
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
2010
|
+
}
|
|
1895
2011
|
async function readBoundedUtf82(handle, size) {
|
|
1896
2012
|
const buffer = Buffer.allocUnsafe(size);
|
|
1897
2013
|
let offset = 0;
|
|
@@ -1902,7 +2018,7 @@ async function readBoundedUtf82(handle, size) {
|
|
|
1902
2018
|
}
|
|
1903
2019
|
offset += bytesRead;
|
|
1904
2020
|
}
|
|
1905
|
-
return buffer.toString("utf8", 0, offset);
|
|
2021
|
+
return stripLeadingBom(buffer.toString("utf8", 0, offset));
|
|
1906
2022
|
}
|
|
1907
2023
|
async function readBounded2(filePath) {
|
|
1908
2024
|
const handle = await open(filePath, "r");
|
|
@@ -2085,11 +2201,24 @@ function unflattenEntries(entries) {
|
|
|
2085
2201
|
}
|
|
2086
2202
|
return root;
|
|
2087
2203
|
}
|
|
2088
|
-
function
|
|
2204
|
+
function mergeDescriptions(entries, content, deriveDescriptions) {
|
|
2205
|
+
if (!deriveDescriptions) {
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
for (const [key, description] of deriveDescriptions(content)) {
|
|
2209
|
+
const entry = entries.get(key);
|
|
2210
|
+
if (entry !== void 0) {
|
|
2211
|
+
entries.set(key, { ...entry, description });
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree, deriveDescriptions) {
|
|
2089
2216
|
try {
|
|
2090
2217
|
const tree = parse3(content);
|
|
2091
2218
|
validateTree?.(tree);
|
|
2092
|
-
|
|
2219
|
+
const entries = flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
2220
|
+
mergeDescriptions(entries, content, deriveDescriptions);
|
|
2221
|
+
return entries;
|
|
2093
2222
|
} catch (error) {
|
|
2094
2223
|
rethrowStructured(error, "The file could not be parsed.");
|
|
2095
2224
|
}
|
|
@@ -2108,6 +2237,7 @@ function createTreeFileAdapter(options) {
|
|
|
2108
2237
|
validateTree,
|
|
2109
2238
|
buildWriteTree,
|
|
2110
2239
|
comparePlaceholders,
|
|
2240
|
+
deriveDescriptions,
|
|
2111
2241
|
keyMode = "literal-leaf"
|
|
2112
2242
|
} = options;
|
|
2113
2243
|
return {
|
|
@@ -2119,7 +2249,15 @@ function createTreeFileAdapter(options) {
|
|
|
2119
2249
|
async read(filePath, locale) {
|
|
2120
2250
|
const content = await readFileContent(filePath);
|
|
2121
2251
|
const namespace = namespaceOf(filePath);
|
|
2122
|
-
const entries = toEntries(
|
|
2252
|
+
const entries = toEntries(
|
|
2253
|
+
content,
|
|
2254
|
+
namespace,
|
|
2255
|
+
parse3,
|
|
2256
|
+
deriveEntry,
|
|
2257
|
+
keyMode,
|
|
2258
|
+
validateTree,
|
|
2259
|
+
deriveDescriptions
|
|
2260
|
+
);
|
|
2123
2261
|
const resource = { locale, namespace, format, entries };
|
|
2124
2262
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
2125
2263
|
return { resource, invalidIcuKeys };
|
|
@@ -2160,6 +2298,31 @@ function stripArbMetadata(tree) {
|
|
|
2160
2298
|
function originalKey(encoded) {
|
|
2161
2299
|
return decodeKeyToSegments(encoded).join(".");
|
|
2162
2300
|
}
|
|
2301
|
+
function messageKeyForMetadata(key) {
|
|
2302
|
+
return isMetadataKey(key) && !key.startsWith("@@") ? key.slice(1) : null;
|
|
2303
|
+
}
|
|
2304
|
+
function descriptionOf(value) {
|
|
2305
|
+
if (typeof value !== "object" || value === null) {
|
|
2306
|
+
return void 0;
|
|
2307
|
+
}
|
|
2308
|
+
const description = value.description;
|
|
2309
|
+
return typeof description === "string" ? description : void 0;
|
|
2310
|
+
}
|
|
2311
|
+
function extractArbDescriptions(content) {
|
|
2312
|
+
const tree = parseArbObject(content);
|
|
2313
|
+
const out = /* @__PURE__ */ new Map();
|
|
2314
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
2315
|
+
const messageKey = messageKeyForMetadata(key);
|
|
2316
|
+
if (messageKey === null) {
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
const description = descriptionOf(value);
|
|
2320
|
+
if (description !== void 0) {
|
|
2321
|
+
out.set(encodeSegment(messageKey), description);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
return out;
|
|
2325
|
+
}
|
|
2163
2326
|
function messagesFromEntries(entries) {
|
|
2164
2327
|
const out = /* @__PURE__ */ new Map();
|
|
2165
2328
|
for (const [key, entry] of entries) {
|
|
@@ -2216,7 +2379,8 @@ function createArbAdapter() {
|
|
|
2216
2379
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
2217
2380
|
validateMessage: icuIsValid,
|
|
2218
2381
|
comparePlaceholders: compareIcuPlaceholders,
|
|
2219
|
-
buildWriteTree: buildArbWriteTree
|
|
2382
|
+
buildWriteTree: buildArbWriteTree,
|
|
2383
|
+
deriveDescriptions: extractArbDescriptions
|
|
2220
2384
|
});
|
|
2221
2385
|
}
|
|
2222
2386
|
function createJsonFileAdapter(options) {
|
|
@@ -2522,16 +2686,33 @@ function parseXml(content) {
|
|
|
2522
2686
|
}
|
|
2523
2687
|
return { doc, root };
|
|
2524
2688
|
}
|
|
2689
|
+
function firstNoteText(parent) {
|
|
2690
|
+
const note = childByName(parent, "note");
|
|
2691
|
+
if (note === null) {
|
|
2692
|
+
return void 0;
|
|
2693
|
+
}
|
|
2694
|
+
const text = (note.textContent ?? "").trim();
|
|
2695
|
+
return text === "" ? void 0 : text;
|
|
2696
|
+
}
|
|
2697
|
+
function transUnitDescription(tu) {
|
|
2698
|
+
return firstNoteText(tu);
|
|
2699
|
+
}
|
|
2700
|
+
function unitDescription(unit) {
|
|
2701
|
+
const notes = childByName(unit, "notes");
|
|
2702
|
+
return notes === null ? void 0 : firstNoteText(notes);
|
|
2703
|
+
}
|
|
2525
2704
|
function walkXliff12(root) {
|
|
2526
2705
|
const units = [];
|
|
2527
2706
|
collectByTag(root, "trans-unit").forEach((tu, index) => {
|
|
2528
2707
|
const source = childByName(tu, "source");
|
|
2529
2708
|
if (source !== null) {
|
|
2709
|
+
const description = transUnitDescription(tu);
|
|
2530
2710
|
units.push({
|
|
2531
2711
|
key: unitKey(tu, index),
|
|
2532
2712
|
source,
|
|
2533
2713
|
target: childByName(tu, "target"),
|
|
2534
|
-
container: tu
|
|
2714
|
+
container: tu,
|
|
2715
|
+
...description !== void 0 ? { description } : {}
|
|
2535
2716
|
});
|
|
2536
2717
|
}
|
|
2537
2718
|
});
|
|
@@ -2541,12 +2722,19 @@ function walkXliff20(root) {
|
|
|
2541
2722
|
const units = [];
|
|
2542
2723
|
collectByTag(root, "unit").forEach((unit, index) => {
|
|
2543
2724
|
const baseKey = unitKey(unit, index);
|
|
2725
|
+
const description = unitDescription(unit);
|
|
2544
2726
|
const segments = elementChildren(unit).filter((el) => el.localName === "segment");
|
|
2545
2727
|
segments.forEach((segment, segIndex) => {
|
|
2546
2728
|
const source = childByName(segment, "source");
|
|
2547
2729
|
if (source !== null) {
|
|
2548
2730
|
const key = segments.length > 1 ? `${baseKey}#${segIndex}` : baseKey;
|
|
2549
|
-
units.push({
|
|
2731
|
+
units.push({
|
|
2732
|
+
key,
|
|
2733
|
+
source,
|
|
2734
|
+
target: childByName(segment, "target"),
|
|
2735
|
+
container: segment,
|
|
2736
|
+
...description !== void 0 ? { description } : {}
|
|
2737
|
+
});
|
|
2550
2738
|
}
|
|
2551
2739
|
});
|
|
2552
2740
|
});
|
|
@@ -2585,7 +2773,8 @@ function parseXliffEntries(content, namespace) {
|
|
|
2585
2773
|
namespace,
|
|
2586
2774
|
value,
|
|
2587
2775
|
placeholders: extractXliffPlaceholders(value),
|
|
2588
|
-
isPlural: false
|
|
2776
|
+
isPlural: false,
|
|
2777
|
+
...unit.description !== void 0 ? { description: unit.description } : {}
|
|
2589
2778
|
});
|
|
2590
2779
|
}
|
|
2591
2780
|
return out;
|
|
@@ -2945,15 +3134,8 @@ function subBatchFailedNotice(count, error) {
|
|
|
2945
3134
|
}
|
|
2946
3135
|
|
|
2947
3136
|
// src/flow/notices.ts
|
|
2948
|
-
function isNotice(value) {
|
|
2949
|
-
return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.message === "string";
|
|
2950
|
-
}
|
|
2951
3137
|
function readNotices(result) {
|
|
2952
|
-
|
|
2953
|
-
if (!Array.isArray(candidate)) {
|
|
2954
|
-
return [];
|
|
2955
|
-
}
|
|
2956
|
-
return candidate.filter(isNotice);
|
|
3138
|
+
return result.notices ?? [];
|
|
2957
3139
|
}
|
|
2958
3140
|
|
|
2959
3141
|
// src/flow/plural-categories.ts
|
|
@@ -3157,20 +3339,26 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
|
|
|
3157
3339
|
return [subBatchFailedNotice(batch.length, error)];
|
|
3158
3340
|
}
|
|
3159
3341
|
for (const item of batch) {
|
|
3160
|
-
|
|
3161
|
-
const integrity = result.integrity.get(item.targetKey);
|
|
3162
|
-
if (value !== void 0 && integrity?.matches === true) {
|
|
3163
|
-
accepted.push({
|
|
3164
|
-
targetKey: item.targetKey,
|
|
3165
|
-
entry: { ...syntheticEntry(item), value },
|
|
3166
|
-
lockHash: generatedLockHash(item.governingEntries, item.category)
|
|
3167
|
-
});
|
|
3168
|
-
} else {
|
|
3169
|
-
withheld.push(item.targetKey);
|
|
3170
|
-
}
|
|
3342
|
+
foldGenerationItem(item, result, accepted, withheld, providerFailures);
|
|
3171
3343
|
}
|
|
3172
3344
|
return readNotices(result);
|
|
3173
3345
|
}
|
|
3346
|
+
function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
|
|
3347
|
+
const value = result.values.get(item.targetKey);
|
|
3348
|
+
if (value === void 0) {
|
|
3349
|
+
providerFailures.push(item.targetKey);
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
if (result.integrity.get(item.targetKey)?.matches === true) {
|
|
3353
|
+
accepted.push({
|
|
3354
|
+
targetKey: item.targetKey,
|
|
3355
|
+
entry: { ...syntheticEntry(item), value },
|
|
3356
|
+
lockHash: generatedLockHash(item.governingEntries, item.category)
|
|
3357
|
+
});
|
|
3358
|
+
} else {
|
|
3359
|
+
withheld.push(item.targetKey);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3174
3362
|
|
|
3175
3363
|
// src/flow/locale-run.ts
|
|
3176
3364
|
function emptyResource(locale, format) {
|
|
@@ -3358,16 +3546,22 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
|
|
|
3358
3546
|
return [subBatchFailedNotice(batch.length, error)];
|
|
3359
3547
|
}
|
|
3360
3548
|
for (const entry of batch) {
|
|
3361
|
-
|
|
3362
|
-
const integrity = result.integrity.get(entry.key);
|
|
3363
|
-
if (value !== void 0 && integrity?.matches === true) {
|
|
3364
|
-
accepted.set(entry.key, { value, source: entry });
|
|
3365
|
-
} else {
|
|
3366
|
-
integrityMismatches.push(entry.key);
|
|
3367
|
-
}
|
|
3549
|
+
foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
|
|
3368
3550
|
}
|
|
3369
3551
|
return readNotices(result);
|
|
3370
3552
|
}
|
|
3553
|
+
function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
|
|
3554
|
+
const value = result.values.get(entry.key);
|
|
3555
|
+
if (value === void 0) {
|
|
3556
|
+
providerFailures.push(entry.key);
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
if (result.integrity.get(entry.key)?.matches === true) {
|
|
3560
|
+
accepted.set(entry.key, { value, source: entry });
|
|
3561
|
+
} else {
|
|
3562
|
+
integrityMismatches.push(entry.key);
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3371
3565
|
function computeLockEntries(params, merged, withheld, generated) {
|
|
3372
3566
|
const lockEntries = {};
|
|
3373
3567
|
const sourceBaseKeys = sourcePluralBaseKeys(params.source);
|
|
@@ -3471,10 +3665,13 @@ var INSTRUCTIONS_LINES = [
|
|
|
3471
3665
|
" structure and the argument names exactly. Translate only the human-readable text.",
|
|
3472
3666
|
"6. Do not edit the 'Key' column or the hidden 'Source hash' column. They map your translation",
|
|
3473
3667
|
" back to the right string. You may sort or filter rows freely; mapping does not depend on row order.",
|
|
3668
|
+
"7. The 'Context' column, when present, is reference text from the source file explaining what the",
|
|
3669
|
+
" string means or where it is used. It is read-only; leave it as-is.",
|
|
3474
3670
|
"",
|
|
3475
3671
|
"Status values:",
|
|
3476
3672
|
" new - this string has no translation yet.",
|
|
3477
3673
|
" changed - the source string changed and the translation needs updating.",
|
|
3674
|
+
" unchanged - already up to date, included for reference. No action needed.",
|
|
3478
3675
|
"",
|
|
3479
3676
|
"When you are done, save the file and send it back. verbatra checks every value on import:",
|
|
3480
3677
|
"a value with a broken placeholder, invalid ICU, or a source that changed since export is",
|
|
@@ -3486,7 +3683,8 @@ var COLUMN = {
|
|
|
3486
3683
|
current: 3,
|
|
3487
3684
|
status: 4,
|
|
3488
3685
|
translation: 5,
|
|
3489
|
-
sourceHash: 6
|
|
3686
|
+
sourceHash: 6,
|
|
3687
|
+
context: 7
|
|
3490
3688
|
};
|
|
3491
3689
|
var HEADERS = [
|
|
3492
3690
|
"Key",
|
|
@@ -3494,7 +3692,8 @@ var HEADERS = [
|
|
|
3494
3692
|
"Current translation",
|
|
3495
3693
|
"Status",
|
|
3496
3694
|
"Translation",
|
|
3497
|
-
"Source hash"
|
|
3695
|
+
"Source hash",
|
|
3696
|
+
"Context"
|
|
3498
3697
|
];
|
|
3499
3698
|
var HEADER_ROW = 1;
|
|
3500
3699
|
var INSTRUCTIONS_SHEET_NAME = "Instructions";
|
|
@@ -3513,7 +3712,8 @@ var COLUMN_WIDTHS = {
|
|
|
3513
3712
|
[COLUMN.source]: 50,
|
|
3514
3713
|
[COLUMN.current]: 50,
|
|
3515
3714
|
[COLUMN.status]: 12,
|
|
3516
|
-
[COLUMN.translation]: 50
|
|
3715
|
+
[COLUMN.translation]: 50,
|
|
3716
|
+
[COLUMN.context]: 50
|
|
3517
3717
|
};
|
|
3518
3718
|
var TEXT_NUMBER_FORMAT = "@";
|
|
3519
3719
|
function styleHeader(sheet) {
|
|
@@ -3544,7 +3744,8 @@ function writeRow(sheet, sheetRow) {
|
|
|
3544
3744
|
translationCell.numFmt = TEXT_NUMBER_FORMAT;
|
|
3545
3745
|
translationCell.value = sheetRow.translation === "" ? null : sheetRow.translation;
|
|
3546
3746
|
row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
|
|
3547
|
-
|
|
3747
|
+
row.getCell(COLUMN.context).value = sheetRow.context;
|
|
3748
|
+
for (let column = COLUMN.key; column <= COLUMN.context; column += 1) {
|
|
3548
3749
|
const cell = row.getCell(column);
|
|
3549
3750
|
cell.protection = { locked: column !== COLUMN.translation };
|
|
3550
3751
|
if (column !== COLUMN.translation) {
|
|
@@ -3569,6 +3770,26 @@ function assertValidWorksheetName(locale) {
|
|
|
3569
3770
|
);
|
|
3570
3771
|
}
|
|
3571
3772
|
}
|
|
3773
|
+
function assertNoWorksheetNameCollisions(sheets) {
|
|
3774
|
+
const reservedKey = INSTRUCTIONS_SHEET_NAME.toLowerCase();
|
|
3775
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3776
|
+
for (const sheet of sheets) {
|
|
3777
|
+
const key = sheet.locale.toLowerCase();
|
|
3778
|
+
if (key === reservedKey) {
|
|
3779
|
+
throw new ExchangeError(
|
|
3780
|
+
"WORKBOOK_INVALID",
|
|
3781
|
+
`The locale "${sheet.locale}" cannot be an Excel worksheet name: it collides with the reserved "${INSTRUCTIONS_SHEET_NAME}" sheet.`
|
|
3782
|
+
);
|
|
3783
|
+
}
|
|
3784
|
+
if (seen.has(key)) {
|
|
3785
|
+
throw new ExchangeError(
|
|
3786
|
+
"WORKBOOK_INVALID",
|
|
3787
|
+
`The locale "${sheet.locale}" cannot be an Excel worksheet name: it collides with another target locale's sheet name.`
|
|
3788
|
+
);
|
|
3789
|
+
}
|
|
3790
|
+
seen.add(key);
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3572
3793
|
async function buildDataSheet(workbook, sheet) {
|
|
3573
3794
|
assertValidWorksheetName(sheet.locale);
|
|
3574
3795
|
const worksheet = workbook.addWorksheet(sheet.locale);
|
|
@@ -3595,6 +3816,7 @@ function buildInstructionsSheet(workbook) {
|
|
|
3595
3816
|
sheet.getRow(1).font = { bold: true };
|
|
3596
3817
|
}
|
|
3597
3818
|
async function buildWorkbook(model) {
|
|
3819
|
+
assertNoWorksheetNameCollisions(model.sheets);
|
|
3598
3820
|
const workbook = new ExcelJS.Workbook();
|
|
3599
3821
|
buildInstructionsSheet(workbook);
|
|
3600
3822
|
for (const sheet of model.sheets) {
|
|
@@ -3695,9 +3917,10 @@ var rowSchema = z.object({
|
|
|
3695
3917
|
key: z.string().min(1),
|
|
3696
3918
|
source: z.string(),
|
|
3697
3919
|
currentTarget: z.string(),
|
|
3698
|
-
status: z.enum(["new", "changed"]),
|
|
3920
|
+
status: z.enum(["new", "changed", "unchanged"]),
|
|
3699
3921
|
sourceHash: z.string(),
|
|
3700
|
-
translation: z.string()
|
|
3922
|
+
translation: z.string(),
|
|
3923
|
+
context: z.string()
|
|
3701
3924
|
});
|
|
3702
3925
|
function cellString(cell) {
|
|
3703
3926
|
const value = cell.value;
|
|
@@ -3730,7 +3953,10 @@ function parseRow(sheet, row) {
|
|
|
3730
3953
|
currentTarget: cellString(row.getCell(COLUMN.current)),
|
|
3731
3954
|
status: cellString(row.getCell(COLUMN.status)),
|
|
3732
3955
|
sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
|
|
3733
|
-
translation: cellString(row.getCell(COLUMN.translation))
|
|
3956
|
+
translation: cellString(row.getCell(COLUMN.translation)),
|
|
3957
|
+
// A workbook built before the Context column existed has no cell here; getCell auto-vivifies an
|
|
3958
|
+
// empty one, so cellString yields "" and the row still validates, keeping import backward-compatible.
|
|
3959
|
+
context: cellString(row.getCell(COLUMN.context))
|
|
3734
3960
|
};
|
|
3735
3961
|
const result = rowSchema.safeParse(candidate);
|
|
3736
3962
|
if (!result.success) {
|
|
@@ -3819,14 +4045,15 @@ function buildRows(source, target, baseline, includeUnchanged) {
|
|
|
3819
4045
|
currentTarget: target.entries.get(key)?.value ?? "",
|
|
3820
4046
|
status,
|
|
3821
4047
|
sourceHash: contentHash(sourceEntry),
|
|
3822
|
-
translation: ""
|
|
4048
|
+
translation: "",
|
|
4049
|
+
context: sourceEntry.description ?? ""
|
|
3823
4050
|
});
|
|
3824
4051
|
}
|
|
3825
4052
|
};
|
|
3826
4053
|
add(diff2.missing, "new");
|
|
3827
4054
|
add(diff2.changed, "changed");
|
|
3828
4055
|
if (includeUnchanged) {
|
|
3829
|
-
add(diff2.unchanged, "
|
|
4056
|
+
add(diff2.unchanged, "unchanged");
|
|
3830
4057
|
}
|
|
3831
4058
|
return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
|
|
3832
4059
|
}
|