@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/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) {
|
|
@@ -773,6 +807,50 @@ function createDefaultClient2() {
|
|
|
773
807
|
};
|
|
774
808
|
return { client, freeAccount };
|
|
775
809
|
}
|
|
810
|
+
var DEEPL_MAX_TEXTS_PER_REQUEST = 50;
|
|
811
|
+
var DEEPL_MAX_PAYLOAD_BYTES = 128 * 1024;
|
|
812
|
+
var DEEPL_PAYLOAD_OVERHEAD_RESERVE_BYTES = 4 * 1024;
|
|
813
|
+
var DEEPL_MAX_TEXT_PAYLOAD_BYTES = DEEPL_MAX_PAYLOAD_BYTES - DEEPL_PAYLOAD_OVERHEAD_RESERVE_BYTES;
|
|
814
|
+
function estimateWireBytes(text) {
|
|
815
|
+
return encodeURIComponent(text).length;
|
|
816
|
+
}
|
|
817
|
+
function chunkTextsForDeepL(texts) {
|
|
818
|
+
const chunks = [];
|
|
819
|
+
let current = [];
|
|
820
|
+
let currentBytes = 0;
|
|
821
|
+
for (const text of texts) {
|
|
822
|
+
const textBytes = estimateWireBytes(text);
|
|
823
|
+
const startsNewChunk = current.length > 0 && (current.length >= DEEPL_MAX_TEXTS_PER_REQUEST || currentBytes + textBytes > DEEPL_MAX_TEXT_PAYLOAD_BYTES);
|
|
824
|
+
if (startsNewChunk) {
|
|
825
|
+
chunks.push(current);
|
|
826
|
+
current = [];
|
|
827
|
+
currentBytes = 0;
|
|
828
|
+
}
|
|
829
|
+
current.push(text);
|
|
830
|
+
currentBytes += textBytes;
|
|
831
|
+
}
|
|
832
|
+
if (current.length > 0) {
|
|
833
|
+
chunks.push(current);
|
|
834
|
+
}
|
|
835
|
+
return chunks;
|
|
836
|
+
}
|
|
837
|
+
var DEPRECATED_BARE_TARGET_CODES = /* @__PURE__ */ new Set(["en", "pt"]);
|
|
838
|
+
function assertValidDeepLSourceLocale(locale) {
|
|
839
|
+
if (locale.includes("-")) {
|
|
840
|
+
throw new ProviderError(
|
|
841
|
+
"INVALID_REQUEST",
|
|
842
|
+
`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").`
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function assertValidDeepLTargetLocale(locale) {
|
|
847
|
+
if (DEPRECATED_BARE_TARGET_CODES.has(locale.toLowerCase())) {
|
|
848
|
+
throw new ProviderError(
|
|
849
|
+
"INVALID_REQUEST",
|
|
850
|
+
`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).`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
776
854
|
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.";
|
|
777
855
|
function partitionByPlaceholders(entries) {
|
|
778
856
|
const protectable = [];
|
|
@@ -837,7 +915,10 @@ function createDeepLProvider(config, deps = {}) {
|
|
|
837
915
|
return {
|
|
838
916
|
id: PROVIDER_ID2,
|
|
839
917
|
kind: "machine-translation",
|
|
840
|
-
|
|
918
|
+
// Honest capability signal: DeepL only ever applies a pre-created native glossary id, never a
|
|
919
|
+
// generic term map (see buildTranslateOptions). Without one configured, a supplied term map is
|
|
920
|
+
// silently ignored and surfaces only as a GLOSSARY_IGNORED notice.
|
|
921
|
+
supportsGlossary: validConfig.glossaryId !== void 0,
|
|
841
922
|
translateBatch: (request) => translate(bundle, validConfig, request)
|
|
842
923
|
};
|
|
843
924
|
}
|
|
@@ -849,6 +930,8 @@ function resolveClient(deps) {
|
|
|
849
930
|
}
|
|
850
931
|
async function translate(bundle, config, request) {
|
|
851
932
|
const data = validateRequest(request);
|
|
933
|
+
assertValidDeepLSourceLocale(data.sourceLocale);
|
|
934
|
+
assertValidDeepLTargetLocale(data.targetLocale);
|
|
852
935
|
const { protectable, unprotectable } = partitionByPlaceholders(data.entries);
|
|
853
936
|
const genericGlossarySupplied = request.glossary !== void 0 && Object.keys(request.glossary).length > 0;
|
|
854
937
|
const { options, notices } = buildTranslateOptions({
|
|
@@ -876,7 +959,7 @@ async function translateProtectable(client, data, protectable, options, extract,
|
|
|
876
959
|
return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
|
|
877
960
|
}
|
|
878
961
|
const texts = protectable.map((entry) => entry.value);
|
|
879
|
-
const results = await
|
|
962
|
+
const results = await callClientChunked(
|
|
880
963
|
client,
|
|
881
964
|
texts,
|
|
882
965
|
data.sourceLocale,
|
|
@@ -894,6 +977,14 @@ function callClient2(client, texts, sourceLang, targetLang, options, signal) {
|
|
|
894
977
|
signal
|
|
895
978
|
);
|
|
896
979
|
}
|
|
980
|
+
async function callClientChunked(client, texts, sourceLang, targetLang, options, signal) {
|
|
981
|
+
const results = [];
|
|
982
|
+
for (const chunk2 of chunkTextsForDeepL(texts)) {
|
|
983
|
+
const chunkResults = await callClient2(client, chunk2, sourceLang, targetLang, options, signal);
|
|
984
|
+
results.push(...chunkResults);
|
|
985
|
+
}
|
|
986
|
+
return results;
|
|
987
|
+
}
|
|
897
988
|
var geminiConfigSchema = zod.z.object({
|
|
898
989
|
model: zod.z.string().min(1),
|
|
899
990
|
maxOutputTokens: zod.z.number().int().positive()
|
|
@@ -1141,16 +1232,19 @@ function buildResponseFormat(mode) {
|
|
|
1141
1232
|
}
|
|
1142
1233
|
};
|
|
1143
1234
|
}
|
|
1144
|
-
function buildOpenAiRequest(config, payloadJson, mode = "strict-schema") {
|
|
1145
|
-
|
|
1146
|
-
model: config.model,
|
|
1147
|
-
max_completion_tokens: config.maxOutputTokens,
|
|
1235
|
+
function buildOpenAiRequest(config, payloadJson, mode = "strict-schema", tokenLimitField = "max_completion_tokens") {
|
|
1236
|
+
const base = {
|
|
1148
1237
|
messages: [
|
|
1149
1238
|
{ role: "system", content: OPENAI_SYSTEM_RULES },
|
|
1150
1239
|
{ role: "user", content: payloadJson }
|
|
1151
1240
|
],
|
|
1241
|
+
model: config.model,
|
|
1152
1242
|
response_format: buildResponseFormat(mode)
|
|
1153
1243
|
};
|
|
1244
|
+
if (tokenLimitField === "max_tokens") {
|
|
1245
|
+
return { ...base, max_tokens: config.maxOutputTokens };
|
|
1246
|
+
}
|
|
1247
|
+
return { ...base, max_completion_tokens: config.maxOutputTokens };
|
|
1154
1248
|
}
|
|
1155
1249
|
function advanceStringScan(char, escaped) {
|
|
1156
1250
|
if (escaped) {
|
|
@@ -1321,7 +1415,7 @@ function createOpenAiCompatibleProvider(config, deps = {}) {
|
|
|
1321
1415
|
function createMechanism4(client, config) {
|
|
1322
1416
|
return {
|
|
1323
1417
|
translate: async ({ payloadJson, signal }) => {
|
|
1324
|
-
const body = buildOpenAiRequest(config, payloadJson, "strict-schema");
|
|
1418
|
+
const body = buildOpenAiRequest(config, payloadJson, "strict-schema", "max_tokens");
|
|
1325
1419
|
const completion = await callClient5(client, body, signal);
|
|
1326
1420
|
return extractOpenAiResult(completion, true);
|
|
1327
1421
|
}
|
|
@@ -1362,6 +1456,17 @@ function buildProvider(config) {
|
|
|
1362
1456
|
|
|
1363
1457
|
// src/config/schema.ts
|
|
1364
1458
|
var DEFAULT_MAX_BATCH_SIZE = 50;
|
|
1459
|
+
function findCaseInsensitiveDuplicate(locales) {
|
|
1460
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1461
|
+
for (const locale of locales) {
|
|
1462
|
+
const key = locale.toLowerCase();
|
|
1463
|
+
if (seen.has(key)) {
|
|
1464
|
+
return locale;
|
|
1465
|
+
}
|
|
1466
|
+
seen.add(key);
|
|
1467
|
+
}
|
|
1468
|
+
return void 0;
|
|
1469
|
+
}
|
|
1365
1470
|
var verbatraConfigSchema = zod.z.strictObject({
|
|
1366
1471
|
sourceLocale: zod.z.string().min(1),
|
|
1367
1472
|
targetLocales: zod.z.array(zod.z.string().min(1)).min(1),
|
|
@@ -1403,6 +1508,14 @@ var verbatraConfigSchema = zod.z.strictObject({
|
|
|
1403
1508
|
}).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
|
|
1404
1509
|
message: "targetLocales must not include the source locale",
|
|
1405
1510
|
path: ["targetLocales"]
|
|
1511
|
+
}).refine((config) => findCaseInsensitiveDuplicate(config.targetLocales) === void 0, {
|
|
1512
|
+
error: (issue) => {
|
|
1513
|
+
const duplicate = findCaseInsensitiveDuplicate(
|
|
1514
|
+
issue.input.targetLocales
|
|
1515
|
+
);
|
|
1516
|
+
return `targetLocales must not contain case-insensitively duplicate locales: "${duplicate}"`;
|
|
1517
|
+
},
|
|
1518
|
+
path: ["targetLocales"]
|
|
1406
1519
|
}).refine((config) => config.files.pattern.includes(LOCALE_TOKEN), {
|
|
1407
1520
|
message: `files.pattern must contain the ${LOCALE_TOKEN} token`,
|
|
1408
1521
|
path: ["files", "pattern"]
|
|
@@ -1922,6 +2035,9 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
|
1922
2035
|
throw error;
|
|
1923
2036
|
}
|
|
1924
2037
|
}
|
|
2038
|
+
function stripLeadingBom(content) {
|
|
2039
|
+
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
2040
|
+
}
|
|
1925
2041
|
async function readBoundedUtf82(handle, size) {
|
|
1926
2042
|
const buffer = Buffer.allocUnsafe(size);
|
|
1927
2043
|
let offset = 0;
|
|
@@ -1932,7 +2048,7 @@ async function readBoundedUtf82(handle, size) {
|
|
|
1932
2048
|
}
|
|
1933
2049
|
offset += bytesRead;
|
|
1934
2050
|
}
|
|
1935
|
-
return buffer.toString("utf8", 0, offset);
|
|
2051
|
+
return stripLeadingBom(buffer.toString("utf8", 0, offset));
|
|
1936
2052
|
}
|
|
1937
2053
|
async function readBounded2(filePath) {
|
|
1938
2054
|
const handle = await promises.open(filePath, "r");
|
|
@@ -2115,11 +2231,24 @@ function unflattenEntries(entries) {
|
|
|
2115
2231
|
}
|
|
2116
2232
|
return root;
|
|
2117
2233
|
}
|
|
2118
|
-
function
|
|
2234
|
+
function mergeDescriptions(entries, content, deriveDescriptions) {
|
|
2235
|
+
if (!deriveDescriptions) {
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
for (const [key, description] of deriveDescriptions(content)) {
|
|
2239
|
+
const entry = entries.get(key);
|
|
2240
|
+
if (entry !== void 0) {
|
|
2241
|
+
entries.set(key, { ...entry, description });
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree, deriveDescriptions) {
|
|
2119
2246
|
try {
|
|
2120
2247
|
const tree = parse3(content);
|
|
2121
2248
|
validateTree?.(tree);
|
|
2122
|
-
|
|
2249
|
+
const entries = flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
2250
|
+
mergeDescriptions(entries, content, deriveDescriptions);
|
|
2251
|
+
return entries;
|
|
2123
2252
|
} catch (error) {
|
|
2124
2253
|
rethrowStructured(error, "The file could not be parsed.");
|
|
2125
2254
|
}
|
|
@@ -2138,6 +2267,7 @@ function createTreeFileAdapter(options) {
|
|
|
2138
2267
|
validateTree,
|
|
2139
2268
|
buildWriteTree,
|
|
2140
2269
|
comparePlaceholders,
|
|
2270
|
+
deriveDescriptions,
|
|
2141
2271
|
keyMode = "literal-leaf"
|
|
2142
2272
|
} = options;
|
|
2143
2273
|
return {
|
|
@@ -2149,7 +2279,15 @@ function createTreeFileAdapter(options) {
|
|
|
2149
2279
|
async read(filePath, locale) {
|
|
2150
2280
|
const content = await readFileContent(filePath);
|
|
2151
2281
|
const namespace = namespaceOf(filePath);
|
|
2152
|
-
const entries = toEntries(
|
|
2282
|
+
const entries = toEntries(
|
|
2283
|
+
content,
|
|
2284
|
+
namespace,
|
|
2285
|
+
parse3,
|
|
2286
|
+
deriveEntry,
|
|
2287
|
+
keyMode,
|
|
2288
|
+
validateTree,
|
|
2289
|
+
deriveDescriptions
|
|
2290
|
+
);
|
|
2153
2291
|
const resource = { locale, namespace, format, entries };
|
|
2154
2292
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
2155
2293
|
return { resource, invalidIcuKeys };
|
|
@@ -2190,6 +2328,31 @@ function stripArbMetadata(tree) {
|
|
|
2190
2328
|
function originalKey(encoded) {
|
|
2191
2329
|
return decodeKeyToSegments(encoded).join(".");
|
|
2192
2330
|
}
|
|
2331
|
+
function messageKeyForMetadata(key) {
|
|
2332
|
+
return isMetadataKey(key) && !key.startsWith("@@") ? key.slice(1) : null;
|
|
2333
|
+
}
|
|
2334
|
+
function descriptionOf(value) {
|
|
2335
|
+
if (typeof value !== "object" || value === null) {
|
|
2336
|
+
return void 0;
|
|
2337
|
+
}
|
|
2338
|
+
const description = value.description;
|
|
2339
|
+
return typeof description === "string" ? description : void 0;
|
|
2340
|
+
}
|
|
2341
|
+
function extractArbDescriptions(content) {
|
|
2342
|
+
const tree = parseArbObject(content);
|
|
2343
|
+
const out = /* @__PURE__ */ new Map();
|
|
2344
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
2345
|
+
const messageKey = messageKeyForMetadata(key);
|
|
2346
|
+
if (messageKey === null) {
|
|
2347
|
+
continue;
|
|
2348
|
+
}
|
|
2349
|
+
const description = descriptionOf(value);
|
|
2350
|
+
if (description !== void 0) {
|
|
2351
|
+
out.set(encodeSegment(messageKey), description);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
return out;
|
|
2355
|
+
}
|
|
2193
2356
|
function messagesFromEntries(entries) {
|
|
2194
2357
|
const out = /* @__PURE__ */ new Map();
|
|
2195
2358
|
for (const [key, entry] of entries) {
|
|
@@ -2246,7 +2409,8 @@ function createArbAdapter() {
|
|
|
2246
2409
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
2247
2410
|
validateMessage: icuIsValid,
|
|
2248
2411
|
comparePlaceholders: compareIcuPlaceholders,
|
|
2249
|
-
buildWriteTree: buildArbWriteTree
|
|
2412
|
+
buildWriteTree: buildArbWriteTree,
|
|
2413
|
+
deriveDescriptions: extractArbDescriptions
|
|
2250
2414
|
});
|
|
2251
2415
|
}
|
|
2252
2416
|
function createJsonFileAdapter(options) {
|
|
@@ -2552,16 +2716,33 @@ function parseXml(content) {
|
|
|
2552
2716
|
}
|
|
2553
2717
|
return { doc, root };
|
|
2554
2718
|
}
|
|
2719
|
+
function firstNoteText(parent) {
|
|
2720
|
+
const note = childByName(parent, "note");
|
|
2721
|
+
if (note === null) {
|
|
2722
|
+
return void 0;
|
|
2723
|
+
}
|
|
2724
|
+
const text = (note.textContent ?? "").trim();
|
|
2725
|
+
return text === "" ? void 0 : text;
|
|
2726
|
+
}
|
|
2727
|
+
function transUnitDescription(tu) {
|
|
2728
|
+
return firstNoteText(tu);
|
|
2729
|
+
}
|
|
2730
|
+
function unitDescription(unit) {
|
|
2731
|
+
const notes = childByName(unit, "notes");
|
|
2732
|
+
return notes === null ? void 0 : firstNoteText(notes);
|
|
2733
|
+
}
|
|
2555
2734
|
function walkXliff12(root) {
|
|
2556
2735
|
const units = [];
|
|
2557
2736
|
collectByTag(root, "trans-unit").forEach((tu, index) => {
|
|
2558
2737
|
const source = childByName(tu, "source");
|
|
2559
2738
|
if (source !== null) {
|
|
2739
|
+
const description = transUnitDescription(tu);
|
|
2560
2740
|
units.push({
|
|
2561
2741
|
key: unitKey(tu, index),
|
|
2562
2742
|
source,
|
|
2563
2743
|
target: childByName(tu, "target"),
|
|
2564
|
-
container: tu
|
|
2744
|
+
container: tu,
|
|
2745
|
+
...description !== void 0 ? { description } : {}
|
|
2565
2746
|
});
|
|
2566
2747
|
}
|
|
2567
2748
|
});
|
|
@@ -2571,12 +2752,19 @@ function walkXliff20(root) {
|
|
|
2571
2752
|
const units = [];
|
|
2572
2753
|
collectByTag(root, "unit").forEach((unit, index) => {
|
|
2573
2754
|
const baseKey = unitKey(unit, index);
|
|
2755
|
+
const description = unitDescription(unit);
|
|
2574
2756
|
const segments = elementChildren(unit).filter((el) => el.localName === "segment");
|
|
2575
2757
|
segments.forEach((segment, segIndex) => {
|
|
2576
2758
|
const source = childByName(segment, "source");
|
|
2577
2759
|
if (source !== null) {
|
|
2578
2760
|
const key = segments.length > 1 ? `${baseKey}#${segIndex}` : baseKey;
|
|
2579
|
-
units.push({
|
|
2761
|
+
units.push({
|
|
2762
|
+
key,
|
|
2763
|
+
source,
|
|
2764
|
+
target: childByName(segment, "target"),
|
|
2765
|
+
container: segment,
|
|
2766
|
+
...description !== void 0 ? { description } : {}
|
|
2767
|
+
});
|
|
2580
2768
|
}
|
|
2581
2769
|
});
|
|
2582
2770
|
});
|
|
@@ -2615,7 +2803,8 @@ function parseXliffEntries(content, namespace) {
|
|
|
2615
2803
|
namespace,
|
|
2616
2804
|
value,
|
|
2617
2805
|
placeholders: extractXliffPlaceholders(value),
|
|
2618
|
-
isPlural: false
|
|
2806
|
+
isPlural: false,
|
|
2807
|
+
...unit.description !== void 0 ? { description: unit.description } : {}
|
|
2619
2808
|
});
|
|
2620
2809
|
}
|
|
2621
2810
|
return out;
|
|
@@ -2975,15 +3164,8 @@ function subBatchFailedNotice(count, error) {
|
|
|
2975
3164
|
}
|
|
2976
3165
|
|
|
2977
3166
|
// 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
3167
|
function readNotices(result) {
|
|
2982
|
-
|
|
2983
|
-
if (!Array.isArray(candidate)) {
|
|
2984
|
-
return [];
|
|
2985
|
-
}
|
|
2986
|
-
return candidate.filter(isNotice);
|
|
3168
|
+
return result.notices ?? [];
|
|
2987
3169
|
}
|
|
2988
3170
|
|
|
2989
3171
|
// src/flow/plural-categories.ts
|
|
@@ -3187,20 +3369,26 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
|
|
|
3187
3369
|
return [subBatchFailedNotice(batch.length, error)];
|
|
3188
3370
|
}
|
|
3189
3371
|
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
|
-
}
|
|
3372
|
+
foldGenerationItem(item, result, accepted, withheld, providerFailures);
|
|
3201
3373
|
}
|
|
3202
3374
|
return readNotices(result);
|
|
3203
3375
|
}
|
|
3376
|
+
function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
|
|
3377
|
+
const value = result.values.get(item.targetKey);
|
|
3378
|
+
if (value === void 0) {
|
|
3379
|
+
providerFailures.push(item.targetKey);
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
if (result.integrity.get(item.targetKey)?.matches === true) {
|
|
3383
|
+
accepted.push({
|
|
3384
|
+
targetKey: item.targetKey,
|
|
3385
|
+
entry: { ...syntheticEntry(item), value },
|
|
3386
|
+
lockHash: generatedLockHash(item.governingEntries, item.category)
|
|
3387
|
+
});
|
|
3388
|
+
} else {
|
|
3389
|
+
withheld.push(item.targetKey);
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3204
3392
|
|
|
3205
3393
|
// src/flow/locale-run.ts
|
|
3206
3394
|
function emptyResource(locale, format) {
|
|
@@ -3388,16 +3576,22 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
|
|
|
3388
3576
|
return [subBatchFailedNotice(batch.length, error)];
|
|
3389
3577
|
}
|
|
3390
3578
|
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
|
-
}
|
|
3579
|
+
foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
|
|
3398
3580
|
}
|
|
3399
3581
|
return readNotices(result);
|
|
3400
3582
|
}
|
|
3583
|
+
function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
|
|
3584
|
+
const value = result.values.get(entry.key);
|
|
3585
|
+
if (value === void 0) {
|
|
3586
|
+
providerFailures.push(entry.key);
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
if (result.integrity.get(entry.key)?.matches === true) {
|
|
3590
|
+
accepted.set(entry.key, { value, source: entry });
|
|
3591
|
+
} else {
|
|
3592
|
+
integrityMismatches.push(entry.key);
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3401
3595
|
function computeLockEntries(params, merged, withheld, generated) {
|
|
3402
3596
|
const lockEntries = {};
|
|
3403
3597
|
const sourceBaseKeys = sourcePluralBaseKeys(params.source);
|
|
@@ -3501,10 +3695,13 @@ var INSTRUCTIONS_LINES = [
|
|
|
3501
3695
|
" structure and the argument names exactly. Translate only the human-readable text.",
|
|
3502
3696
|
"6. Do not edit the 'Key' column or the hidden 'Source hash' column. They map your translation",
|
|
3503
3697
|
" back to the right string. You may sort or filter rows freely; mapping does not depend on row order.",
|
|
3698
|
+
"7. The 'Context' column, when present, is reference text from the source file explaining what the",
|
|
3699
|
+
" string means or where it is used. It is read-only; leave it as-is.",
|
|
3504
3700
|
"",
|
|
3505
3701
|
"Status values:",
|
|
3506
3702
|
" new - this string has no translation yet.",
|
|
3507
3703
|
" changed - the source string changed and the translation needs updating.",
|
|
3704
|
+
" unchanged - already up to date, included for reference. No action needed.",
|
|
3508
3705
|
"",
|
|
3509
3706
|
"When you are done, save the file and send it back. verbatra checks every value on import:",
|
|
3510
3707
|
"a value with a broken placeholder, invalid ICU, or a source that changed since export is",
|
|
@@ -3516,7 +3713,8 @@ var COLUMN = {
|
|
|
3516
3713
|
current: 3,
|
|
3517
3714
|
status: 4,
|
|
3518
3715
|
translation: 5,
|
|
3519
|
-
sourceHash: 6
|
|
3716
|
+
sourceHash: 6,
|
|
3717
|
+
context: 7
|
|
3520
3718
|
};
|
|
3521
3719
|
var HEADERS = [
|
|
3522
3720
|
"Key",
|
|
@@ -3524,7 +3722,8 @@ var HEADERS = [
|
|
|
3524
3722
|
"Current translation",
|
|
3525
3723
|
"Status",
|
|
3526
3724
|
"Translation",
|
|
3527
|
-
"Source hash"
|
|
3725
|
+
"Source hash",
|
|
3726
|
+
"Context"
|
|
3528
3727
|
];
|
|
3529
3728
|
var HEADER_ROW = 1;
|
|
3530
3729
|
var INSTRUCTIONS_SHEET_NAME = "Instructions";
|
|
@@ -3543,7 +3742,8 @@ var COLUMN_WIDTHS = {
|
|
|
3543
3742
|
[COLUMN.source]: 50,
|
|
3544
3743
|
[COLUMN.current]: 50,
|
|
3545
3744
|
[COLUMN.status]: 12,
|
|
3546
|
-
[COLUMN.translation]: 50
|
|
3745
|
+
[COLUMN.translation]: 50,
|
|
3746
|
+
[COLUMN.context]: 50
|
|
3547
3747
|
};
|
|
3548
3748
|
var TEXT_NUMBER_FORMAT = "@";
|
|
3549
3749
|
function styleHeader(sheet) {
|
|
@@ -3574,7 +3774,8 @@ function writeRow(sheet, sheetRow) {
|
|
|
3574
3774
|
translationCell.numFmt = TEXT_NUMBER_FORMAT;
|
|
3575
3775
|
translationCell.value = sheetRow.translation === "" ? null : sheetRow.translation;
|
|
3576
3776
|
row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
|
|
3577
|
-
|
|
3777
|
+
row.getCell(COLUMN.context).value = sheetRow.context;
|
|
3778
|
+
for (let column = COLUMN.key; column <= COLUMN.context; column += 1) {
|
|
3578
3779
|
const cell = row.getCell(column);
|
|
3579
3780
|
cell.protection = { locked: column !== COLUMN.translation };
|
|
3580
3781
|
if (column !== COLUMN.translation) {
|
|
@@ -3599,6 +3800,26 @@ function assertValidWorksheetName(locale) {
|
|
|
3599
3800
|
);
|
|
3600
3801
|
}
|
|
3601
3802
|
}
|
|
3803
|
+
function assertNoWorksheetNameCollisions(sheets) {
|
|
3804
|
+
const reservedKey = INSTRUCTIONS_SHEET_NAME.toLowerCase();
|
|
3805
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3806
|
+
for (const sheet of sheets) {
|
|
3807
|
+
const key = sheet.locale.toLowerCase();
|
|
3808
|
+
if (key === reservedKey) {
|
|
3809
|
+
throw new ExchangeError(
|
|
3810
|
+
"WORKBOOK_INVALID",
|
|
3811
|
+
`The locale "${sheet.locale}" cannot be an Excel worksheet name: it collides with the reserved "${INSTRUCTIONS_SHEET_NAME}" sheet.`
|
|
3812
|
+
);
|
|
3813
|
+
}
|
|
3814
|
+
if (seen.has(key)) {
|
|
3815
|
+
throw new ExchangeError(
|
|
3816
|
+
"WORKBOOK_INVALID",
|
|
3817
|
+
`The locale "${sheet.locale}" cannot be an Excel worksheet name: it collides with another target locale's sheet name.`
|
|
3818
|
+
);
|
|
3819
|
+
}
|
|
3820
|
+
seen.add(key);
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3602
3823
|
async function buildDataSheet(workbook, sheet) {
|
|
3603
3824
|
assertValidWorksheetName(sheet.locale);
|
|
3604
3825
|
const worksheet = workbook.addWorksheet(sheet.locale);
|
|
@@ -3625,6 +3846,7 @@ function buildInstructionsSheet(workbook) {
|
|
|
3625
3846
|
sheet.getRow(1).font = { bold: true };
|
|
3626
3847
|
}
|
|
3627
3848
|
async function buildWorkbook(model) {
|
|
3849
|
+
assertNoWorksheetNameCollisions(model.sheets);
|
|
3628
3850
|
const workbook = new ExcelJS__default.default.Workbook();
|
|
3629
3851
|
buildInstructionsSheet(workbook);
|
|
3630
3852
|
for (const sheet of model.sheets) {
|
|
@@ -3725,9 +3947,10 @@ var rowSchema = zod.z.object({
|
|
|
3725
3947
|
key: zod.z.string().min(1),
|
|
3726
3948
|
source: zod.z.string(),
|
|
3727
3949
|
currentTarget: zod.z.string(),
|
|
3728
|
-
status: zod.z.enum(["new", "changed"]),
|
|
3950
|
+
status: zod.z.enum(["new", "changed", "unchanged"]),
|
|
3729
3951
|
sourceHash: zod.z.string(),
|
|
3730
|
-
translation: zod.z.string()
|
|
3952
|
+
translation: zod.z.string(),
|
|
3953
|
+
context: zod.z.string()
|
|
3731
3954
|
});
|
|
3732
3955
|
function cellString(cell) {
|
|
3733
3956
|
const value = cell.value;
|
|
@@ -3760,7 +3983,10 @@ function parseRow(sheet, row) {
|
|
|
3760
3983
|
currentTarget: cellString(row.getCell(COLUMN.current)),
|
|
3761
3984
|
status: cellString(row.getCell(COLUMN.status)),
|
|
3762
3985
|
sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
|
|
3763
|
-
translation: cellString(row.getCell(COLUMN.translation))
|
|
3986
|
+
translation: cellString(row.getCell(COLUMN.translation)),
|
|
3987
|
+
// A workbook built before the Context column existed has no cell here; getCell auto-vivifies an
|
|
3988
|
+
// empty one, so cellString yields "" and the row still validates, keeping import backward-compatible.
|
|
3989
|
+
context: cellString(row.getCell(COLUMN.context))
|
|
3764
3990
|
};
|
|
3765
3991
|
const result = rowSchema.safeParse(candidate);
|
|
3766
3992
|
if (!result.success) {
|
|
@@ -3849,14 +4075,15 @@ function buildRows(source, target, baseline, includeUnchanged) {
|
|
|
3849
4075
|
currentTarget: target.entries.get(key)?.value ?? "",
|
|
3850
4076
|
status,
|
|
3851
4077
|
sourceHash: contentHash(sourceEntry),
|
|
3852
|
-
translation: ""
|
|
4078
|
+
translation: "",
|
|
4079
|
+
context: sourceEntry.description ?? ""
|
|
3853
4080
|
});
|
|
3854
4081
|
}
|
|
3855
4082
|
};
|
|
3856
4083
|
add(diff2.missing, "new");
|
|
3857
4084
|
add(diff2.changed, "changed");
|
|
3858
4085
|
if (includeUnchanged) {
|
|
3859
|
-
add(diff2.unchanged, "
|
|
4086
|
+
add(diff2.unchanged, "unchanged");
|
|
3860
4087
|
}
|
|
3861
4088
|
return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
|
|
3862
4089
|
}
|