@verbatra/sdk 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/config-schema.json +30 -1
- package/dist/index.cjs +3051 -349
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +252 -45
- package/dist/index.d.ts +252 -45
- package/dist/index.js +3053 -353
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/dist/index.cjs
CHANGED
|
@@ -5,16 +5,17 @@ var zod = require('zod');
|
|
|
5
5
|
var crypto = require('crypto');
|
|
6
6
|
var promises = require('fs/promises');
|
|
7
7
|
var fs = require('fs');
|
|
8
|
+
var os = require('os');
|
|
8
9
|
var cosmiconfig = require('cosmiconfig');
|
|
9
10
|
var cosmiconfigTypescriptLoader = require('cosmiconfig-typescript-loader');
|
|
11
|
+
var module$1 = require('module');
|
|
10
12
|
var Anthropic = require('@anthropic-ai/sdk');
|
|
11
13
|
var deepl = require('deepl-node');
|
|
12
|
-
var module$1 = require('module');
|
|
13
14
|
var log = require('loglevel');
|
|
14
15
|
var genai = require('@google/genai');
|
|
15
16
|
var OpenAI = require('openai');
|
|
16
|
-
var icuMessageformatParser = require('@formatjs/icu-messageformat-parser');
|
|
17
17
|
var xmldom = require('@xmldom/xmldom');
|
|
18
|
+
var icuMessageformatParser = require('@formatjs/icu-messageformat-parser');
|
|
18
19
|
var yaml = require('yaml');
|
|
19
20
|
var ExcelJS = require('exceljs');
|
|
20
21
|
var JSZip = require('jszip');
|
|
@@ -297,9 +298,20 @@ var defaultFs = {
|
|
|
297
298
|
await promises.mkdir(path, { recursive: true });
|
|
298
299
|
}
|
|
299
300
|
};
|
|
301
|
+
|
|
302
|
+
// src/locale-path/shared-catalogue-format.ts
|
|
303
|
+
var SHARED_CATALOGUE_FORMATS = /* @__PURE__ */ new Set([
|
|
304
|
+
"apple-xcstrings"
|
|
305
|
+
]);
|
|
306
|
+
function isSharedCatalogueFormat(format) {
|
|
307
|
+
return SHARED_CATALOGUE_FORMATS.has(format);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/lock/locale-write-lock.ts
|
|
300
311
|
var LOCAL_DIR_NAME = ".verbatra-local";
|
|
301
312
|
var LOCK_FILE_GUARD_STEM = "_lockfile";
|
|
302
313
|
var GLOSSARY_GUARD_STEM = "_glossary";
|
|
314
|
+
var SHARED_CATALOGUE_STEM = "_catalogue";
|
|
303
315
|
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
304
316
|
var DEFAULT_ACQUIRE_TIMEOUT_MS = 10 * 6e4;
|
|
305
317
|
var WAIT_NOTICE_INTERVAL_MS = 1e3;
|
|
@@ -310,6 +322,9 @@ function lockPath(cwd, stem) {
|
|
|
310
322
|
function localeLockPath(cwd, locale) {
|
|
311
323
|
return lockPath(cwd, locale);
|
|
312
324
|
}
|
|
325
|
+
function writeLockKeyFor(format, locale) {
|
|
326
|
+
return isSharedCatalogueFormat(format) ? SHARED_CATALOGUE_STEM : locale;
|
|
327
|
+
}
|
|
313
328
|
function lockFileGuardPath(cwd) {
|
|
314
329
|
return lockPath(cwd, LOCK_FILE_GUARD_STEM);
|
|
315
330
|
}
|
|
@@ -530,6 +545,67 @@ async function updateGlossaryTerm(input, deps = {}) {
|
|
|
530
545
|
return entries;
|
|
531
546
|
});
|
|
532
547
|
}
|
|
548
|
+
var SELF_ALIASED_PACKAGES = ["@verbatra/sdk", "@verbatra/cli"];
|
|
549
|
+
var ENTRY_CONDITION_ORDER = ["import", "node", "default", "require"];
|
|
550
|
+
function resolvePathsFromRunningModule(packageName) {
|
|
551
|
+
return module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))).resolve.paths(packageName) ?? [];
|
|
552
|
+
}
|
|
553
|
+
function readPackageManifestFromDisk(packageRoot) {
|
|
554
|
+
try {
|
|
555
|
+
return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
556
|
+
} catch {
|
|
557
|
+
return void 0;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
var defaultModuleAliasDeps = {
|
|
561
|
+
resolvePaths: resolvePathsFromRunningModule,
|
|
562
|
+
readPackageManifest: readPackageManifestFromDisk
|
|
563
|
+
};
|
|
564
|
+
function resolveRootExportPath(manifest) {
|
|
565
|
+
const exportsField = manifest.exports;
|
|
566
|
+
if (typeof exportsField === "string") {
|
|
567
|
+
return exportsField;
|
|
568
|
+
}
|
|
569
|
+
if (typeof exportsField === "object" && exportsField !== null) {
|
|
570
|
+
const rootExport = exportsField["."];
|
|
571
|
+
if (typeof rootExport === "string") {
|
|
572
|
+
return rootExport;
|
|
573
|
+
}
|
|
574
|
+
if (typeof rootExport === "object" && rootExport !== null) {
|
|
575
|
+
for (const condition of ENTRY_CONDITION_ORDER) {
|
|
576
|
+
const candidate = rootExport[condition];
|
|
577
|
+
if (typeof candidate === "string") {
|
|
578
|
+
return candidate;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return manifest.main;
|
|
584
|
+
}
|
|
585
|
+
function findPackageEntry(packageName, deps) {
|
|
586
|
+
for (const candidateDir of deps.resolvePaths(packageName)) {
|
|
587
|
+
const packageRoot = path.join(candidateDir, packageName);
|
|
588
|
+
const manifest = deps.readPackageManifest(packageRoot);
|
|
589
|
+
if (manifest === void 0) {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const relativeEntry = resolveRootExportPath(manifest);
|
|
593
|
+
if (relativeEntry !== void 0) {
|
|
594
|
+
return path.join(packageRoot, relativeEntry);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return void 0;
|
|
598
|
+
}
|
|
599
|
+
function resolveSelfPackageAliases(deps = defaultModuleAliasDeps) {
|
|
600
|
+
const aliases = {};
|
|
601
|
+
for (const packageName of SELF_ALIASED_PACKAGES) {
|
|
602
|
+
const entry = findPackageEntry(packageName, deps);
|
|
603
|
+
if (entry !== void 0) {
|
|
604
|
+
aliases[packageName] = entry;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return aliases;
|
|
608
|
+
}
|
|
533
609
|
async function resolveGlossary(glossary, baseDir, fs) {
|
|
534
610
|
if (glossary === void 0) {
|
|
535
611
|
return { glossary: void 0, provenance: { source: "none" } };
|
|
@@ -611,7 +687,11 @@ var SUPPORTED_FORMATS = [
|
|
|
611
687
|
"xliff",
|
|
612
688
|
"yaml",
|
|
613
689
|
"arb",
|
|
614
|
-
"properties"
|
|
690
|
+
"properties",
|
|
691
|
+
"apple-strings",
|
|
692
|
+
"apple-xcstrings",
|
|
693
|
+
"android-xml",
|
|
694
|
+
"gettext-po"
|
|
615
695
|
];
|
|
616
696
|
var supportedFormatSchema = zod.z.enum(SUPPORTED_FORMATS);
|
|
617
697
|
var translationEntrySchema = zod.z.object({
|
|
@@ -1312,7 +1392,8 @@ var PROVIDER_ENV = {
|
|
|
1312
1392
|
anthropic: "ANTHROPIC_API_KEY",
|
|
1313
1393
|
openai: "OPENAI_API_KEY",
|
|
1314
1394
|
gemini: "GEMINI_API_KEY",
|
|
1315
|
-
deepl: "DEEPL_API_KEY"
|
|
1395
|
+
deepl: "DEEPL_API_KEY",
|
|
1396
|
+
"google-translate": "GOOGLE_TRANSLATE_API_KEY"
|
|
1316
1397
|
};
|
|
1317
1398
|
function readRequiredEnv(name) {
|
|
1318
1399
|
const value = process.env[name];
|
|
@@ -1333,6 +1414,9 @@ function requireGeminiKey() {
|
|
|
1333
1414
|
function requireDeepLKey() {
|
|
1334
1415
|
return readRequiredEnv(PROVIDER_ENV.deepl);
|
|
1335
1416
|
}
|
|
1417
|
+
function requireGoogleTranslateKey() {
|
|
1418
|
+
return readRequiredEnv(PROVIDER_ENV["google-translate"]);
|
|
1419
|
+
}
|
|
1336
1420
|
var OPENAI_COMPATIBLE_ENV_VAR = "OPENAI_COMPATIBLE_API_KEY";
|
|
1337
1421
|
var OPENAI_COMPATIBLE_KEY_PLACEHOLDER = "local";
|
|
1338
1422
|
function resolveOpenAiCompatibleKey(customEnvVar) {
|
|
@@ -1873,11 +1957,287 @@ function createMechanism2(client, config) {
|
|
|
1873
1957
|
}
|
|
1874
1958
|
};
|
|
1875
1959
|
}
|
|
1960
|
+
var googleTranslateConfigSchema = zod.z.object({}).extend(requestTimeoutConfigSchema.shape);
|
|
1961
|
+
var GOOGLE_TRANSLATE_ENDPOINT = "https://translation.googleapis.com/language/translate/v2";
|
|
1962
|
+
var GOOGLE_TRANSLATE_ENDPOINT_HOST = "translation.googleapis.com";
|
|
1963
|
+
async function parseJsonBody(response) {
|
|
1964
|
+
try {
|
|
1965
|
+
return await response.json();
|
|
1966
|
+
} catch {
|
|
1967
|
+
return void 0;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
function createDefaultClient4() {
|
|
1971
|
+
const apiKey = requireGoogleTranslateKey();
|
|
1972
|
+
const client = {
|
|
1973
|
+
translate: async (texts, sourceLang, targetLang, signal) => {
|
|
1974
|
+
const response = await fetch(
|
|
1975
|
+
`${GOOGLE_TRANSLATE_ENDPOINT}?key=${encodeURIComponent(apiKey)}`,
|
|
1976
|
+
{
|
|
1977
|
+
method: "POST",
|
|
1978
|
+
headers: { "content-type": "application/json" },
|
|
1979
|
+
body: JSON.stringify({
|
|
1980
|
+
q: texts,
|
|
1981
|
+
source: sourceLang,
|
|
1982
|
+
target: targetLang,
|
|
1983
|
+
format: "text"
|
|
1984
|
+
}),
|
|
1985
|
+
signal
|
|
1986
|
+
}
|
|
1987
|
+
);
|
|
1988
|
+
return { status: response.status, body: await parseJsonBody(response) };
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
return { client };
|
|
1992
|
+
}
|
|
1993
|
+
var GOOGLE_TRANSLATE_MAX_REQUEST_BYTES = 100 * 1024;
|
|
1994
|
+
var GOOGLE_TRANSLATE_PAYLOAD_OVERHEAD_RESERVE_BYTES = 2 * 1024;
|
|
1995
|
+
var GOOGLE_TRANSLATE_MAX_TEXT_PAYLOAD_BYTES = GOOGLE_TRANSLATE_MAX_REQUEST_BYTES - GOOGLE_TRANSLATE_PAYLOAD_OVERHEAD_RESERVE_BYTES;
|
|
1996
|
+
var textEncoder = new TextEncoder();
|
|
1997
|
+
function estimateJsonBytes(text) {
|
|
1998
|
+
return textEncoder.encode(JSON.stringify(text)).length;
|
|
1999
|
+
}
|
|
2000
|
+
function chunkTextsForGoogleTranslate(texts) {
|
|
2001
|
+
const chunks = [];
|
|
2002
|
+
let current = [];
|
|
2003
|
+
let currentBytes = 0;
|
|
2004
|
+
for (const text of texts) {
|
|
2005
|
+
const textBytes = estimateJsonBytes(text);
|
|
2006
|
+
const startsNewChunk = current.length > 0 && currentBytes + textBytes > GOOGLE_TRANSLATE_MAX_TEXT_PAYLOAD_BYTES;
|
|
2007
|
+
if (startsNewChunk) {
|
|
2008
|
+
chunks.push(current);
|
|
2009
|
+
current = [];
|
|
2010
|
+
currentBytes = 0;
|
|
2011
|
+
}
|
|
2012
|
+
current.push(text);
|
|
2013
|
+
currentBytes += textBytes;
|
|
2014
|
+
}
|
|
2015
|
+
if (current.length > 0) {
|
|
2016
|
+
chunks.push(current);
|
|
2017
|
+
}
|
|
2018
|
+
return chunks;
|
|
2019
|
+
}
|
|
2020
|
+
var BCP47_LIKE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
2021
|
+
function assertValidGoogleTranslateLocale(locale, role) {
|
|
2022
|
+
if (!BCP47_LIKE.test(locale)) {
|
|
2023
|
+
throw new ProviderError(
|
|
2024
|
+
"INVALID_REQUEST",
|
|
2025
|
+
`Google Cloud Translation does not accept "${locale}" as a ${role} locale code. Use a well-formed BCP-47 language code (for example, "en" or "pt-BR").`
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
var PLACEHOLDER_UNSUPPORTED_MESSAGE2 = "Some entries contain placeholders or ICU syntax that Google Cloud Translation cannot preserve; they were left untranslated. Use an LLM provider to translate placeholder-bearing strings.";
|
|
2030
|
+
function partitionByPlaceholders2(entries) {
|
|
2031
|
+
const protectable = [];
|
|
2032
|
+
const unprotectable = [];
|
|
2033
|
+
for (const entry of entries) {
|
|
2034
|
+
if (entry.placeholders.length > 0) {
|
|
2035
|
+
unprotectable.push(entry);
|
|
2036
|
+
} else {
|
|
2037
|
+
protectable.push(entry);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
return { protectable, unprotectable };
|
|
2041
|
+
}
|
|
2042
|
+
var FORMALITY_DOWNGRADED_MESSAGE2 = "Formality was not applied: Cloud Translation Basic (v2) has no formality control.";
|
|
2043
|
+
var GLOSSARY_IGNORED_MESSAGE2 = "The supplied glossary term map was not applied: Cloud Translation Basic (v2) does not support a glossary.";
|
|
2044
|
+
function buildTranslateNotices(input) {
|
|
2045
|
+
const notices = [];
|
|
2046
|
+
if (input.tone === "formal" || input.tone === "informal") {
|
|
2047
|
+
notices.push({ code: "FORMALITY_DOWNGRADED", message: FORMALITY_DOWNGRADED_MESSAGE2 });
|
|
2048
|
+
}
|
|
2049
|
+
if (input.genericGlossarySupplied) {
|
|
2050
|
+
notices.push({ code: "GLOSSARY_IGNORED", message: GLOSSARY_IGNORED_MESSAGE2 });
|
|
2051
|
+
}
|
|
2052
|
+
return notices;
|
|
2053
|
+
}
|
|
2054
|
+
var successSchema = zod.z.object({
|
|
2055
|
+
data: zod.z.object({
|
|
2056
|
+
translations: zod.z.array(zod.z.object({ translatedText: zod.z.string() }))
|
|
2057
|
+
})
|
|
2058
|
+
});
|
|
2059
|
+
var errorDetailSchema = zod.z.object({
|
|
2060
|
+
reason: zod.z.string().optional()
|
|
2061
|
+
});
|
|
2062
|
+
var errorSchema = zod.z.object({
|
|
2063
|
+
error: zod.z.object({
|
|
2064
|
+
errors: zod.z.array(errorDetailSchema).optional()
|
|
2065
|
+
})
|
|
2066
|
+
});
|
|
2067
|
+
var QUOTA_REASONS = /* @__PURE__ */ new Set([
|
|
2068
|
+
"dailyLimitExceeded",
|
|
2069
|
+
"dailyLimitExceededUnreg",
|
|
2070
|
+
"userRateLimitExceeded",
|
|
2071
|
+
"rateLimitExceeded",
|
|
2072
|
+
"quotaExceeded"
|
|
2073
|
+
]);
|
|
2074
|
+
var AUTH_FAILED_STATUSES2 = /* @__PURE__ */ new Set([401, 403]);
|
|
2075
|
+
var SERVER_OUTAGE_MIN_STATUS2 = 500;
|
|
2076
|
+
var SERVER_OUTAGE_MAX_STATUS2 = 599;
|
|
2077
|
+
var RATE_LIMITED_STATUS2 = 429;
|
|
2078
|
+
var INVALID_REQUEST_STATUS = 400;
|
|
2079
|
+
var MALFORMED_RESPONSE_MESSAGE = "Google Cloud Translation returned a response verbatra could not parse.";
|
|
2080
|
+
var MISMATCH_MESSAGE2 = "The provider returned a mismatched number of translations.";
|
|
2081
|
+
var AUTH_FAILED_MESSAGE2 = "Google Cloud Translation rejected the request credentials. Check that GOOGLE_TRANSLATE_API_KEY is a valid, unrestricted API key for a Google Cloud project with the Cloud Translation API enabled.";
|
|
2082
|
+
var RATE_LIMITED_MESSAGE2 = "Google Cloud Translation rate-limited or quota-capped this request. Wait for the quota to reset, or raise your project's Cloud Translation quota, then retry.";
|
|
2083
|
+
var INVALID_REQUEST_MESSAGE = "Google Cloud Translation rejected the request as malformed or unsupported: check the source and target locale codes and retry.";
|
|
2084
|
+
var PROVIDER_UNAVAILABLE_MESSAGE2 = "Google Cloud Translation is currently unavailable.";
|
|
2085
|
+
var PROVIDER_ERROR_MESSAGE = "The translation provider request failed.";
|
|
2086
|
+
function extractReasons(body) {
|
|
2087
|
+
const parsed = errorSchema.safeParse(body);
|
|
2088
|
+
if (!parsed.success) {
|
|
2089
|
+
return [];
|
|
2090
|
+
}
|
|
2091
|
+
return (parsed.data.error.errors ?? []).map((detail) => detail.reason).filter((reason) => reason !== void 0);
|
|
2092
|
+
}
|
|
2093
|
+
function classifyErrorStatus(status, reasons) {
|
|
2094
|
+
if (status === RATE_LIMITED_STATUS2) {
|
|
2095
|
+
return { code: "RATE_LIMITED", message: RATE_LIMITED_MESSAGE2 };
|
|
2096
|
+
}
|
|
2097
|
+
if (status === INVALID_REQUEST_STATUS) {
|
|
2098
|
+
return { code: "INVALID_REQUEST", message: INVALID_REQUEST_MESSAGE };
|
|
2099
|
+
}
|
|
2100
|
+
if (AUTH_FAILED_STATUSES2.has(status)) {
|
|
2101
|
+
if (reasons.some((reason) => QUOTA_REASONS.has(reason))) {
|
|
2102
|
+
return { code: "RATE_LIMITED", message: RATE_LIMITED_MESSAGE2 };
|
|
2103
|
+
}
|
|
2104
|
+
return { code: "AUTH_FAILED", message: AUTH_FAILED_MESSAGE2 };
|
|
2105
|
+
}
|
|
2106
|
+
if (status >= SERVER_OUTAGE_MIN_STATUS2 && status <= SERVER_OUTAGE_MAX_STATUS2) {
|
|
2107
|
+
return { code: "PROVIDER_UNAVAILABLE", message: PROVIDER_UNAVAILABLE_MESSAGE2 };
|
|
2108
|
+
}
|
|
2109
|
+
return { code: "PROVIDER_ERROR", message: PROVIDER_ERROR_MESSAGE };
|
|
2110
|
+
}
|
|
2111
|
+
function isSuccessStatus(status) {
|
|
2112
|
+
return status >= 200 && status < 300;
|
|
2113
|
+
}
|
|
2114
|
+
function parseGoogleTranslateHttpResult(status, body) {
|
|
2115
|
+
if (isSuccessStatus(status)) {
|
|
2116
|
+
const parsed = successSchema.safeParse(body);
|
|
2117
|
+
if (!parsed.success) {
|
|
2118
|
+
throw new ProviderError("INVALID_RESPONSE", MALFORMED_RESPONSE_MESSAGE);
|
|
2119
|
+
}
|
|
2120
|
+
return parsed.data.data.translations.map((translation) => translation.translatedText);
|
|
2121
|
+
}
|
|
2122
|
+
const { code, message } = classifyErrorStatus(status, extractReasons(body));
|
|
2123
|
+
throw new ProviderError(code, message);
|
|
2124
|
+
}
|
|
2125
|
+
function zipResults2(entries, translatedTexts) {
|
|
2126
|
+
const values = /* @__PURE__ */ new Map();
|
|
2127
|
+
const integrityInputs = [];
|
|
2128
|
+
const resultIter = translatedTexts[Symbol.iterator]();
|
|
2129
|
+
for (const entry of entries) {
|
|
2130
|
+
const next = resultIter.next();
|
|
2131
|
+
if (next.done === true) {
|
|
2132
|
+
throw new ProviderError("INVALID_RESPONSE", MISMATCH_MESSAGE2);
|
|
2133
|
+
}
|
|
2134
|
+
const translatedValue = next.value;
|
|
2135
|
+
values.set(entry.key, translatedValue);
|
|
2136
|
+
integrityInputs.push({ key: entry.key, sourceValue: entry.value, translatedValue });
|
|
2137
|
+
}
|
|
2138
|
+
if (resultIter.next().done === false) {
|
|
2139
|
+
throw new ProviderError("INVALID_RESPONSE", MISMATCH_MESSAGE2);
|
|
2140
|
+
}
|
|
2141
|
+
return { values, integrityInputs };
|
|
2142
|
+
}
|
|
2143
|
+
var PROVIDER_ID4 = "google-translate";
|
|
2144
|
+
function createGoogleTranslateProvider(config, deps = {}) {
|
|
2145
|
+
const validConfig = googleTranslateConfigSchema.parse(config);
|
|
2146
|
+
const bundle = resolveClient2(deps);
|
|
2147
|
+
return {
|
|
2148
|
+
id: PROVIDER_ID4,
|
|
2149
|
+
kind: "machine-translation",
|
|
2150
|
+
supportsGlossary: false,
|
|
2151
|
+
translateBatch: (request) => translate2(bundle, validConfig, request)
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
function resolveClient2(deps) {
|
|
2155
|
+
if (deps.client !== void 0) {
|
|
2156
|
+
return { client: deps.client };
|
|
2157
|
+
}
|
|
2158
|
+
return createDefaultClient4();
|
|
2159
|
+
}
|
|
2160
|
+
async function translate2(bundle, config, request) {
|
|
2161
|
+
const data = validateRequest(request);
|
|
2162
|
+
assertValidGoogleTranslateLocale(data.sourceLocale, "source");
|
|
2163
|
+
assertValidGoogleTranslateLocale(data.targetLocale, "target");
|
|
2164
|
+
const { protectable, unprotectable } = partitionByPlaceholders2(data.entries);
|
|
2165
|
+
const genericGlossarySupplied = request.glossary !== void 0 && Object.keys(request.glossary).length > 0;
|
|
2166
|
+
const notices = buildTranslateNotices({
|
|
2167
|
+
...data.tone !== void 0 ? { tone: data.tone } : {},
|
|
2168
|
+
genericGlossarySupplied
|
|
2169
|
+
});
|
|
2170
|
+
const { values, integrity } = await translateProtectable2(
|
|
2171
|
+
bundle.client,
|
|
2172
|
+
data,
|
|
2173
|
+
protectable,
|
|
2174
|
+
request.extractPlaceholders,
|
|
2175
|
+
request.comparePlaceholders,
|
|
2176
|
+
config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
2177
|
+
request.signal
|
|
2178
|
+
);
|
|
2179
|
+
if (unprotectable.length > 0) {
|
|
2180
|
+
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE2 });
|
|
2181
|
+
}
|
|
2182
|
+
const reviewFlags = applyProviderDegraded(
|
|
2183
|
+
buildEntryReviewFlags(
|
|
2184
|
+
protectable,
|
|
2185
|
+
values,
|
|
2186
|
+
integrity,
|
|
2187
|
+
data.sourceLocale,
|
|
2188
|
+
data.targetLocale,
|
|
2189
|
+
request.glossary
|
|
2190
|
+
),
|
|
2191
|
+
notices,
|
|
2192
|
+
[...values.keys()]
|
|
2193
|
+
);
|
|
2194
|
+
return { values, integrity, notices, reviewFlags };
|
|
2195
|
+
}
|
|
2196
|
+
async function translateProtectable2(client, data, protectable, extract, compare, timeoutMs, signal) {
|
|
2197
|
+
if (protectable.length === 0) {
|
|
2198
|
+
return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
|
|
2199
|
+
}
|
|
2200
|
+
const texts = protectable.map((entry) => entry.value);
|
|
2201
|
+
const translatedTexts = await callClientChunked2(
|
|
2202
|
+
client,
|
|
2203
|
+
texts,
|
|
2204
|
+
data.sourceLocale,
|
|
2205
|
+
data.targetLocale,
|
|
2206
|
+
timeoutMs,
|
|
2207
|
+
signal
|
|
2208
|
+
);
|
|
2209
|
+
const { values, integrityInputs } = zipResults2(protectable, translatedTexts);
|
|
2210
|
+
const integrity = checkBatchIntegrity(integrityInputs, extract, compare);
|
|
2211
|
+
return { values, integrity };
|
|
2212
|
+
}
|
|
2213
|
+
function callClient3(client, texts, sourceLang, targetLang, timeoutMs, signal) {
|
|
2214
|
+
return withRequestTimeout(
|
|
2215
|
+
timeoutMs,
|
|
2216
|
+
signal,
|
|
2217
|
+
(guardedSignal) => client.translate(texts, sourceLang, targetLang, guardedSignal),
|
|
2218
|
+
{ endpointHost: GOOGLE_TRANSLATE_ENDPOINT_HOST }
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
async function callClientChunked2(client, texts, sourceLang, targetLang, timeoutMs, signal) {
|
|
2222
|
+
const results = [];
|
|
2223
|
+
for (const chunk2 of chunkTextsForGoogleTranslate(texts)) {
|
|
2224
|
+
const { status, body } = await callClient3(
|
|
2225
|
+
client,
|
|
2226
|
+
chunk2,
|
|
2227
|
+
sourceLang,
|
|
2228
|
+
targetLang,
|
|
2229
|
+
timeoutMs,
|
|
2230
|
+
signal
|
|
2231
|
+
);
|
|
2232
|
+
results.push(...parseGoogleTranslateHttpResult(status, body));
|
|
2233
|
+
}
|
|
2234
|
+
return results;
|
|
2235
|
+
}
|
|
1876
2236
|
var openAiConfigSchema = zod.z.object({
|
|
1877
2237
|
model: zod.z.string().min(1),
|
|
1878
2238
|
maxOutputTokens: zod.z.number().int().positive()
|
|
1879
2239
|
}).extend(requestTimeoutConfigSchema.shape);
|
|
1880
|
-
function
|
|
2240
|
+
function createDefaultClient5() {
|
|
1881
2241
|
const sdk = new OpenAI__default.default({ apiKey: requireOpenAiKey(), logLevel: "off" });
|
|
1882
2242
|
return {
|
|
1883
2243
|
chat: {
|
|
@@ -2000,13 +2360,13 @@ function extractOpenAiResult(completion, tolerant = false) {
|
|
|
2000
2360
|
const usage = toUsage(completion.usage?.prompt_tokens, completion.usage?.completion_tokens);
|
|
2001
2361
|
return usage === void 0 ? { raw } : { raw, usage };
|
|
2002
2362
|
}
|
|
2003
|
-
var
|
|
2363
|
+
var PROVIDER_ID5 = "openai";
|
|
2004
2364
|
function createOpenAiProvider(config, deps = {}) {
|
|
2005
2365
|
const validConfig = openAiConfigSchema.parse(config);
|
|
2006
|
-
const client = deps.client ??
|
|
2366
|
+
const client = deps.client ?? createDefaultClient5();
|
|
2007
2367
|
const mechanism = createMechanism3(client, validConfig);
|
|
2008
2368
|
return {
|
|
2009
|
-
id:
|
|
2369
|
+
id: PROVIDER_ID5,
|
|
2010
2370
|
kind: "llm",
|
|
2011
2371
|
supportsGlossary: true,
|
|
2012
2372
|
translateBatch: (request) => runLlmTranslation(request, mechanism)
|
|
@@ -2017,12 +2377,12 @@ function createMechanism3(client, config) {
|
|
|
2017
2377
|
return {
|
|
2018
2378
|
translate: async ({ payloadJson, signal }) => {
|
|
2019
2379
|
const body = buildOpenAiRequest(config, payloadJson);
|
|
2020
|
-
const completion = await
|
|
2380
|
+
const completion = await callClient4(client, body, timeoutMs, signal);
|
|
2021
2381
|
return extractOpenAiResult(completion);
|
|
2022
2382
|
}
|
|
2023
2383
|
};
|
|
2024
2384
|
}
|
|
2025
|
-
function
|
|
2385
|
+
function callClient4(client, body, timeoutMs, signal) {
|
|
2026
2386
|
return withRequestTimeout(
|
|
2027
2387
|
timeoutMs,
|
|
2028
2388
|
signal,
|
|
@@ -2051,7 +2411,7 @@ function endpointContextOf(baseUrl) {
|
|
|
2051
2411
|
return void 0;
|
|
2052
2412
|
}
|
|
2053
2413
|
}
|
|
2054
|
-
function
|
|
2414
|
+
function createDefaultClient6(config) {
|
|
2055
2415
|
const sdk = new OpenAI__default.default({
|
|
2056
2416
|
apiKey: resolveOpenAiCompatibleKey(config.apiKeyEnvVar),
|
|
2057
2417
|
baseURL: config.baseUrl,
|
|
@@ -2068,13 +2428,13 @@ function createDefaultClient5(config) {
|
|
|
2068
2428
|
}
|
|
2069
2429
|
};
|
|
2070
2430
|
}
|
|
2071
|
-
var
|
|
2431
|
+
var PROVIDER_ID6 = "openai-compatible";
|
|
2072
2432
|
function createOpenAiCompatibleProvider(config, deps = {}) {
|
|
2073
2433
|
const validConfig = openAiCompatibleConfigSchema.parse(config);
|
|
2074
|
-
const client = deps.client ??
|
|
2434
|
+
const client = deps.client ?? createDefaultClient6(validConfig);
|
|
2075
2435
|
const mechanism = createMechanism4(client, validConfig);
|
|
2076
2436
|
return {
|
|
2077
|
-
id:
|
|
2437
|
+
id: PROVIDER_ID6,
|
|
2078
2438
|
kind: "llm",
|
|
2079
2439
|
supportsGlossary: true,
|
|
2080
2440
|
translateBatch: (request) => runLlmTranslation(request, mechanism)
|
|
@@ -2086,12 +2446,12 @@ function createMechanism4(client, config) {
|
|
|
2086
2446
|
return {
|
|
2087
2447
|
translate: async ({ payloadJson, signal }) => {
|
|
2088
2448
|
const body = buildOpenAiRequest(config, payloadJson, "strict-schema", "max_tokens");
|
|
2089
|
-
const completion = await
|
|
2449
|
+
const completion = await callClient5(client, body, timeoutMs, signal, endpoint);
|
|
2090
2450
|
return extractOpenAiResult(completion, true);
|
|
2091
2451
|
}
|
|
2092
2452
|
};
|
|
2093
2453
|
}
|
|
2094
|
-
function
|
|
2454
|
+
function callClient5(client, body, timeoutMs, signal, endpoint) {
|
|
2095
2455
|
return withRequestTimeout(
|
|
2096
2456
|
timeoutMs,
|
|
2097
2457
|
signal,
|
|
@@ -2114,6 +2474,10 @@ var providerConfigSchema = zod.z.discriminatedUnion("id", [
|
|
|
2114
2474
|
zod.z.object({ id: zod.z.literal("openai"), options: openAiConfigSchema.strict() }),
|
|
2115
2475
|
zod.z.object({ id: zod.z.literal("gemini"), options: geminiConfigSchema.strict() }),
|
|
2116
2476
|
zod.z.object({ id: zod.z.literal("deepl"), options: deepLConfigSchema.strict() }),
|
|
2477
|
+
zod.z.object({
|
|
2478
|
+
id: zod.z.literal("google-translate"),
|
|
2479
|
+
options: googleTranslateConfigSchema.strict()
|
|
2480
|
+
}),
|
|
2117
2481
|
zod.z.object({
|
|
2118
2482
|
id: zod.z.literal("openai-compatible"),
|
|
2119
2483
|
options: openAiCompatibleConfigSchema.strict()
|
|
@@ -2124,6 +2488,7 @@ var providerFactories = {
|
|
|
2124
2488
|
openai: (options) => createOpenAiProvider(options),
|
|
2125
2489
|
gemini: (options) => createGeminiProvider(options),
|
|
2126
2490
|
deepl: (options) => createDeepLProvider(options),
|
|
2491
|
+
"google-translate": (options) => createGoogleTranslateProvider(options),
|
|
2127
2492
|
"openai-compatible": (options) => createOpenAiCompatibleProvider(options)
|
|
2128
2493
|
};
|
|
2129
2494
|
var PROVIDER_IDS = Object.keys(providerFactories);
|
|
@@ -2166,10 +2531,16 @@ var verbatraConfigSchema = zod.z.strictObject({
|
|
|
2166
2531
|
maxBatchSize: zod.z.number().int().positive().optional(),
|
|
2167
2532
|
maxTokens: zod.z.number().int().positive().optional(),
|
|
2168
2533
|
budgetBehavior: zod.z.enum(["warn", "stop"]).optional()
|
|
2169
|
-
}).refine(
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2534
|
+
}).refine(
|
|
2535
|
+
(config) => {
|
|
2536
|
+
const sourceKey = config.sourceLocale.toLowerCase();
|
|
2537
|
+
return !config.targetLocales.some((locale) => locale.toLowerCase() === sourceKey);
|
|
2538
|
+
},
|
|
2539
|
+
{
|
|
2540
|
+
message: "targetLocales must not include the source locale",
|
|
2541
|
+
path: ["targetLocales"]
|
|
2542
|
+
}
|
|
2543
|
+
).refine((config) => findCaseInsensitiveDuplicate(config.targetLocales) === void 0, {
|
|
2173
2544
|
error: (issue) => {
|
|
2174
2545
|
const duplicate = findCaseInsensitiveDuplicate(
|
|
2175
2546
|
issue.input.targetLocales
|
|
@@ -2194,6 +2565,50 @@ var SEARCH_PLACES = [
|
|
|
2194
2565
|
`${MODULE_NAME}.config.cjs`,
|
|
2195
2566
|
`${MODULE_NAME}.config.ts`
|
|
2196
2567
|
];
|
|
2568
|
+
function isAncestorOrSelf(ancestor, startDir) {
|
|
2569
|
+
const target = path.resolve(ancestor);
|
|
2570
|
+
let dir = path.resolve(startDir);
|
|
2571
|
+
while (true) {
|
|
2572
|
+
if (dir === target) {
|
|
2573
|
+
return true;
|
|
2574
|
+
}
|
|
2575
|
+
const parent = path.dirname(dir);
|
|
2576
|
+
if (parent === dir) {
|
|
2577
|
+
return false;
|
|
2578
|
+
}
|
|
2579
|
+
dir = parent;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
function findSearchStopDir(startDir) {
|
|
2583
|
+
let dir = path.resolve(startDir);
|
|
2584
|
+
while (true) {
|
|
2585
|
+
if (fs.existsSync(path.join(dir, ".git"))) {
|
|
2586
|
+
return dir;
|
|
2587
|
+
}
|
|
2588
|
+
const parent = path.dirname(dir);
|
|
2589
|
+
if (parent === dir) {
|
|
2590
|
+
break;
|
|
2591
|
+
}
|
|
2592
|
+
dir = parent;
|
|
2593
|
+
}
|
|
2594
|
+
const home = path.resolve(os.homedir());
|
|
2595
|
+
return isAncestorOrSelf(home, startDir) ? home : path.resolve(startDir);
|
|
2596
|
+
}
|
|
2597
|
+
function collectSearchChain(startDir, stopDir) {
|
|
2598
|
+
const stop = path.resolve(stopDir);
|
|
2599
|
+
const chain = /* @__PURE__ */ new Set();
|
|
2600
|
+
let dir = path.resolve(startDir);
|
|
2601
|
+
while (dir !== stop) {
|
|
2602
|
+
chain.add(dir);
|
|
2603
|
+
const parent = path.dirname(dir);
|
|
2604
|
+
if (parent === dir) {
|
|
2605
|
+
break;
|
|
2606
|
+
}
|
|
2607
|
+
dir = parent;
|
|
2608
|
+
}
|
|
2609
|
+
chain.add(stop);
|
|
2610
|
+
return chain;
|
|
2611
|
+
}
|
|
2197
2612
|
function formatIssues(error) {
|
|
2198
2613
|
return error.issues.map((issue) => {
|
|
2199
2614
|
const path = issue.path.join(".");
|
|
@@ -2243,20 +2658,30 @@ async function loadConfigWithMeta(options = {}) {
|
|
|
2243
2658
|
const { config: config2, glossary: glossary2 } = await finalizeConfig(parsed2, options.cwd ?? process.cwd(), fs);
|
|
2244
2659
|
return { config: config2, source: { kind: "override" }, glossary: glossary2 };
|
|
2245
2660
|
}
|
|
2661
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2662
|
+
const stopDir = findSearchStopDir(cwd);
|
|
2246
2663
|
const explorer = cosmiconfig.cosmiconfig(MODULE_NAME, {
|
|
2247
2664
|
searchPlaces: SEARCH_PLACES,
|
|
2248
|
-
loaders: { ".ts": cosmiconfigTypescriptLoader.TypeScriptLoader() }
|
|
2665
|
+
loaders: { ".ts": cosmiconfigTypescriptLoader.TypeScriptLoader({ alias: resolveSelfPackageAliases() }) },
|
|
2666
|
+
searchStrategy: "global",
|
|
2667
|
+
stopDir
|
|
2249
2668
|
});
|
|
2250
2669
|
if (options.configPath !== void 0) {
|
|
2251
2670
|
return loadExplicitWithMeta(explorer, options.configPath, options.cwd, fs);
|
|
2252
2671
|
}
|
|
2253
2672
|
let result;
|
|
2254
2673
|
try {
|
|
2255
|
-
result = await explorer.search(
|
|
2674
|
+
result = await explorer.search(cwd);
|
|
2256
2675
|
} catch (error) {
|
|
2257
2676
|
const detail = errorMessage(error);
|
|
2258
2677
|
throw new SdkError("CONFIG_INVALID", `Failed to load the verbatra configuration: ${detail}`);
|
|
2259
2678
|
}
|
|
2679
|
+
if (result !== null && result.isEmpty !== true) {
|
|
2680
|
+
const resultDir = path.dirname(path.resolve(result.filepath));
|
|
2681
|
+
if (!collectSearchChain(cwd, stopDir).has(resultDir)) {
|
|
2682
|
+
result = null;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2260
2685
|
if (result === null || result.isEmpty === true) {
|
|
2261
2686
|
throw new SdkError(
|
|
2262
2687
|
"CONFIG_NOT_FOUND",
|
|
@@ -2317,10 +2742,17 @@ function buildForwardMap(config, pathFor) {
|
|
|
2317
2742
|
}
|
|
2318
2743
|
return forward;
|
|
2319
2744
|
}
|
|
2745
|
+
function sharedCataloguePathFor(cwd, pattern) {
|
|
2746
|
+
const path$1 = path.resolve(cwd, expandPattern(pattern, ""));
|
|
2747
|
+
return () => path$1;
|
|
2748
|
+
}
|
|
2320
2749
|
function createLocalePathResolver(cwd, config) {
|
|
2321
2750
|
const style = config.files.localeStyle ?? DEFAULT_LOCALE_STYLE;
|
|
2322
2751
|
const pattern = config.files.pattern;
|
|
2323
2752
|
validatePattern(pattern, style);
|
|
2753
|
+
if (isSharedCatalogueFormat(config.format)) {
|
|
2754
|
+
return { pathFor: sharedCataloguePathFor(cwd, pattern), localeFor: () => void 0 };
|
|
2755
|
+
}
|
|
2324
2756
|
const pathFor = (locale) => path.resolve(cwd, expandPattern(pattern, safeSpelling(locale, style, config.sourceLocale)));
|
|
2325
2757
|
const forward = buildForwardMap(config, pathFor);
|
|
2326
2758
|
return {
|
|
@@ -2491,34 +2923,1313 @@ var nodeAdapterFs = {
|
|
|
2491
2923
|
readBounded: nodeReadBounded,
|
|
2492
2924
|
writeFileAtomic: (path, data) => atomicWriteFile(path, data, nodeOps)
|
|
2493
2925
|
};
|
|
2494
|
-
var
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
case icuMessageformatParser.TYPE.date:
|
|
2501
|
-
case icuMessageformatParser.TYPE.time:
|
|
2502
|
-
case icuMessageformatParser.TYPE.select:
|
|
2503
|
-
case icuMessageformatParser.TYPE.plural:
|
|
2504
|
-
return `{${element.value}}`;
|
|
2505
|
-
case icuMessageformatParser.TYPE.tag:
|
|
2506
|
-
return `<${element.value}>`;
|
|
2507
|
-
default:
|
|
2508
|
-
return void 0;
|
|
2926
|
+
var AdapterError = class extends Error {
|
|
2927
|
+
code;
|
|
2928
|
+
constructor(code, message) {
|
|
2929
|
+
super(message);
|
|
2930
|
+
this.name = "AdapterError";
|
|
2931
|
+
this.code = code;
|
|
2509
2932
|
}
|
|
2933
|
+
};
|
|
2934
|
+
var MAX_DEPTH = 100;
|
|
2935
|
+
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
2936
|
+
function stripLeadingBom(content) {
|
|
2937
|
+
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
2510
2938
|
}
|
|
2511
|
-
function
|
|
2512
|
-
|
|
2513
|
-
|
|
2939
|
+
async function readBoundedFile(fs, filePath) {
|
|
2940
|
+
const outcome = await fs.readBounded(filePath, MAX_INPUT_BYTES);
|
|
2941
|
+
return outcome.kind === "ok" ? { kind: "ok", content: stripLeadingBom(outcome.content) } : outcome;
|
|
2942
|
+
}
|
|
2943
|
+
function outcomeToContent(outcome, notAFileMessage) {
|
|
2944
|
+
if (outcome.kind === "not-a-file") {
|
|
2945
|
+
throw new AdapterError("INVALID_STRUCTURE", notAFileMessage);
|
|
2514
2946
|
}
|
|
2515
|
-
if (
|
|
2516
|
-
|
|
2947
|
+
if (outcome.kind === "too-large") {
|
|
2948
|
+
throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
|
|
2517
2949
|
}
|
|
2518
|
-
return
|
|
2950
|
+
return outcome.content;
|
|
2519
2951
|
}
|
|
2520
|
-
function
|
|
2521
|
-
|
|
2952
|
+
async function readFileContent(fs, filePath) {
|
|
2953
|
+
const outcome = await readBoundedFile(fs, filePath);
|
|
2954
|
+
return outcomeToContent(outcome, "The path is not a regular file.");
|
|
2955
|
+
}
|
|
2956
|
+
var LINE_TERMINATOR_SPLIT = /\r\n|\r|\n/;
|
|
2957
|
+
var TRAILING_LINE_TERMINATOR = /(?:\r\n|\r|\n)$/;
|
|
2958
|
+
function detectLineTerminator(content) {
|
|
2959
|
+
if (content.includes("\r\n")) {
|
|
2960
|
+
return "\r\n";
|
|
2961
|
+
}
|
|
2962
|
+
return content.includes("\r") ? "\r" : "\n";
|
|
2963
|
+
}
|
|
2964
|
+
function splitPhysicalLines(content) {
|
|
2965
|
+
if (content === "") {
|
|
2966
|
+
return [];
|
|
2967
|
+
}
|
|
2968
|
+
const lines = content.split(LINE_TERMINATOR_SPLIT);
|
|
2969
|
+
if (TRAILING_LINE_TERMINATOR.test(content)) {
|
|
2970
|
+
lines.pop();
|
|
2971
|
+
}
|
|
2972
|
+
return lines;
|
|
2973
|
+
}
|
|
2974
|
+
function scanTokens(value, pattern, extract = (match) => match[0]) {
|
|
2975
|
+
const result = [];
|
|
2976
|
+
for (const match of value.matchAll(pattern)) {
|
|
2977
|
+
const token = extract(match);
|
|
2978
|
+
if (token !== void 0) {
|
|
2979
|
+
result.push(token);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
return result;
|
|
2983
|
+
}
|
|
2984
|
+
function namespaceOf(filePath) {
|
|
2985
|
+
return path.basename(filePath, path.extname(filePath));
|
|
2986
|
+
}
|
|
2987
|
+
function isEnoent(error) {
|
|
2988
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2989
|
+
}
|
|
2990
|
+
function rethrowStructured(error, message) {
|
|
2991
|
+
if (error instanceof AdapterError) {
|
|
2992
|
+
throw error;
|
|
2993
|
+
}
|
|
2994
|
+
throw new AdapterError("INVALID_STRUCTURE", message);
|
|
2995
|
+
}
|
|
2996
|
+
function computeIcu(entries, compute) {
|
|
2997
|
+
if (!compute) {
|
|
2998
|
+
return [];
|
|
2999
|
+
}
|
|
3000
|
+
try {
|
|
3001
|
+
return compute(entries);
|
|
3002
|
+
} catch (error) {
|
|
3003
|
+
rethrowStructured(error, "The file could not be analyzed for message validity.");
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
function buildCanHandle(extensions, sniff) {
|
|
3007
|
+
return (filePath, sample) => {
|
|
3008
|
+
if (!extensions.includes(path.extname(filePath).toLowerCase())) {
|
|
3009
|
+
return false;
|
|
3010
|
+
}
|
|
3011
|
+
return sample === void 0 || sniff === void 0 || sniff(sample);
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
async function toEntries(content, namespace, filePath, fs, parseEntries) {
|
|
3015
|
+
try {
|
|
3016
|
+
return await parseEntries(content, namespace, filePath, fs);
|
|
3017
|
+
} catch (error) {
|
|
3018
|
+
rethrowStructured(error, "The file could not be parsed.");
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
function createFlatFileAdapter(options) {
|
|
3022
|
+
const {
|
|
3023
|
+
format,
|
|
3024
|
+
extensions,
|
|
3025
|
+
sniff,
|
|
3026
|
+
parseEntries,
|
|
3027
|
+
serializeEntries,
|
|
3028
|
+
extractPlaceholders,
|
|
3029
|
+
validateMessage,
|
|
3030
|
+
computeInvalidIcuKeys,
|
|
3031
|
+
fs = nodeAdapterFs
|
|
3032
|
+
} = options;
|
|
3033
|
+
return {
|
|
3034
|
+
format,
|
|
3035
|
+
canHandle: buildCanHandle(extensions, sniff),
|
|
3036
|
+
extractPlaceholders,
|
|
3037
|
+
validateMessage: validateMessage ?? (() => true),
|
|
3038
|
+
async read(filePath, locale) {
|
|
3039
|
+
const content = await readFileContent(fs, filePath);
|
|
3040
|
+
const namespace = namespaceOf(filePath);
|
|
3041
|
+
const entries = await toEntries(content, namespace, filePath, fs, parseEntries);
|
|
3042
|
+
const resource = { locale, namespace, format, entries };
|
|
3043
|
+
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
3044
|
+
return { resource, invalidIcuKeys, excludedLeafPaths: [] };
|
|
3045
|
+
},
|
|
3046
|
+
async write(resource, filePath) {
|
|
3047
|
+
const data = await serializeEntries(resource.entries, filePath, fs);
|
|
3048
|
+
await fs.writeFileAtomic(filePath, data);
|
|
3049
|
+
}
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
var UNICODE_ESCAPE = /^[0-9a-fA-F]{4}$/;
|
|
3053
|
+
function decodeUnicodeEscape(raw, at) {
|
|
3054
|
+
const hex = raw.slice(at, at + 4);
|
|
3055
|
+
if (!UNICODE_ESCAPE.test(hex)) {
|
|
3056
|
+
throw new AdapterError("INVALID_STRUCTURE", "The file has a malformed \\u unicode escape.");
|
|
3057
|
+
}
|
|
3058
|
+
return String.fromCharCode(Number.parseInt(hex, 16));
|
|
3059
|
+
}
|
|
3060
|
+
function decodeSimpleEscape(char) {
|
|
3061
|
+
switch (char) {
|
|
3062
|
+
case "n":
|
|
3063
|
+
return "\n";
|
|
3064
|
+
case "t":
|
|
3065
|
+
return " ";
|
|
3066
|
+
case "'":
|
|
3067
|
+
return "'";
|
|
3068
|
+
case '"':
|
|
3069
|
+
return '"';
|
|
3070
|
+
case "\\":
|
|
3071
|
+
return "\\";
|
|
3072
|
+
case "@":
|
|
3073
|
+
return "@";
|
|
3074
|
+
case "?":
|
|
3075
|
+
return "?";
|
|
3076
|
+
default:
|
|
3077
|
+
return char;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
function decodeAndroidEscapes(raw) {
|
|
3081
|
+
const pieces = [];
|
|
3082
|
+
let i = 0;
|
|
3083
|
+
while (i < raw.length) {
|
|
3084
|
+
const char = raw[i];
|
|
3085
|
+
if (char !== "\\") {
|
|
3086
|
+
pieces.push(char ?? "");
|
|
3087
|
+
i += 1;
|
|
3088
|
+
continue;
|
|
3089
|
+
}
|
|
3090
|
+
const next = raw[i + 1];
|
|
3091
|
+
if (next === void 0) {
|
|
3092
|
+
i += 1;
|
|
3093
|
+
continue;
|
|
3094
|
+
}
|
|
3095
|
+
if (next === "u") {
|
|
3096
|
+
pieces.push(decodeUnicodeEscape(raw, i + 2));
|
|
3097
|
+
i += 6;
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3100
|
+
pieces.push(decodeSimpleEscape(next));
|
|
3101
|
+
i += 2;
|
|
3102
|
+
}
|
|
3103
|
+
return pieces.join("");
|
|
3104
|
+
}
|
|
3105
|
+
function escapeChar(char, isFirst) {
|
|
3106
|
+
switch (char) {
|
|
3107
|
+
case "\\":
|
|
3108
|
+
return "\\\\";
|
|
3109
|
+
case "'":
|
|
3110
|
+
return "\\'";
|
|
3111
|
+
case '"':
|
|
3112
|
+
return '\\"';
|
|
3113
|
+
case "\n":
|
|
3114
|
+
return "\\n";
|
|
3115
|
+
case " ":
|
|
3116
|
+
return "\\t";
|
|
3117
|
+
case "@":
|
|
3118
|
+
return isFirst ? "\\@" : "@";
|
|
3119
|
+
case "?":
|
|
3120
|
+
return isFirst ? "\\?" : "?";
|
|
3121
|
+
default:
|
|
3122
|
+
return char;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
function encodeAndroidEscapes(value) {
|
|
3126
|
+
const pieces = [];
|
|
3127
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
3128
|
+
const char = value[i];
|
|
3129
|
+
if (char !== void 0) {
|
|
3130
|
+
pieces.push(escapeChar(char, i === 0));
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
return pieces.join("");
|
|
3134
|
+
}
|
|
3135
|
+
var RESOURCE_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3136
|
+
function assertValidResourceName(name, tag) {
|
|
3137
|
+
if (!RESOURCE_NAME.test(name)) {
|
|
3138
|
+
throw new AdapterError(
|
|
3139
|
+
"INVALID_STRUCTURE",
|
|
3140
|
+
`The <${tag} name="${name}"> resource name is not a valid Android resource name. A resource name must start with a letter or underscore and contain only letters, digits, and underscores.`
|
|
3141
|
+
);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
var PRINTF_TOKEN = /%(?:(\d+)\$)?[-+0#]*(?:[1-9]\d*)?(?:\.\d+)?(?:hh|h|ll|l|q|z|j|t|L)?([A-Za-z@])|%%/g;
|
|
3145
|
+
function extractPrintfPlaceholders(value, options) {
|
|
3146
|
+
const out = [];
|
|
3147
|
+
for (const match of value.matchAll(PRINTF_TOKEN)) {
|
|
3148
|
+
if (match[0] === "%%") {
|
|
3149
|
+
out.push("%%");
|
|
3150
|
+
continue;
|
|
3151
|
+
}
|
|
3152
|
+
const conversion = match[2];
|
|
3153
|
+
if (conversion === void 0 || !options.conversions.has(conversion)) {
|
|
3154
|
+
continue;
|
|
3155
|
+
}
|
|
3156
|
+
const position = match[1];
|
|
3157
|
+
out.push(position === void 0 ? `%${conversion}` : `%${position}$${conversion}`);
|
|
3158
|
+
}
|
|
3159
|
+
return out;
|
|
3160
|
+
}
|
|
3161
|
+
var ANDROID_PRINTF_CONVERSIONS = /* @__PURE__ */ new Set([
|
|
3162
|
+
"s",
|
|
3163
|
+
"S",
|
|
3164
|
+
"d",
|
|
3165
|
+
"f",
|
|
3166
|
+
"x",
|
|
3167
|
+
"X",
|
|
3168
|
+
"o",
|
|
3169
|
+
"c",
|
|
3170
|
+
"C",
|
|
3171
|
+
"b",
|
|
3172
|
+
"B",
|
|
3173
|
+
"e",
|
|
3174
|
+
"E",
|
|
3175
|
+
"g",
|
|
3176
|
+
"G"
|
|
3177
|
+
]);
|
|
3178
|
+
function extractAndroidPlaceholders(value) {
|
|
3179
|
+
return extractPrintfPlaceholders(value, { conversions: ANDROID_PRINTF_CONVERSIONS });
|
|
3180
|
+
}
|
|
3181
|
+
var ANDROID_PLURAL_CATEGORIES = [
|
|
3182
|
+
"zero",
|
|
3183
|
+
"one",
|
|
3184
|
+
"two",
|
|
3185
|
+
"few",
|
|
3186
|
+
"many",
|
|
3187
|
+
"other"
|
|
3188
|
+
];
|
|
3189
|
+
var ANDROID_PLURAL_CATEGORY_SET = new Set(ANDROID_PLURAL_CATEGORIES);
|
|
3190
|
+
var PLURAL_KEY = /^(.*)\[(zero|one|two|few|many|other)\]$/;
|
|
3191
|
+
function isAndroidPluralCategory(value) {
|
|
3192
|
+
return ANDROID_PLURAL_CATEGORY_SET.has(value);
|
|
3193
|
+
}
|
|
3194
|
+
function makeAndroidPluralKey(baseKey, category) {
|
|
3195
|
+
return `${baseKey}[${category}]`;
|
|
3196
|
+
}
|
|
3197
|
+
function androidPluralBaseKey(key) {
|
|
3198
|
+
return PLURAL_KEY.exec(key)?.[1];
|
|
3199
|
+
}
|
|
3200
|
+
function androidPluralCategoryOf(key) {
|
|
3201
|
+
const category = PLURAL_KEY.exec(key)?.[2];
|
|
3202
|
+
return category === void 0 ? void 0 : category;
|
|
3203
|
+
}
|
|
3204
|
+
var ELEMENT_NODE = 1;
|
|
3205
|
+
var TEXT_NODE = 3;
|
|
3206
|
+
var ROOT_TAG = "resources";
|
|
3207
|
+
function onFatal(level, message) {
|
|
3208
|
+
if (level === "fatalError") {
|
|
3209
|
+
throw new Error(message);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
function assertNoDoctype(content) {
|
|
3213
|
+
if (/<!DOCTYPE/i.test(content) || /<!ENTITY/i.test(content)) {
|
|
3214
|
+
throw new AdapterError(
|
|
3215
|
+
"INVALID_XML",
|
|
3216
|
+
"strings.xml with a DTD or entity declaration is rejected."
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
function parseErrorDetail(error) {
|
|
3221
|
+
return error instanceof Error && error.message.length > 0 ? `: ${error.message}` : "";
|
|
3222
|
+
}
|
|
3223
|
+
function parseAndroidXml(content) {
|
|
3224
|
+
assertNoDoctype(content);
|
|
3225
|
+
let doc;
|
|
3226
|
+
try {
|
|
3227
|
+
doc = new xmldom.DOMParser({ onError: onFatal }).parseFromString(content, "text/xml");
|
|
3228
|
+
} catch (error) {
|
|
3229
|
+
throw new AdapterError("INVALID_XML", `The file is not valid XML${parseErrorDetail(error)}.`);
|
|
3230
|
+
}
|
|
3231
|
+
const root = doc.documentElement;
|
|
3232
|
+
if (root === null || root.localName !== ROOT_TAG) {
|
|
3233
|
+
throw new AdapterError(
|
|
3234
|
+
"INVALID_STRUCTURE",
|
|
3235
|
+
`The file is not an Android resources file: the root element must be <${ROOT_TAG}>.`
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3238
|
+
return { doc, root };
|
|
3239
|
+
}
|
|
3240
|
+
function createAndroidXmlDocument() {
|
|
3241
|
+
const doc = new xmldom.DOMImplementation().createDocument(null, ROOT_TAG, null);
|
|
3242
|
+
const root = doc.documentElement;
|
|
3243
|
+
if (root === null) {
|
|
3244
|
+
throw new AdapterError("INVALID_STRUCTURE", "Could not synthesize a new resources document.");
|
|
3245
|
+
}
|
|
3246
|
+
return { doc, root };
|
|
3247
|
+
}
|
|
3248
|
+
function isElement(node) {
|
|
3249
|
+
return node.nodeType === ELEMENT_NODE;
|
|
3250
|
+
}
|
|
3251
|
+
function elementChildren(parent, tag) {
|
|
3252
|
+
const children = Array.from(parent.childNodes).filter(isElement);
|
|
3253
|
+
return tag === void 0 ? children : children.filter((el) => el.localName === tag);
|
|
3254
|
+
}
|
|
3255
|
+
function singleTextValue(element) {
|
|
3256
|
+
const children = element.childNodes;
|
|
3257
|
+
if (children.length === 0) {
|
|
3258
|
+
return "";
|
|
3259
|
+
}
|
|
3260
|
+
if (children.length === 1) {
|
|
3261
|
+
const only = children.item(0);
|
|
3262
|
+
if (only !== null && only.nodeType === TEXT_NODE) {
|
|
3263
|
+
return only.nodeValue ?? "";
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
return void 0;
|
|
3267
|
+
}
|
|
3268
|
+
function setSingleTextValue(doc, element, value) {
|
|
3269
|
+
while (element.firstChild !== null) {
|
|
3270
|
+
element.removeChild(element.firstChild);
|
|
3271
|
+
}
|
|
3272
|
+
element.appendChild(doc.createTextNode(value));
|
|
3273
|
+
}
|
|
3274
|
+
function isTranslatable(element) {
|
|
3275
|
+
return element.getAttribute("translatable") !== "false";
|
|
3276
|
+
}
|
|
3277
|
+
function setEntry(out, key, namespace, value, isPlural) {
|
|
3278
|
+
if (out.has(key)) {
|
|
3279
|
+
throw new AdapterError(
|
|
3280
|
+
"INVALID_STRUCTURE",
|
|
3281
|
+
`The resolved key "${key}" is defined more than once in the file.`
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
out.set(key, {
|
|
3285
|
+
key,
|
|
3286
|
+
namespace,
|
|
3287
|
+
value,
|
|
3288
|
+
placeholders: extractAndroidPlaceholders(value),
|
|
3289
|
+
isPlural
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
function parseStringElement(element, namespace, out) {
|
|
3293
|
+
const name = element.getAttribute("name") ?? "";
|
|
3294
|
+
assertValidResourceName(name, "string");
|
|
3295
|
+
if (!isTranslatable(element)) {
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
const raw = singleTextValue(element);
|
|
3299
|
+
if (raw === void 0) {
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
setEntry(out, name, namespace, decodeAndroidEscapes(raw), false);
|
|
3303
|
+
}
|
|
3304
|
+
function assertUniqueQuantities(items, name) {
|
|
3305
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3306
|
+
for (const item of items) {
|
|
3307
|
+
const quantity = item.getAttribute("quantity") ?? "";
|
|
3308
|
+
if (seen.has(quantity)) {
|
|
3309
|
+
throw new AdapterError(
|
|
3310
|
+
"INVALID_STRUCTURE",
|
|
3311
|
+
`The <plurals name="${name}"> element has more than one item for quantity "${quantity}".`
|
|
3312
|
+
);
|
|
3313
|
+
}
|
|
3314
|
+
seen.add(quantity);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
function parsePluralsElement(element, namespace, out) {
|
|
3318
|
+
const name = element.getAttribute("name") ?? "";
|
|
3319
|
+
assertValidResourceName(name, "plurals");
|
|
3320
|
+
if (!isTranslatable(element)) {
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
const items = elementChildren(element, "item");
|
|
3324
|
+
assertUniqueQuantities(items, name);
|
|
3325
|
+
const values = [];
|
|
3326
|
+
for (const item of items) {
|
|
3327
|
+
const quantity = item.getAttribute("quantity") ?? "";
|
|
3328
|
+
if (!isAndroidPluralCategory(quantity)) {
|
|
3329
|
+
throw new AdapterError(
|
|
3330
|
+
"INVALID_STRUCTURE",
|
|
3331
|
+
`The <plurals name="${name}"> element has an item with quantity="${quantity}", which is not one of ${ANDROID_PLURAL_CATEGORIES.join(", ")}.`
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3334
|
+
const raw = singleTextValue(item);
|
|
3335
|
+
if (raw === void 0) {
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
values.push([quantity, raw]);
|
|
3339
|
+
}
|
|
3340
|
+
for (const [quantity, raw] of values) {
|
|
3341
|
+
setEntry(
|
|
3342
|
+
out,
|
|
3343
|
+
makeAndroidPluralKey(name, quantity),
|
|
3344
|
+
namespace,
|
|
3345
|
+
decodeAndroidEscapes(raw),
|
|
3346
|
+
true
|
|
3347
|
+
);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
function parseAndroidXmlEntries(content, namespace) {
|
|
3351
|
+
const { root } = parseAndroidXml(content);
|
|
3352
|
+
const out = /* @__PURE__ */ new Map();
|
|
3353
|
+
for (const element of elementChildren(root)) {
|
|
3354
|
+
if (element.localName === "string") {
|
|
3355
|
+
parseStringElement(element, namespace, out);
|
|
3356
|
+
} else if (element.localName === "plurals") {
|
|
3357
|
+
parsePluralsElement(element, namespace, out);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
return out;
|
|
3361
|
+
}
|
|
3362
|
+
var XML_PROLOG = '<?xml version="1.0" encoding="utf-8"?>\n';
|
|
3363
|
+
function groupEntries(entries) {
|
|
3364
|
+
const singular = /* @__PURE__ */ new Map();
|
|
3365
|
+
const plural = /* @__PURE__ */ new Map();
|
|
3366
|
+
for (const [key, entry] of entries) {
|
|
3367
|
+
const baseKey = androidPluralBaseKey(key);
|
|
3368
|
+
const category = androidPluralCategoryOf(key);
|
|
3369
|
+
if (baseKey !== void 0 && category !== void 0) {
|
|
3370
|
+
const group = plural.get(baseKey) ?? /* @__PURE__ */ new Map();
|
|
3371
|
+
group.set(category, entry);
|
|
3372
|
+
plural.set(baseKey, group);
|
|
3373
|
+
} else {
|
|
3374
|
+
singular.set(key, entry);
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
return { singular, plural };
|
|
3378
|
+
}
|
|
3379
|
+
function isTranslatable2(element) {
|
|
3380
|
+
return element.getAttribute("translatable") !== "false";
|
|
3381
|
+
}
|
|
3382
|
+
function appendStringElement(doc, root, name, value) {
|
|
3383
|
+
const element = doc.createElement("string");
|
|
3384
|
+
element.setAttribute("name", name);
|
|
3385
|
+
element.appendChild(doc.createTextNode(encodeAndroidEscapes(value)));
|
|
3386
|
+
root.appendChild(element);
|
|
3387
|
+
}
|
|
3388
|
+
function appendPluralsElement(doc, root, name, categories) {
|
|
3389
|
+
const plurals = doc.createElement("plurals");
|
|
3390
|
+
plurals.setAttribute("name", name);
|
|
3391
|
+
for (const category of ANDROID_PLURAL_CATEGORIES) {
|
|
3392
|
+
const entry = categories.get(category);
|
|
3393
|
+
if (entry === void 0) {
|
|
3394
|
+
continue;
|
|
3395
|
+
}
|
|
3396
|
+
const item = doc.createElement("item");
|
|
3397
|
+
item.setAttribute("quantity", category);
|
|
3398
|
+
item.appendChild(doc.createTextNode(encodeAndroidEscapes(entry.value)));
|
|
3399
|
+
plurals.appendChild(item);
|
|
3400
|
+
}
|
|
3401
|
+
root.appendChild(plurals);
|
|
3402
|
+
}
|
|
3403
|
+
function appendUnmatched(doc, root, grouped, matched) {
|
|
3404
|
+
for (const [key, entry] of grouped.singular) {
|
|
3405
|
+
if (!matched.has(key)) {
|
|
3406
|
+
appendStringElement(doc, root, key, entry.value);
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
for (const [baseKey, categories] of grouped.plural) {
|
|
3410
|
+
if (!matched.has(baseKey)) {
|
|
3411
|
+
appendPluralsElement(doc, root, baseKey, categories);
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
function isReadThroughString(element) {
|
|
3416
|
+
return !isTranslatable2(element) || singleTextValue(element) === void 0;
|
|
3417
|
+
}
|
|
3418
|
+
function patchStringElement(doc, element, grouped, matched) {
|
|
3419
|
+
const name = element.getAttribute("name") ?? "";
|
|
3420
|
+
assertValidResourceName(name, "string");
|
|
3421
|
+
const entry = grouped.singular.get(name);
|
|
3422
|
+
if (entry !== void 0) {
|
|
3423
|
+
matched.add(name);
|
|
3424
|
+
element.removeAttribute("translatable");
|
|
3425
|
+
setSingleTextValue(doc, element, encodeAndroidEscapes(entry.value));
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
if (isReadThroughString(element)) {
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
matched.add(name);
|
|
3432
|
+
element.parentNode?.removeChild(element);
|
|
3433
|
+
}
|
|
3434
|
+
function isReadThroughPlurals(items) {
|
|
3435
|
+
return items.some((item) => singleTextValue(item) === void 0);
|
|
3436
|
+
}
|
|
3437
|
+
function patchTranslatablePlurals(doc, element, items, categories) {
|
|
3438
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3439
|
+
for (const item of items) {
|
|
3440
|
+
const quantity = item.getAttribute("quantity") ?? "";
|
|
3441
|
+
const entry = categories.get(quantity);
|
|
3442
|
+
if (entry === void 0) {
|
|
3443
|
+
element.removeChild(item);
|
|
3444
|
+
continue;
|
|
3445
|
+
}
|
|
3446
|
+
setSingleTextValue(doc, item, encodeAndroidEscapes(entry.value));
|
|
3447
|
+
seen.add(quantity);
|
|
3448
|
+
}
|
|
3449
|
+
for (const category of ANDROID_PLURAL_CATEGORIES) {
|
|
3450
|
+
const entry = categories.get(category);
|
|
3451
|
+
if (entry === void 0 || seen.has(category)) {
|
|
3452
|
+
continue;
|
|
3453
|
+
}
|
|
3454
|
+
const item = doc.createElement("item");
|
|
3455
|
+
item.setAttribute("quantity", category);
|
|
3456
|
+
item.appendChild(doc.createTextNode(encodeAndroidEscapes(entry.value)));
|
|
3457
|
+
element.appendChild(item);
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
function patchPluralsElement(doc, element, grouped, matched) {
|
|
3461
|
+
const name = element.getAttribute("name") ?? "";
|
|
3462
|
+
assertValidResourceName(name, "plurals");
|
|
3463
|
+
const items = elementChildren(element, "item");
|
|
3464
|
+
const categories = grouped.plural.get(name);
|
|
3465
|
+
if (categories !== void 0) {
|
|
3466
|
+
matched.add(name);
|
|
3467
|
+
element.removeAttribute("translatable");
|
|
3468
|
+
patchTranslatablePlurals(doc, element, items, categories);
|
|
3469
|
+
if (elementChildren(element, "item").length === 0) {
|
|
3470
|
+
element.parentNode?.removeChild(element);
|
|
3471
|
+
}
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
if (!isTranslatable2(element) || isReadThroughPlurals(items)) {
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
matched.add(name);
|
|
3478
|
+
element.parentNode?.removeChild(element);
|
|
3479
|
+
}
|
|
3480
|
+
function patchDocument(doc, root, grouped) {
|
|
3481
|
+
const matched = /* @__PURE__ */ new Set();
|
|
3482
|
+
for (const element of elementChildren(root)) {
|
|
3483
|
+
if (element.localName === "string") {
|
|
3484
|
+
patchStringElement(doc, element, grouped, matched);
|
|
3485
|
+
} else if (element.localName === "plurals") {
|
|
3486
|
+
patchPluralsElement(doc, element, grouped, matched);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
appendUnmatched(doc, root, grouped, matched);
|
|
3490
|
+
}
|
|
3491
|
+
async function readDestination(filePath, fs) {
|
|
3492
|
+
try {
|
|
3493
|
+
const outcome = await readBoundedFile(fs, filePath);
|
|
3494
|
+
return outcomeToContent(outcome, "The destination path is not a regular file.");
|
|
3495
|
+
} catch (error) {
|
|
3496
|
+
if (isEnoent(error)) {
|
|
3497
|
+
return void 0;
|
|
3498
|
+
}
|
|
3499
|
+
throw error;
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
async function serializeAndroidXmlEntries(entries, filePath, fs) {
|
|
3503
|
+
const grouped = groupEntries(entries);
|
|
3504
|
+
const existing = await readDestination(filePath, fs);
|
|
3505
|
+
if (existing === void 0) {
|
|
3506
|
+
const { doc: doc2, root: root2 } = createAndroidXmlDocument();
|
|
3507
|
+
appendUnmatched(doc2, root2, grouped, /* @__PURE__ */ new Set());
|
|
3508
|
+
return XML_PROLOG + new xmldom.XMLSerializer().serializeToString(doc2);
|
|
3509
|
+
}
|
|
3510
|
+
const { doc, root } = parseAndroidXml(existing);
|
|
3511
|
+
patchDocument(doc, root, grouped);
|
|
3512
|
+
return new xmldom.XMLSerializer().serializeToString(doc);
|
|
3513
|
+
}
|
|
3514
|
+
function createAndroidXmlAdapter(fs = nodeAdapterFs) {
|
|
3515
|
+
return createFlatFileAdapter({
|
|
3516
|
+
fs,
|
|
3517
|
+
format: "android-xml",
|
|
3518
|
+
extensions: [".xml"],
|
|
3519
|
+
parseEntries: parseAndroidXmlEntries,
|
|
3520
|
+
serializeEntries: serializeAndroidXmlEntries,
|
|
3521
|
+
extractPlaceholders: extractAndroidPlaceholders
|
|
3522
|
+
});
|
|
3523
|
+
}
|
|
3524
|
+
var APPLE_PRINTF_CONVERSIONS = /* @__PURE__ */ new Set([
|
|
3525
|
+
"@",
|
|
3526
|
+
"d",
|
|
3527
|
+
"i",
|
|
3528
|
+
"u",
|
|
3529
|
+
"x",
|
|
3530
|
+
"X",
|
|
3531
|
+
"o",
|
|
3532
|
+
"f",
|
|
3533
|
+
"e",
|
|
3534
|
+
"E",
|
|
3535
|
+
"g",
|
|
3536
|
+
"G",
|
|
3537
|
+
"c",
|
|
3538
|
+
"s"
|
|
3539
|
+
]);
|
|
3540
|
+
function extractAppleStringsPlaceholders(value) {
|
|
3541
|
+
return extractPrintfPlaceholders(value, { conversions: APPLE_PRINTF_CONVERSIONS });
|
|
3542
|
+
}
|
|
3543
|
+
var UNICODE_ESCAPE2 = /^[0-9a-fA-F]{4}$/;
|
|
3544
|
+
var TRAILING_BLOCK_COMMENT = /\/\*([\s\S]*?)\*\//g;
|
|
3545
|
+
var ATTACHED_TAIL = /^[ \t]*\r?\n?[ \t]*$/;
|
|
3546
|
+
function isInlineWhitespace(char) {
|
|
3547
|
+
return char === " " || char === " " || char === "\r" || char === "\n";
|
|
3548
|
+
}
|
|
3549
|
+
function skipInlineWhitespace(content, from) {
|
|
3550
|
+
let i = from;
|
|
3551
|
+
while (isInlineWhitespace(content[i])) {
|
|
3552
|
+
i += 1;
|
|
3553
|
+
}
|
|
3554
|
+
return i;
|
|
3555
|
+
}
|
|
3556
|
+
function lineOf(content, position) {
|
|
3557
|
+
return content.slice(0, position).split("\n").length;
|
|
3558
|
+
}
|
|
3559
|
+
function hasUtf16Bom(content) {
|
|
3560
|
+
return content.charCodeAt(0) === 65533 && content.charCodeAt(1) === 65533;
|
|
3561
|
+
}
|
|
3562
|
+
function assertNotUtf16(content) {
|
|
3563
|
+
if (hasUtf16Bom(content)) {
|
|
3564
|
+
throw new AdapterError(
|
|
3565
|
+
"INVALID_STRUCTURE",
|
|
3566
|
+
"The file appears to be UTF-16 encoded. Apple .strings files must be UTF-8."
|
|
3567
|
+
);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
function decodeUnicodeEscape2(content, at) {
|
|
3571
|
+
const hex = content.slice(at, at + 4);
|
|
3572
|
+
if (!UNICODE_ESCAPE2.test(hex)) {
|
|
3573
|
+
throw new AdapterError(
|
|
3574
|
+
"INVALID_STRUCTURE",
|
|
3575
|
+
`The file has a malformed \\U unicode escape (line ${lineOf(content, at)}).`
|
|
3576
|
+
);
|
|
3577
|
+
}
|
|
3578
|
+
return { char: String.fromCharCode(Number.parseInt(hex, 16)), next: at + 4 };
|
|
3579
|
+
}
|
|
3580
|
+
function decodeEscape(content, at) {
|
|
3581
|
+
const escaped = content[at];
|
|
3582
|
+
switch (escaped) {
|
|
3583
|
+
case '"':
|
|
3584
|
+
return { char: '"', next: at + 1 };
|
|
3585
|
+
case "\\":
|
|
3586
|
+
return { char: "\\", next: at + 1 };
|
|
3587
|
+
case "n":
|
|
3588
|
+
return { char: "\n", next: at + 1 };
|
|
3589
|
+
case "t":
|
|
3590
|
+
return { char: " ", next: at + 1 };
|
|
3591
|
+
case "U":
|
|
3592
|
+
return decodeUnicodeEscape2(content, at + 1);
|
|
3593
|
+
default:
|
|
3594
|
+
throw new AdapterError(
|
|
3595
|
+
"INVALID_STRUCTURE",
|
|
3596
|
+
`The file has an unknown escape sequence "\\${escaped ?? ""}" (line ${lineOf(content, at)}).`
|
|
3597
|
+
);
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
function parseQuotedString(content, start) {
|
|
3601
|
+
let i = start + 1;
|
|
3602
|
+
let decoded = "";
|
|
3603
|
+
while (true) {
|
|
3604
|
+
const char = content[i];
|
|
3605
|
+
if (char === void 0) {
|
|
3606
|
+
throw new AdapterError(
|
|
3607
|
+
"INVALID_STRUCTURE",
|
|
3608
|
+
`The file has an unterminated quoted string (line ${lineOf(content, start)}).`
|
|
3609
|
+
);
|
|
3610
|
+
}
|
|
3611
|
+
if (char === '"') {
|
|
3612
|
+
i += 1;
|
|
3613
|
+
break;
|
|
3614
|
+
}
|
|
3615
|
+
if (char === "\\") {
|
|
3616
|
+
const token = decodeEscape(content, i + 1);
|
|
3617
|
+
decoded += token.char;
|
|
3618
|
+
i = token.next;
|
|
3619
|
+
continue;
|
|
3620
|
+
}
|
|
3621
|
+
decoded += char;
|
|
3622
|
+
i += 1;
|
|
3623
|
+
}
|
|
3624
|
+
return { raw: content.slice(start, i), decoded, end: i };
|
|
3625
|
+
}
|
|
3626
|
+
function parseStatement(content, start) {
|
|
3627
|
+
const keyToken = parseQuotedString(content, start);
|
|
3628
|
+
const afterKey = skipInlineWhitespace(content, keyToken.end);
|
|
3629
|
+
if (content[afterKey] !== "=") {
|
|
3630
|
+
throw new AdapterError(
|
|
3631
|
+
"INVALID_STRUCTURE",
|
|
3632
|
+
`The file has a statement missing its = separator (line ${lineOf(content, afterKey)}).`
|
|
3633
|
+
);
|
|
3634
|
+
}
|
|
3635
|
+
const valueStart = skipInlineWhitespace(content, afterKey + 1);
|
|
3636
|
+
if (content[valueStart] !== '"') {
|
|
3637
|
+
throw new AdapterError(
|
|
3638
|
+
"INVALID_STRUCTURE",
|
|
3639
|
+
`The file has a statement with no quoted value (line ${lineOf(content, valueStart)}).`
|
|
3640
|
+
);
|
|
3641
|
+
}
|
|
3642
|
+
const valueToken = parseQuotedString(content, valueStart);
|
|
3643
|
+
const semicolonAt = skipInlineWhitespace(content, valueToken.end);
|
|
3644
|
+
if (content[semicolonAt] !== ";") {
|
|
3645
|
+
throw new AdapterError(
|
|
3646
|
+
"INVALID_STRUCTURE",
|
|
3647
|
+
`The file has a statement missing its ; terminator (line ${lineOf(content, semicolonAt)}).`
|
|
3648
|
+
);
|
|
3649
|
+
}
|
|
3650
|
+
return {
|
|
3651
|
+
key: keyToken.decoded,
|
|
3652
|
+
keyRaw: keyToken.raw,
|
|
3653
|
+
value: valueToken.decoded,
|
|
3654
|
+
betweenKeyAndValue: content.slice(keyToken.end, valueStart),
|
|
3655
|
+
afterValueBeforeSemicolon: content.slice(valueToken.end, semicolonAt),
|
|
3656
|
+
end: semicolonAt + 1
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
function skipComment(content, at) {
|
|
3660
|
+
if (content[at + 1] === "*") {
|
|
3661
|
+
const closeAt = content.indexOf("*/", at + 2);
|
|
3662
|
+
return closeAt === -1 ? content.length : closeAt + 2;
|
|
3663
|
+
}
|
|
3664
|
+
let i = at + 2;
|
|
3665
|
+
while (i < content.length && content[i] !== "\n") {
|
|
3666
|
+
i += 1;
|
|
3667
|
+
}
|
|
3668
|
+
return i;
|
|
3669
|
+
}
|
|
3670
|
+
function isCommentStart(content, at) {
|
|
3671
|
+
return content[at] === "/" && (content[at + 1] === "*" || content[at + 1] === "/");
|
|
3672
|
+
}
|
|
3673
|
+
function splitLeading(leading) {
|
|
3674
|
+
let lastMatch = null;
|
|
3675
|
+
for (const match of leading.matchAll(TRAILING_BLOCK_COMMENT)) {
|
|
3676
|
+
lastMatch = match;
|
|
3677
|
+
}
|
|
3678
|
+
if (lastMatch === null) {
|
|
3679
|
+
return { alwaysPreserved: "", ownedByEntry: leading };
|
|
3680
|
+
}
|
|
3681
|
+
const commentEnd = lastMatch.index + lastMatch[0].length;
|
|
3682
|
+
const tail = leading.slice(commentEnd);
|
|
3683
|
+
if (!ATTACHED_TAIL.test(tail)) {
|
|
3684
|
+
return { alwaysPreserved: leading.slice(0, commentEnd), ownedByEntry: tail };
|
|
3685
|
+
}
|
|
3686
|
+
const inner = (lastMatch[1] ?? "").trim();
|
|
3687
|
+
return {
|
|
3688
|
+
alwaysPreserved: "",
|
|
3689
|
+
ownedByEntry: leading,
|
|
3690
|
+
...inner !== "" ? { description: inner } : {}
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
function scanAppleStrings(content) {
|
|
3694
|
+
const nodes = [];
|
|
3695
|
+
let rawStart = 0;
|
|
3696
|
+
let i = 0;
|
|
3697
|
+
while (i < content.length) {
|
|
3698
|
+
const char = content[i];
|
|
3699
|
+
if (char === '"') {
|
|
3700
|
+
const split = splitLeading(content.slice(rawStart, i));
|
|
3701
|
+
const statement = parseStatement(content, i);
|
|
3702
|
+
nodes.push({
|
|
3703
|
+
kind: "entry",
|
|
3704
|
+
alwaysPreserved: split.alwaysPreserved,
|
|
3705
|
+
ownedByEntry: split.ownedByEntry,
|
|
3706
|
+
...split.description !== void 0 ? { description: split.description } : {},
|
|
3707
|
+
key: statement.key,
|
|
3708
|
+
keyRaw: statement.keyRaw,
|
|
3709
|
+
value: statement.value,
|
|
3710
|
+
betweenKeyAndValue: statement.betweenKeyAndValue,
|
|
3711
|
+
afterValueBeforeSemicolon: statement.afterValueBeforeSemicolon
|
|
3712
|
+
});
|
|
3713
|
+
i = statement.end;
|
|
3714
|
+
rawStart = i;
|
|
3715
|
+
continue;
|
|
3716
|
+
}
|
|
3717
|
+
if (isCommentStart(content, i)) {
|
|
3718
|
+
i = skipComment(content, i);
|
|
3719
|
+
continue;
|
|
3720
|
+
}
|
|
3721
|
+
i += 1;
|
|
3722
|
+
}
|
|
3723
|
+
if (rawStart < content.length) {
|
|
3724
|
+
nodes.push({ kind: "text", text: content.slice(rawStart) });
|
|
3725
|
+
}
|
|
3726
|
+
return nodes;
|
|
3727
|
+
}
|
|
3728
|
+
function parseAppleStringsEntries(content, namespace) {
|
|
3729
|
+
assertNotUtf16(content);
|
|
3730
|
+
const map = /* @__PURE__ */ new Map();
|
|
3731
|
+
for (const node of scanAppleStrings(content)) {
|
|
3732
|
+
if (node.kind === "text") {
|
|
3733
|
+
continue;
|
|
3734
|
+
}
|
|
3735
|
+
map.set(node.key, {
|
|
3736
|
+
key: node.key,
|
|
3737
|
+
namespace,
|
|
3738
|
+
value: node.value,
|
|
3739
|
+
placeholders: extractAppleStringsPlaceholders(node.value),
|
|
3740
|
+
isPlural: false,
|
|
3741
|
+
...node.description !== void 0 ? { description: node.description } : {}
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
return map;
|
|
3745
|
+
}
|
|
3746
|
+
function escapeChar2(char) {
|
|
3747
|
+
switch (char) {
|
|
3748
|
+
case "\\":
|
|
3749
|
+
return "\\\\";
|
|
3750
|
+
case '"':
|
|
3751
|
+
return '\\"';
|
|
3752
|
+
case "\n":
|
|
3753
|
+
return "\\n";
|
|
3754
|
+
case " ":
|
|
3755
|
+
return "\\t";
|
|
3756
|
+
default:
|
|
3757
|
+
return char;
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
function escapeValue(value) {
|
|
3761
|
+
let out = "";
|
|
3762
|
+
for (const char of value) {
|
|
3763
|
+
out += escapeChar2(char);
|
|
3764
|
+
}
|
|
3765
|
+
return out;
|
|
3766
|
+
}
|
|
3767
|
+
async function readDestinationNodes(filePath, fs) {
|
|
3768
|
+
let outcome;
|
|
3769
|
+
try {
|
|
3770
|
+
outcome = await readBoundedFile(fs, filePath);
|
|
3771
|
+
} catch (error) {
|
|
3772
|
+
if (isEnoent(error)) {
|
|
3773
|
+
return { nodes: [], terminator: "\n" };
|
|
3774
|
+
}
|
|
3775
|
+
throw new AdapterError("INVALID_STRUCTURE", "The destination file could not be read.");
|
|
3776
|
+
}
|
|
3777
|
+
const content = outcomeToContent(outcome, "The destination path is not a regular file.");
|
|
3778
|
+
assertNotUtf16(content);
|
|
3779
|
+
return { nodes: scanAppleStrings(content), terminator: detectLineTerminator(content) };
|
|
3780
|
+
}
|
|
3781
|
+
function endsWithNewline(text) {
|
|
3782
|
+
return text === "" || text.endsWith("\n") || text.endsWith("\r");
|
|
3783
|
+
}
|
|
3784
|
+
async function serializeAppleStringsEntries(entries, filePath, fs) {
|
|
3785
|
+
const { nodes, terminator } = await readDestinationNodes(filePath, fs);
|
|
3786
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
3787
|
+
const parts = [];
|
|
3788
|
+
for (const node of nodes) {
|
|
3789
|
+
if (node.kind === "text") {
|
|
3790
|
+
parts.push(node.text);
|
|
3791
|
+
continue;
|
|
3792
|
+
}
|
|
3793
|
+
parts.push(node.alwaysPreserved);
|
|
3794
|
+
const entry = entries.get(node.key);
|
|
3795
|
+
if (entry === void 0 || emitted.has(node.key)) {
|
|
3796
|
+
continue;
|
|
3797
|
+
}
|
|
3798
|
+
parts.push(
|
|
3799
|
+
`${node.ownedByEntry}${node.keyRaw}${node.betweenKeyAndValue}"${escapeValue(entry.value)}"${node.afterValueBeforeSemicolon};`
|
|
3800
|
+
);
|
|
3801
|
+
emitted.add(node.key);
|
|
3802
|
+
}
|
|
3803
|
+
let result = parts.join("");
|
|
3804
|
+
const appended = [];
|
|
3805
|
+
for (const [key, entry] of entries) {
|
|
3806
|
+
if (!emitted.has(key)) {
|
|
3807
|
+
appended.push(`"${escapeValue(key)}" = "${escapeValue(entry.value)}";${terminator}`);
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
if (appended.length > 0) {
|
|
3811
|
+
if (!endsWithNewline(result)) {
|
|
3812
|
+
result += terminator;
|
|
3813
|
+
}
|
|
3814
|
+
result += appended.join("");
|
|
3815
|
+
}
|
|
3816
|
+
return result;
|
|
3817
|
+
}
|
|
3818
|
+
var PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/;
|
|
3819
|
+
function isPluralKey(key) {
|
|
3820
|
+
return PLURAL_SUFFIX.test(key);
|
|
3821
|
+
}
|
|
3822
|
+
function pluralCategoryOf(key) {
|
|
3823
|
+
const match = PLURAL_SUFFIX.exec(key);
|
|
3824
|
+
return match?.[1];
|
|
3825
|
+
}
|
|
3826
|
+
function pluralBaseKey(key) {
|
|
3827
|
+
if (!isPluralKey(key)) {
|
|
3828
|
+
return void 0;
|
|
3829
|
+
}
|
|
3830
|
+
return key.replace(PLURAL_SUFFIX, "");
|
|
3831
|
+
}
|
|
3832
|
+
function makePluralKey(baseKey, category) {
|
|
3833
|
+
return `${baseKey}_${category}`;
|
|
3834
|
+
}
|
|
3835
|
+
var ELEMENT_NODE2 = 1;
|
|
3836
|
+
var PLURAL_CATEGORIES = [
|
|
3837
|
+
"zero",
|
|
3838
|
+
"one",
|
|
3839
|
+
"two",
|
|
3840
|
+
"few",
|
|
3841
|
+
"many",
|
|
3842
|
+
"other"
|
|
3843
|
+
];
|
|
3844
|
+
var PLURAL_CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
3845
|
+
var FORMAT_KEY = "NSStringLocalizedFormatKey";
|
|
3846
|
+
var SPEC_TYPE_KEY = "NSStringFormatSpecTypeKey";
|
|
3847
|
+
var VALUE_TYPE_KEY = "NSStringFormatValueTypeKey";
|
|
3848
|
+
var PLURAL_RULE_TYPE = "NSStringPluralRuleType";
|
|
3849
|
+
var VARIABLE_REF = /%#@(\w+)@/;
|
|
3850
|
+
var PLIST_HEADER = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n';
|
|
3851
|
+
function isElement2(node) {
|
|
3852
|
+
return node.nodeType === ELEMENT_NODE2;
|
|
3853
|
+
}
|
|
3854
|
+
function elementChildren2(parent) {
|
|
3855
|
+
return Array.from(parent.childNodes).filter(isElement2);
|
|
3856
|
+
}
|
|
3857
|
+
function doctypeHasInternalSubset(content) {
|
|
3858
|
+
const start = content.search(/<!DOCTYPE/i);
|
|
3859
|
+
if (start === -1) {
|
|
3860
|
+
return false;
|
|
3861
|
+
}
|
|
3862
|
+
for (let i = start; i < content.length; i += 1) {
|
|
3863
|
+
const char = content[i];
|
|
3864
|
+
if (char === "[") {
|
|
3865
|
+
return true;
|
|
3866
|
+
}
|
|
3867
|
+
if (char === ">") {
|
|
3868
|
+
return false;
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
return false;
|
|
3872
|
+
}
|
|
3873
|
+
function assertSafePlist(content, filePath) {
|
|
3874
|
+
if (/<!ENTITY/i.test(content)) {
|
|
3875
|
+
throw new AdapterError(
|
|
3876
|
+
"INVALID_XML",
|
|
3877
|
+
`${filePath}: the plist contains an <!ENTITY> declaration, which is rejected before parsing.`
|
|
3878
|
+
);
|
|
3879
|
+
}
|
|
3880
|
+
if (doctypeHasInternalSubset(content)) {
|
|
3881
|
+
throw new AdapterError(
|
|
3882
|
+
"INVALID_XML",
|
|
3883
|
+
`${filePath}: the plist's <!DOCTYPE> declares an internal subset, which is rejected before parsing.`
|
|
3884
|
+
);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
function onFatal2(level) {
|
|
3888
|
+
if (level === "fatalError") {
|
|
3889
|
+
throw new Error("malformed XML");
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
function parsePlistRoot(content, filePath) {
|
|
3893
|
+
assertSafePlist(content, filePath);
|
|
3894
|
+
let doc;
|
|
3895
|
+
try {
|
|
3896
|
+
doc = new xmldom.DOMParser({ onError: onFatal2 }).parseFromString(content, "text/xml");
|
|
3897
|
+
} catch {
|
|
3898
|
+
throw new AdapterError("INVALID_XML", `${filePath}: the file is not valid XML.`);
|
|
3899
|
+
}
|
|
3900
|
+
const plistEl = doc.documentElement;
|
|
3901
|
+
if (plistEl === null || plistEl.localName !== "plist") {
|
|
3902
|
+
throw new AdapterError("INVALID_STRUCTURE", `${filePath}: the file is not a plist document.`);
|
|
3903
|
+
}
|
|
3904
|
+
const root = elementChildren2(plistEl).find((el) => el.localName === "dict");
|
|
3905
|
+
if (root === void 0) {
|
|
3906
|
+
throw new AdapterError("INVALID_STRUCTURE", `${filePath}: the plist has no top-level <dict>.`);
|
|
3907
|
+
}
|
|
3908
|
+
return { doc, root };
|
|
3909
|
+
}
|
|
3910
|
+
function dictPairs(dict, filePath, where) {
|
|
3911
|
+
const children = elementChildren2(dict);
|
|
3912
|
+
const pairs = [];
|
|
3913
|
+
for (let i = 0; i < children.length; i += 2) {
|
|
3914
|
+
const keyEl = children[i];
|
|
3915
|
+
const valueEl = children[i + 1];
|
|
3916
|
+
if (keyEl === void 0 || keyEl.localName !== "key" || valueEl === void 0) {
|
|
3917
|
+
throw new AdapterError(
|
|
3918
|
+
"INVALID_STRUCTURE",
|
|
3919
|
+
`${filePath}: ${where} has a <dict> with an odd or malformed key/value structure.`
|
|
3920
|
+
);
|
|
3921
|
+
}
|
|
3922
|
+
pairs.push({ keyText: keyEl.textContent ?? "", valueEl });
|
|
3923
|
+
}
|
|
3924
|
+
return pairs;
|
|
3925
|
+
}
|
|
3926
|
+
function stringValue(el, filePath, where) {
|
|
3927
|
+
if (el.localName !== "string") {
|
|
3928
|
+
throw new AdapterError("INVALID_STRUCTURE", `${filePath}: ${where} expected a <string> value.`);
|
|
3929
|
+
}
|
|
3930
|
+
return el.textContent ?? "";
|
|
3931
|
+
}
|
|
3932
|
+
function parseRuleDict(rulePairs, filePath, pluralKey, variableName) {
|
|
3933
|
+
const specPair = rulePairs.find((pair) => pair.keyText === SPEC_TYPE_KEY);
|
|
3934
|
+
const specType = specPair === void 0 ? void 0 : stringValue(specPair.valueEl, filePath, `${pluralKey}'s ${SPEC_TYPE_KEY}`);
|
|
3935
|
+
if (specType !== PLURAL_RULE_TYPE) {
|
|
3936
|
+
throw new AdapterError(
|
|
3937
|
+
"INVALID_STRUCTURE",
|
|
3938
|
+
`${filePath}: the stringsdict entry "${pluralKey}" has an unsupported ${SPEC_TYPE_KEY} (expected "${PLURAL_RULE_TYPE}", found ${specType === void 0 ? "none" : `"${specType}"`}).`
|
|
3939
|
+
);
|
|
3940
|
+
}
|
|
3941
|
+
const valueTypePair = rulePairs.find((pair) => pair.keyText === VALUE_TYPE_KEY);
|
|
3942
|
+
const valueType = valueTypePair === void 0 ? void 0 : stringValue(valueTypePair.valueEl, filePath, `${pluralKey}'s ${VALUE_TYPE_KEY}`);
|
|
3943
|
+
const categories = /* @__PURE__ */ new Map();
|
|
3944
|
+
for (const pair of rulePairs) {
|
|
3945
|
+
if (pair.keyText === SPEC_TYPE_KEY || pair.keyText === VALUE_TYPE_KEY) {
|
|
3946
|
+
continue;
|
|
3947
|
+
}
|
|
3948
|
+
if (!PLURAL_CATEGORY_SET.has(pair.keyText)) {
|
|
3949
|
+
throw new AdapterError(
|
|
3950
|
+
"INVALID_STRUCTURE",
|
|
3951
|
+
`${filePath}: the stringsdict entry "${pluralKey}" has an unsupported plural category "${pair.keyText}" (variable "${variableName}"); expected one of zero, one, two, few, many, other.`
|
|
3952
|
+
);
|
|
3953
|
+
}
|
|
3954
|
+
categories.set(
|
|
3955
|
+
pair.keyText,
|
|
3956
|
+
stringValue(pair.valueEl, filePath, `${pluralKey}'s "${pair.keyText}" category`)
|
|
3957
|
+
);
|
|
3958
|
+
}
|
|
3959
|
+
if (categories.size === 0) {
|
|
3960
|
+
throw new AdapterError(
|
|
3961
|
+
"INVALID_STRUCTURE",
|
|
3962
|
+
`${filePath}: the stringsdict entry "${pluralKey}" has no plural categories.`
|
|
3963
|
+
);
|
|
3964
|
+
}
|
|
3965
|
+
return { valueType, categories };
|
|
3966
|
+
}
|
|
3967
|
+
function parseEntryDict(entryDict, filePath, pluralKey) {
|
|
3968
|
+
const pairs = dictPairs(entryDict, filePath, `the stringsdict entry "${pluralKey}"`);
|
|
3969
|
+
const formatPair = pairs.find((pair) => pair.keyText === FORMAT_KEY);
|
|
3970
|
+
if (formatPair === void 0) {
|
|
3971
|
+
throw new AdapterError(
|
|
3972
|
+
"INVALID_STRUCTURE",
|
|
3973
|
+
`${filePath}: the stringsdict entry "${pluralKey}" is missing ${FORMAT_KEY}.`
|
|
3974
|
+
);
|
|
3975
|
+
}
|
|
3976
|
+
const formatKey = stringValue(
|
|
3977
|
+
formatPair.valueEl,
|
|
3978
|
+
filePath,
|
|
3979
|
+
`the stringsdict entry "${pluralKey}"'s ${FORMAT_KEY}`
|
|
3980
|
+
);
|
|
3981
|
+
const variableMatch = VARIABLE_REF.exec(formatKey);
|
|
3982
|
+
if (variableMatch === null) {
|
|
3983
|
+
throw new AdapterError(
|
|
3984
|
+
"INVALID_STRUCTURE",
|
|
3985
|
+
`${filePath}: the stringsdict entry "${pluralKey}" is missing a plural rule: its ${FORMAT_KEY} does not reference a %#@variable@ substitution.`
|
|
3986
|
+
);
|
|
3987
|
+
}
|
|
3988
|
+
const variableName = variableMatch[1];
|
|
3989
|
+
const rulePair = pairs.find((pair) => pair.keyText === variableName);
|
|
3990
|
+
if (rulePair === void 0) {
|
|
3991
|
+
throw new AdapterError(
|
|
3992
|
+
"INVALID_STRUCTURE",
|
|
3993
|
+
`${filePath}: the stringsdict entry "${pluralKey}" references variable "${variableName}" in its ${FORMAT_KEY} but has no matching rule dictionary.`
|
|
3994
|
+
);
|
|
3995
|
+
}
|
|
3996
|
+
if (rulePair.valueEl.localName !== "dict") {
|
|
3997
|
+
throw new AdapterError(
|
|
3998
|
+
"INVALID_STRUCTURE",
|
|
3999
|
+
`${filePath}: the stringsdict entry "${pluralKey}"'s "${variableName}" rule must be a <dict>.`
|
|
4000
|
+
);
|
|
4001
|
+
}
|
|
4002
|
+
const rulePairs = dictPairs(
|
|
4003
|
+
rulePair.valueEl,
|
|
4004
|
+
filePath,
|
|
4005
|
+
`the stringsdict entry "${pluralKey}"'s "${variableName}" rule`
|
|
4006
|
+
);
|
|
4007
|
+
const { valueType, categories } = parseRuleDict(rulePairs, filePath, pluralKey, variableName);
|
|
4008
|
+
return { formatKey, variableName, valueType, categories };
|
|
4009
|
+
}
|
|
4010
|
+
function parseStringsDictGroups(content, filePath) {
|
|
4011
|
+
const { root } = parsePlistRoot(content, filePath);
|
|
4012
|
+
const pairs = dictPairs(root, filePath, "the stringsdict document");
|
|
4013
|
+
const out = /* @__PURE__ */ new Map();
|
|
4014
|
+
for (const pair of pairs) {
|
|
4015
|
+
const pluralKey = pair.keyText;
|
|
4016
|
+
if (pair.valueEl.localName !== "dict") {
|
|
4017
|
+
throw new AdapterError(
|
|
4018
|
+
"INVALID_STRUCTURE",
|
|
4019
|
+
`${filePath}: the stringsdict entry "${pluralKey}" must be a <dict>.`
|
|
4020
|
+
);
|
|
4021
|
+
}
|
|
4022
|
+
if (out.has(pluralKey)) {
|
|
4023
|
+
throw new AdapterError(
|
|
4024
|
+
"INVALID_STRUCTURE",
|
|
4025
|
+
`${filePath}: the stringsdict file has two entries with the key "${pluralKey}".`
|
|
4026
|
+
);
|
|
4027
|
+
}
|
|
4028
|
+
out.set(pluralKey, parseEntryDict(pair.valueEl, filePath, pluralKey));
|
|
4029
|
+
}
|
|
4030
|
+
return out;
|
|
4031
|
+
}
|
|
4032
|
+
function parseAppleStringsDictEntries(content, namespace, filePath) {
|
|
4033
|
+
const groups = parseStringsDictGroups(content, filePath);
|
|
4034
|
+
const out = /* @__PURE__ */ new Map();
|
|
4035
|
+
for (const [pluralKey, group] of groups) {
|
|
4036
|
+
for (const category of PLURAL_CATEGORIES) {
|
|
4037
|
+
const text = group.categories.get(category);
|
|
4038
|
+
if (text === void 0) {
|
|
4039
|
+
continue;
|
|
4040
|
+
}
|
|
4041
|
+
const key = makePluralKey(pluralKey, category);
|
|
4042
|
+
out.set(key, {
|
|
4043
|
+
key,
|
|
4044
|
+
namespace,
|
|
4045
|
+
value: text,
|
|
4046
|
+
placeholders: extractAppleStringsPlaceholders(text),
|
|
4047
|
+
isPlural: true
|
|
4048
|
+
});
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
return out;
|
|
4052
|
+
}
|
|
4053
|
+
async function readExistingGroups(filePath, fs, strict) {
|
|
4054
|
+
try {
|
|
4055
|
+
const outcome = await readBoundedFile(fs, filePath);
|
|
4056
|
+
const content = outcomeToContent(
|
|
4057
|
+
outcome,
|
|
4058
|
+
`${filePath}: the destination path is not a regular file.`
|
|
4059
|
+
);
|
|
4060
|
+
return parseStringsDictGroups(content, filePath);
|
|
4061
|
+
} catch (error) {
|
|
4062
|
+
if (isEnoent(error) || !strict) {
|
|
4063
|
+
return /* @__PURE__ */ new Map();
|
|
4064
|
+
}
|
|
4065
|
+
if (error instanceof AdapterError) {
|
|
4066
|
+
throw error;
|
|
4067
|
+
}
|
|
4068
|
+
throw new AdapterError(
|
|
4069
|
+
"INVALID_STRUCTURE",
|
|
4070
|
+
`${filePath}: the destination stringsdict file could not be read.`
|
|
4071
|
+
);
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
function groupTargets(entries) {
|
|
4075
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4076
|
+
for (const [key, entry] of entries) {
|
|
4077
|
+
const baseKey = pluralBaseKey(key);
|
|
4078
|
+
const category = pluralCategoryOf(key);
|
|
4079
|
+
if (baseKey === void 0 || category === void 0) {
|
|
4080
|
+
continue;
|
|
4081
|
+
}
|
|
4082
|
+
const group = groups.get(baseKey) ?? /* @__PURE__ */ new Map();
|
|
4083
|
+
group.set(category, entry.value);
|
|
4084
|
+
groups.set(baseKey, group);
|
|
4085
|
+
}
|
|
4086
|
+
return groups;
|
|
4087
|
+
}
|
|
4088
|
+
var DEFAULT_VALUE_TYPE = "d";
|
|
4089
|
+
function appendTextEl(doc, parent, tag, text) {
|
|
4090
|
+
const el = doc.createElement(tag);
|
|
4091
|
+
el.appendChild(doc.createTextNode(text));
|
|
4092
|
+
parent.appendChild(el);
|
|
4093
|
+
}
|
|
4094
|
+
function buildEntryPair(doc, baseKey, entry, categories) {
|
|
4095
|
+
const keyEl = doc.createElement("key");
|
|
4096
|
+
keyEl.appendChild(doc.createTextNode(baseKey));
|
|
4097
|
+
const entryDict = doc.createElement("dict");
|
|
4098
|
+
appendTextEl(doc, entryDict, "key", FORMAT_KEY);
|
|
4099
|
+
appendTextEl(doc, entryDict, "string", entry.formatKey);
|
|
4100
|
+
appendTextEl(doc, entryDict, "key", entry.variableName);
|
|
4101
|
+
const ruleDict = doc.createElement("dict");
|
|
4102
|
+
appendTextEl(doc, ruleDict, "key", SPEC_TYPE_KEY);
|
|
4103
|
+
appendTextEl(doc, ruleDict, "string", PLURAL_RULE_TYPE);
|
|
4104
|
+
appendTextEl(doc, ruleDict, "key", VALUE_TYPE_KEY);
|
|
4105
|
+
appendTextEl(doc, ruleDict, "string", entry.valueType ?? DEFAULT_VALUE_TYPE);
|
|
4106
|
+
for (const category of PLURAL_CATEGORIES) {
|
|
4107
|
+
const text = categories.get(category);
|
|
4108
|
+
if (text === void 0) {
|
|
4109
|
+
continue;
|
|
4110
|
+
}
|
|
4111
|
+
appendTextEl(doc, ruleDict, "key", category);
|
|
4112
|
+
appendTextEl(doc, ruleDict, "string", text);
|
|
4113
|
+
}
|
|
4114
|
+
entryDict.appendChild(ruleDict);
|
|
4115
|
+
return { keyEl, dictEl: entryDict };
|
|
4116
|
+
}
|
|
4117
|
+
function synthesizeEntry(baseKey) {
|
|
4118
|
+
return {
|
|
4119
|
+
formatKey: `%#@${baseKey}@`,
|
|
4120
|
+
variableName: baseKey,
|
|
4121
|
+
valueType: void 0,
|
|
4122
|
+
categories: /* @__PURE__ */ new Map()
|
|
4123
|
+
};
|
|
4124
|
+
}
|
|
4125
|
+
async function serializeAppleStringsDictEntries(entries, filePath, fs) {
|
|
4126
|
+
const targets = groupTargets(entries);
|
|
4127
|
+
const existing = await readExistingGroups(filePath, fs, targets.size > 0);
|
|
4128
|
+
if (targets.size === 0 && existing.size === 0) {
|
|
4129
|
+
return void 0;
|
|
4130
|
+
}
|
|
4131
|
+
const doc = new xmldom.DOMParser().parseFromString('<plist version="1.0"><dict/></plist>', "text/xml");
|
|
4132
|
+
const plistEl = doc.documentElement;
|
|
4133
|
+
const dictEl = elementChildren2(plistEl)[0];
|
|
4134
|
+
const orderedKeys = [
|
|
4135
|
+
...[...existing.keys()].filter((key) => targets.has(key)),
|
|
4136
|
+
...[...targets.keys()].filter((key) => !existing.has(key))
|
|
4137
|
+
];
|
|
4138
|
+
for (const baseKey of orderedKeys) {
|
|
4139
|
+
const categories = targets.get(baseKey);
|
|
4140
|
+
if (categories === void 0) {
|
|
4141
|
+
continue;
|
|
4142
|
+
}
|
|
4143
|
+
const base = existing.get(baseKey) ?? synthesizeEntry(baseKey);
|
|
4144
|
+
const { keyEl, dictEl: entryDictEl } = buildEntryPair(doc, baseKey, base, categories);
|
|
4145
|
+
dictEl.appendChild(keyEl);
|
|
4146
|
+
dictEl.appendChild(entryDictEl);
|
|
4147
|
+
}
|
|
4148
|
+
return `${PLIST_HEADER}${new xmldom.XMLSerializer().serializeToString(doc)}`;
|
|
4149
|
+
}
|
|
4150
|
+
function stringsDictSiblingPath(filePath) {
|
|
4151
|
+
return path.join(path.dirname(filePath), `${path.basename(filePath, path.extname(filePath))}.stringsdict`);
|
|
4152
|
+
}
|
|
4153
|
+
function partitionEntries(entries) {
|
|
4154
|
+
const singular = /* @__PURE__ */ new Map();
|
|
4155
|
+
const plural = /* @__PURE__ */ new Map();
|
|
4156
|
+
for (const [key, entry] of entries) {
|
|
4157
|
+
(entry.isPlural ? plural : singular).set(key, entry);
|
|
4158
|
+
}
|
|
4159
|
+
return { singular, plural };
|
|
4160
|
+
}
|
|
4161
|
+
async function parseAppleEntries(content, namespace, filePath, fs) {
|
|
4162
|
+
const singular = parseAppleStringsEntries(content, namespace);
|
|
4163
|
+
const siblingPath = stringsDictSiblingPath(filePath);
|
|
4164
|
+
let siblingContent;
|
|
4165
|
+
try {
|
|
4166
|
+
siblingContent = await readFileContent(fs, siblingPath);
|
|
4167
|
+
} catch (error) {
|
|
4168
|
+
if (isEnoent(error)) {
|
|
4169
|
+
return singular;
|
|
4170
|
+
}
|
|
4171
|
+
throw error;
|
|
4172
|
+
}
|
|
4173
|
+
const plural = parseAppleStringsDictEntries(siblingContent, namespace, siblingPath);
|
|
4174
|
+
const merged = new Map(singular);
|
|
4175
|
+
for (const [key, entry] of plural) {
|
|
4176
|
+
if (merged.has(key)) {
|
|
4177
|
+
throw new AdapterError(
|
|
4178
|
+
"INVALID_STRUCTURE",
|
|
4179
|
+
`The key "${key}" is defined both in ${filePath} and as a plural category in ${siblingPath}. Rename one of them to avoid ambiguity.`
|
|
4180
|
+
);
|
|
4181
|
+
}
|
|
4182
|
+
merged.set(key, entry);
|
|
4183
|
+
}
|
|
4184
|
+
return merged;
|
|
4185
|
+
}
|
|
4186
|
+
async function serializeAppleEntries(entries, filePath, fs) {
|
|
4187
|
+
const { singular, plural } = partitionEntries(entries);
|
|
4188
|
+
const siblingPath = stringsDictSiblingPath(filePath);
|
|
4189
|
+
const xml = await serializeAppleStringsDictEntries(plural, siblingPath, fs);
|
|
4190
|
+
if (xml !== void 0) {
|
|
4191
|
+
await fs.writeFileAtomic(siblingPath, xml);
|
|
4192
|
+
}
|
|
4193
|
+
return serializeAppleStringsEntries(singular, filePath, fs);
|
|
4194
|
+
}
|
|
4195
|
+
function createAppleStringsAdapter(fs = nodeAdapterFs) {
|
|
4196
|
+
return createFlatFileAdapter({
|
|
4197
|
+
fs,
|
|
4198
|
+
format: "apple-strings",
|
|
4199
|
+
extensions: [".strings"],
|
|
4200
|
+
parseEntries: parseAppleEntries,
|
|
4201
|
+
serializeEntries: serializeAppleEntries,
|
|
4202
|
+
extractPlaceholders: extractAppleStringsPlaceholders
|
|
4203
|
+
});
|
|
4204
|
+
}
|
|
4205
|
+
var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
|
|
4206
|
+
var INVALID = { placeholders: [], isPlural: false, valid: false };
|
|
4207
|
+
function tokenOf(element) {
|
|
4208
|
+
switch (element.type) {
|
|
4209
|
+
case icuMessageformatParser.TYPE.argument:
|
|
4210
|
+
case icuMessageformatParser.TYPE.number:
|
|
4211
|
+
case icuMessageformatParser.TYPE.date:
|
|
4212
|
+
case icuMessageformatParser.TYPE.time:
|
|
4213
|
+
case icuMessageformatParser.TYPE.select:
|
|
4214
|
+
case icuMessageformatParser.TYPE.plural:
|
|
4215
|
+
return `{${element.value}}`;
|
|
4216
|
+
case icuMessageformatParser.TYPE.tag:
|
|
4217
|
+
return `<${element.value}>`;
|
|
4218
|
+
default:
|
|
4219
|
+
return void 0;
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
function childMessages(element) {
|
|
4223
|
+
if (element.type === icuMessageformatParser.TYPE.plural || element.type === icuMessageformatParser.TYPE.select) {
|
|
4224
|
+
return Object.values(element.options).map((option) => option.value);
|
|
4225
|
+
}
|
|
4226
|
+
if (element.type === icuMessageformatParser.TYPE.tag) {
|
|
4227
|
+
return [element.children];
|
|
4228
|
+
}
|
|
4229
|
+
return [];
|
|
4230
|
+
}
|
|
4231
|
+
function addCount(target, token, count) {
|
|
4232
|
+
target.set(token, (target.get(token) ?? 0) + count);
|
|
2522
4233
|
}
|
|
2523
4234
|
function combineBranches(branchMultisets) {
|
|
2524
4235
|
const combined = /* @__PURE__ */ new Map();
|
|
@@ -2726,16 +4437,6 @@ function compareIcuPlaceholders(sourceValue, targetValue) {
|
|
|
2726
4437
|
return checkPlaceholders(icuPlaceholders(sourceValue), icuPlaceholders(targetValue));
|
|
2727
4438
|
}
|
|
2728
4439
|
}
|
|
2729
|
-
var AdapterError = class extends Error {
|
|
2730
|
-
code;
|
|
2731
|
-
constructor(code, message) {
|
|
2732
|
-
super(message);
|
|
2733
|
-
this.name = "AdapterError";
|
|
2734
|
-
this.code = code;
|
|
2735
|
-
}
|
|
2736
|
-
};
|
|
2737
|
-
var MAX_DEPTH = 100;
|
|
2738
|
-
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
2739
4440
|
var SENTINEL_ESCAPE = "\\u0001";
|
|
2740
4441
|
function isJsonWhitespace(char) {
|
|
2741
4442
|
return char === " " || char === " " || char === "\n" || char === "\r";
|
|
@@ -2908,77 +4609,17 @@ function assertJsonRecord(value) {
|
|
|
2908
4609
|
assertNodesValid(value);
|
|
2909
4610
|
return value;
|
|
2910
4611
|
}
|
|
2911
|
-
function parseJsonObject(content) {
|
|
2912
|
-
return assertJsonRecord(parseOrderedJson(content));
|
|
2913
|
-
}
|
|
2914
|
-
function serializeJsonTree(tree) {
|
|
2915
|
-
return serializeOrderedJson(tree);
|
|
2916
|
-
}
|
|
2917
|
-
function isJsonNode(value) {
|
|
2918
|
-
return value instanceof Map;
|
|
2919
|
-
}
|
|
2920
|
-
function sniffJsonObject(sample) {
|
|
2921
|
-
return sample.trimStart().startsWith("{");
|
|
2922
|
-
}
|
|
2923
|
-
function scanTokens(value, pattern, extract = (match) => match[0]) {
|
|
2924
|
-
const result = [];
|
|
2925
|
-
for (const match of value.matchAll(pattern)) {
|
|
2926
|
-
const token = extract(match);
|
|
2927
|
-
if (token !== void 0) {
|
|
2928
|
-
result.push(token);
|
|
2929
|
-
}
|
|
2930
|
-
}
|
|
2931
|
-
return result;
|
|
2932
|
-
}
|
|
2933
|
-
function namespaceOf(filePath) {
|
|
2934
|
-
return path.basename(filePath, path.extname(filePath));
|
|
2935
|
-
}
|
|
2936
|
-
function isEnoent(error) {
|
|
2937
|
-
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2938
|
-
}
|
|
2939
|
-
function rethrowStructured(error, message) {
|
|
2940
|
-
if (error instanceof AdapterError) {
|
|
2941
|
-
throw error;
|
|
2942
|
-
}
|
|
2943
|
-
throw new AdapterError("INVALID_STRUCTURE", message);
|
|
2944
|
-
}
|
|
2945
|
-
function computeIcu(entries, compute) {
|
|
2946
|
-
if (!compute) {
|
|
2947
|
-
return [];
|
|
2948
|
-
}
|
|
2949
|
-
try {
|
|
2950
|
-
return compute(entries);
|
|
2951
|
-
} catch (error) {
|
|
2952
|
-
rethrowStructured(error, "The file could not be analyzed for message validity.");
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
function buildCanHandle(extensions, sniff) {
|
|
2956
|
-
return (filePath, sample) => {
|
|
2957
|
-
if (!extensions.includes(path.extname(filePath).toLowerCase())) {
|
|
2958
|
-
return false;
|
|
2959
|
-
}
|
|
2960
|
-
return sample === void 0 || sniff === void 0 || sniff(sample);
|
|
2961
|
-
};
|
|
2962
|
-
}
|
|
2963
|
-
function stripLeadingBom(content) {
|
|
2964
|
-
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
4612
|
+
function parseJsonObject(content) {
|
|
4613
|
+
return assertJsonRecord(parseOrderedJson(content));
|
|
2965
4614
|
}
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
return outcome.kind === "ok" ? { kind: "ok", content: stripLeadingBom(outcome.content) } : outcome;
|
|
4615
|
+
function serializeJsonTree(tree) {
|
|
4616
|
+
return serializeOrderedJson(tree);
|
|
2969
4617
|
}
|
|
2970
|
-
function
|
|
2971
|
-
|
|
2972
|
-
throw new AdapterError("INVALID_STRUCTURE", notAFileMessage);
|
|
2973
|
-
}
|
|
2974
|
-
if (outcome.kind === "too-large") {
|
|
2975
|
-
throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
|
|
2976
|
-
}
|
|
2977
|
-
return outcome.content;
|
|
4618
|
+
function isJsonNode(value) {
|
|
4619
|
+
return value instanceof Map;
|
|
2978
4620
|
}
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
return outcomeToContent(outcome, "The path is not a regular file.");
|
|
4621
|
+
function sniffJsonObject(sample) {
|
|
4622
|
+
return sample.trimStart().startsWith("{");
|
|
2982
4623
|
}
|
|
2983
4624
|
var BACKSLASH = "\\";
|
|
2984
4625
|
var DOT = ".";
|
|
@@ -3162,7 +4803,7 @@ function mergeDescriptions(entries, content, deriveDescriptions) {
|
|
|
3162
4803
|
}
|
|
3163
4804
|
}
|
|
3164
4805
|
}
|
|
3165
|
-
function
|
|
4806
|
+
function toEntries2(content, namespace, parse3, deriveEntry, keyMode, validateTree, deriveDescriptions) {
|
|
3166
4807
|
try {
|
|
3167
4808
|
const tree = parse3(content);
|
|
3168
4809
|
validateTree?.(tree);
|
|
@@ -3200,7 +4841,7 @@ function createTreeFileAdapter(options) {
|
|
|
3200
4841
|
async read(filePath, locale) {
|
|
3201
4842
|
const content = await readFileContent(fs, filePath);
|
|
3202
4843
|
const namespace = namespaceOf(filePath);
|
|
3203
|
-
const { entries, excludedLeafPaths } =
|
|
4844
|
+
const { entries, excludedLeafPaths } = toEntries2(
|
|
3204
4845
|
content,
|
|
3205
4846
|
namespace,
|
|
3206
4847
|
parse3,
|
|
@@ -3219,114 +4860,781 @@ function createTreeFileAdapter(options) {
|
|
|
3219
4860
|
}
|
|
3220
4861
|
};
|
|
3221
4862
|
}
|
|
3222
|
-
function isMetadataKey(key) {
|
|
3223
|
-
return key.startsWith("@");
|
|
4863
|
+
function isMetadataKey(key) {
|
|
4864
|
+
return key.startsWith("@");
|
|
4865
|
+
}
|
|
4866
|
+
function parseArbObject(content) {
|
|
4867
|
+
const parsed = parseOrderedJson(content);
|
|
4868
|
+
if (!(parsed instanceof Map)) {
|
|
4869
|
+
throw new AdapterError(
|
|
4870
|
+
"INVALID_STRUCTURE",
|
|
4871
|
+
"The file is not a valid object (expected nested objects of string values)."
|
|
4872
|
+
);
|
|
4873
|
+
}
|
|
4874
|
+
return parsed;
|
|
4875
|
+
}
|
|
4876
|
+
function stripArbMetadata(tree) {
|
|
4877
|
+
const out = /* @__PURE__ */ new Map();
|
|
4878
|
+
for (const [key, value] of tree) {
|
|
4879
|
+
if (!isMetadataKey(key)) {
|
|
4880
|
+
out.set(key, value);
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
return out;
|
|
4884
|
+
}
|
|
4885
|
+
function originalKey(encoded) {
|
|
4886
|
+
return decodeKeyToSegments(encoded).join(".");
|
|
4887
|
+
}
|
|
4888
|
+
function messageKeyForMetadata(key) {
|
|
4889
|
+
return isMetadataKey(key) && !key.startsWith("@@") ? key.slice(1) : null;
|
|
4890
|
+
}
|
|
4891
|
+
function descriptionOf(value) {
|
|
4892
|
+
if (!(value instanceof Map)) {
|
|
4893
|
+
return void 0;
|
|
4894
|
+
}
|
|
4895
|
+
const description = value.get("description");
|
|
4896
|
+
return typeof description === "string" ? description : void 0;
|
|
4897
|
+
}
|
|
4898
|
+
function extractArbDescriptions(content) {
|
|
4899
|
+
const tree = parseArbObject(content);
|
|
4900
|
+
const out = /* @__PURE__ */ new Map();
|
|
4901
|
+
for (const [key, value] of tree) {
|
|
4902
|
+
const messageKey = messageKeyForMetadata(key);
|
|
4903
|
+
if (messageKey === null) {
|
|
4904
|
+
continue;
|
|
4905
|
+
}
|
|
4906
|
+
const description = descriptionOf(value);
|
|
4907
|
+
if (description !== void 0) {
|
|
4908
|
+
out.set(encodeSegment(messageKey), description);
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
return out;
|
|
4912
|
+
}
|
|
4913
|
+
function messagesFromEntries(entries) {
|
|
4914
|
+
const out = /* @__PURE__ */ new Map();
|
|
4915
|
+
for (const [key, entry] of entries) {
|
|
4916
|
+
out.set(originalKey(key), entry.value);
|
|
4917
|
+
}
|
|
4918
|
+
return out;
|
|
4919
|
+
}
|
|
4920
|
+
async function readDestinationPairs(filePath, fs) {
|
|
4921
|
+
let content;
|
|
4922
|
+
try {
|
|
4923
|
+
content = await readFileContent(fs, filePath);
|
|
4924
|
+
} catch (error) {
|
|
4925
|
+
if (isEnoent(error)) {
|
|
4926
|
+
return null;
|
|
4927
|
+
}
|
|
4928
|
+
throw error;
|
|
4929
|
+
}
|
|
4930
|
+
return [...parseArbObject(content)];
|
|
4931
|
+
}
|
|
4932
|
+
async function buildArbWriteTree(entries, filePath, fs) {
|
|
4933
|
+
const messages = messagesFromEntries(entries);
|
|
4934
|
+
const pairs = await readDestinationPairs(filePath, fs);
|
|
4935
|
+
const out = /* @__PURE__ */ new Map();
|
|
4936
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
4937
|
+
for (const [key, value] of pairs ?? []) {
|
|
4938
|
+
const translated = isMetadataKey(key) ? void 0 : messages.get(key);
|
|
4939
|
+
if (translated !== void 0) {
|
|
4940
|
+
consumed.add(key);
|
|
4941
|
+
out.set(key, translated);
|
|
4942
|
+
} else if (isMetadataKey(key) || typeof value === "string") {
|
|
4943
|
+
out.set(key, value);
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
for (const [key, value] of messages) {
|
|
4947
|
+
if (!consumed.has(key)) {
|
|
4948
|
+
out.set(key, value);
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
return out;
|
|
4952
|
+
}
|
|
4953
|
+
function parseArb(content) {
|
|
4954
|
+
return assertJsonRecord(stripArbMetadata(parseArbObject(content)));
|
|
4955
|
+
}
|
|
4956
|
+
function createArbAdapter(fs = nodeAdapterFs) {
|
|
4957
|
+
return createTreeFileAdapter({
|
|
4958
|
+
fs,
|
|
4959
|
+
format: "arb",
|
|
4960
|
+
extensions: [".arb"],
|
|
4961
|
+
sniff: sniffJsonObject,
|
|
4962
|
+
parse: parseArb,
|
|
4963
|
+
serialize: serializeJsonTree,
|
|
4964
|
+
extractPlaceholders: icuPlaceholders,
|
|
4965
|
+
deriveEntry: icuDeriveEntry,
|
|
4966
|
+
computeInvalidIcuKeys: icuInvalidKeys,
|
|
4967
|
+
validateMessage: icuIsValid,
|
|
4968
|
+
comparePlaceholders: compareIcuPlaceholders,
|
|
4969
|
+
buildWriteTree: buildArbWriteTree,
|
|
4970
|
+
deriveDescriptions: extractArbDescriptions
|
|
4971
|
+
});
|
|
4972
|
+
}
|
|
4973
|
+
var SIMPLE_ESCAPES = {
|
|
4974
|
+
n: "\n",
|
|
4975
|
+
t: " ",
|
|
4976
|
+
r: "\r",
|
|
4977
|
+
a: "\x07",
|
|
4978
|
+
b: "\b",
|
|
4979
|
+
f: "\f",
|
|
4980
|
+
v: "\v",
|
|
4981
|
+
"0": "\0",
|
|
4982
|
+
'"': '"',
|
|
4983
|
+
"\\": "\\"
|
|
4984
|
+
};
|
|
4985
|
+
var OCTAL_DIGIT = /[0-7]/;
|
|
4986
|
+
function decodeHexEscape(raw, at, line) {
|
|
4987
|
+
let hex = "";
|
|
4988
|
+
let i = at;
|
|
4989
|
+
while (i < raw.length && hex.length < 2 && /[0-9a-fA-F]/.test(raw[i] ?? "")) {
|
|
4990
|
+
hex += raw[i];
|
|
4991
|
+
i += 1;
|
|
4992
|
+
}
|
|
4993
|
+
if (hex === "") {
|
|
4994
|
+
throw new AdapterError(
|
|
4995
|
+
"INVALID_STRUCTURE",
|
|
4996
|
+
`The .po/.pot file has a malformed \\x escape (line ${line}).`
|
|
4997
|
+
);
|
|
4998
|
+
}
|
|
4999
|
+
return { char: String.fromCharCode(Number.parseInt(hex, 16)), next: i };
|
|
5000
|
+
}
|
|
5001
|
+
function decodeOctalEscape(raw, at) {
|
|
5002
|
+
let octal = "";
|
|
5003
|
+
let i = at;
|
|
5004
|
+
while (i < raw.length && octal.length < 3 && OCTAL_DIGIT.test(raw[i] ?? "")) {
|
|
5005
|
+
octal += raw[i];
|
|
5006
|
+
i += 1;
|
|
5007
|
+
}
|
|
5008
|
+
return { char: String.fromCharCode(Number.parseInt(octal, 8)), next: i };
|
|
5009
|
+
}
|
|
5010
|
+
function decodeCString(raw, line) {
|
|
5011
|
+
let out = "";
|
|
5012
|
+
let i = 0;
|
|
5013
|
+
while (i < raw.length) {
|
|
5014
|
+
const char = raw[i];
|
|
5015
|
+
if (char !== "\\") {
|
|
5016
|
+
out += char;
|
|
5017
|
+
i += 1;
|
|
5018
|
+
continue;
|
|
5019
|
+
}
|
|
5020
|
+
const next = raw[i + 1];
|
|
5021
|
+
if (next === void 0) {
|
|
5022
|
+
throw new AdapterError(
|
|
5023
|
+
"INVALID_STRUCTURE",
|
|
5024
|
+
`The .po/.pot file has a trailing, unterminated escape (line ${line}).`
|
|
5025
|
+
);
|
|
5026
|
+
}
|
|
5027
|
+
if (next === "x") {
|
|
5028
|
+
const hex = decodeHexEscape(raw, i + 2, line);
|
|
5029
|
+
out += hex.char;
|
|
5030
|
+
i = hex.next;
|
|
5031
|
+
continue;
|
|
5032
|
+
}
|
|
5033
|
+
if (OCTAL_DIGIT.test(next)) {
|
|
5034
|
+
const octal = decodeOctalEscape(raw, i + 1);
|
|
5035
|
+
out += octal.char;
|
|
5036
|
+
i = octal.next;
|
|
5037
|
+
continue;
|
|
5038
|
+
}
|
|
5039
|
+
const simple = SIMPLE_ESCAPES[next];
|
|
5040
|
+
if (simple === void 0) {
|
|
5041
|
+
throw new AdapterError(
|
|
5042
|
+
"INVALID_STRUCTURE",
|
|
5043
|
+
`The .po/.pot file has an unknown escape sequence "\\${next}" (line ${line}).`
|
|
5044
|
+
);
|
|
5045
|
+
}
|
|
5046
|
+
out += simple;
|
|
5047
|
+
i += 2;
|
|
5048
|
+
}
|
|
5049
|
+
return out;
|
|
5050
|
+
}
|
|
5051
|
+
function escapeChar3(char) {
|
|
5052
|
+
switch (char) {
|
|
5053
|
+
case "\\":
|
|
5054
|
+
return "\\\\";
|
|
5055
|
+
case '"':
|
|
5056
|
+
return '\\"';
|
|
5057
|
+
case "\n":
|
|
5058
|
+
return "\\n";
|
|
5059
|
+
case " ":
|
|
5060
|
+
return "\\t";
|
|
5061
|
+
case "\r":
|
|
5062
|
+
return "\\r";
|
|
5063
|
+
}
|
|
5064
|
+
const code = char.charCodeAt(0);
|
|
5065
|
+
return code < 32 ? `\\x${code.toString(16).padStart(2, "0")}` : char;
|
|
5066
|
+
}
|
|
5067
|
+
function encodeCString(value) {
|
|
5068
|
+
let out = "";
|
|
5069
|
+
for (const char of value) {
|
|
5070
|
+
out += escapeChar3(char);
|
|
5071
|
+
}
|
|
5072
|
+
return out;
|
|
5073
|
+
}
|
|
5074
|
+
var CONTEXT_SEPARATOR = "\uE000";
|
|
5075
|
+
var PLURAL_SUFFIX2 = /^\[(\d+)\]$/;
|
|
5076
|
+
function assertNoReservedChar(raw, field) {
|
|
5077
|
+
if (raw.includes(CONTEXT_SEPARATOR)) {
|
|
5078
|
+
throw new AdapterError(
|
|
5079
|
+
"INVALID_STRUCTURE",
|
|
5080
|
+
`The ${field} value contains a reserved private-use character (U+E000) that verbatra uses internally to combine msgctxt and msgid into one key. Remove it from the source file.`
|
|
5081
|
+
);
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
function escapeBase(raw) {
|
|
5085
|
+
return raw.replace(/\\/g, "\\\\").replace(/\[/g, "\\[");
|
|
5086
|
+
}
|
|
5087
|
+
function unescapeBase(escaped) {
|
|
5088
|
+
let out = "";
|
|
5089
|
+
let i = 0;
|
|
5090
|
+
while (i < escaped.length) {
|
|
5091
|
+
const char = escaped[i];
|
|
5092
|
+
if (char === "\\" && i + 1 < escaped.length) {
|
|
5093
|
+
out += escaped[i + 1];
|
|
5094
|
+
i += 2;
|
|
5095
|
+
continue;
|
|
5096
|
+
}
|
|
5097
|
+
out += char;
|
|
5098
|
+
i += 1;
|
|
5099
|
+
}
|
|
5100
|
+
return out;
|
|
5101
|
+
}
|
|
5102
|
+
function findUnescapedBracket(base) {
|
|
5103
|
+
let i = 0;
|
|
5104
|
+
while (i < base.length) {
|
|
5105
|
+
if (base[i] === "\\") {
|
|
5106
|
+
i += 2;
|
|
5107
|
+
continue;
|
|
5108
|
+
}
|
|
5109
|
+
if (base[i] === "[") {
|
|
5110
|
+
return i;
|
|
5111
|
+
}
|
|
5112
|
+
i += 1;
|
|
5113
|
+
}
|
|
5114
|
+
return -1;
|
|
5115
|
+
}
|
|
5116
|
+
function composeKey(msgctxt, msgid, pluralIndex) {
|
|
5117
|
+
assertNoReservedChar(msgid, "msgid");
|
|
5118
|
+
if (msgctxt !== void 0) {
|
|
5119
|
+
assertNoReservedChar(msgctxt, "msgctxt");
|
|
5120
|
+
}
|
|
5121
|
+
const base = msgctxt === void 0 ? escapeBase(msgid) : `${escapeBase(msgctxt)}${CONTEXT_SEPARATOR}${escapeBase(msgid)}`;
|
|
5122
|
+
return pluralIndex === void 0 ? base : `${base}[${pluralIndex}]`;
|
|
5123
|
+
}
|
|
5124
|
+
function decomposeKey(key) {
|
|
5125
|
+
const bracketAt = findUnescapedBracket(key);
|
|
5126
|
+
let base = key;
|
|
5127
|
+
let pluralIndex;
|
|
5128
|
+
if (bracketAt !== -1) {
|
|
5129
|
+
const suffix = key.slice(bracketAt);
|
|
5130
|
+
const match = PLURAL_SUFFIX2.exec(suffix);
|
|
5131
|
+
if (!match) {
|
|
5132
|
+
throw new AdapterError(
|
|
5133
|
+
"INVALID_STRUCTURE",
|
|
5134
|
+
`The key "${key}" has a malformed plural-index suffix.`
|
|
5135
|
+
);
|
|
5136
|
+
}
|
|
5137
|
+
base = key.slice(0, bracketAt);
|
|
5138
|
+
pluralIndex = Number(match[1]);
|
|
5139
|
+
}
|
|
5140
|
+
const separatorAt = base.indexOf(CONTEXT_SEPARATOR);
|
|
5141
|
+
if (separatorAt === -1) {
|
|
5142
|
+
return { msgctxt: void 0, msgid: unescapeBase(base), pluralIndex };
|
|
5143
|
+
}
|
|
5144
|
+
return {
|
|
5145
|
+
msgctxt: unescapeBase(base.slice(0, separatorAt)),
|
|
5146
|
+
msgid: unescapeBase(base.slice(separatorAt + 1)),
|
|
5147
|
+
pluralIndex
|
|
5148
|
+
};
|
|
5149
|
+
}
|
|
5150
|
+
var GETTEXT_PLACEHOLDER = /%(?:\((\w+)\))?(?:(\d+)\$)?[-+0 #]*(?:[1-9]\d*)?(?:\.\d+)?([disuoxXfFeEgGc])|%%/g;
|
|
5151
|
+
function extractGettextPlaceholders(value) {
|
|
5152
|
+
const out = [];
|
|
5153
|
+
for (const match of value.matchAll(GETTEXT_PLACEHOLDER)) {
|
|
5154
|
+
if (match[0] === "%%") {
|
|
5155
|
+
out.push("%%");
|
|
5156
|
+
continue;
|
|
5157
|
+
}
|
|
5158
|
+
const [, name, position, conversion] = match;
|
|
5159
|
+
if (conversion === void 0) {
|
|
5160
|
+
continue;
|
|
5161
|
+
}
|
|
5162
|
+
if (name !== void 0) {
|
|
5163
|
+
out.push(`%(${name})${conversion}`);
|
|
5164
|
+
} else if (position !== void 0) {
|
|
5165
|
+
out.push(`%${position}$${conversion}`);
|
|
5166
|
+
} else {
|
|
5167
|
+
out.push(`%${conversion}`);
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
return out;
|
|
5171
|
+
}
|
|
5172
|
+
var HEADER_FIELD = /^([\w-]+):\s?(.*)$/;
|
|
5173
|
+
var NPLURALS = /nplurals\s*=\s*(\d+)/;
|
|
5174
|
+
function parseHeaderFields(headerText) {
|
|
5175
|
+
const fields = /* @__PURE__ */ new Map();
|
|
5176
|
+
for (const line of headerText.split("\n")) {
|
|
5177
|
+
const match = HEADER_FIELD.exec(line);
|
|
5178
|
+
if (match?.[1] !== void 0 && match[2] !== void 0) {
|
|
5179
|
+
fields.set(match[1], match[2]);
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
return fields;
|
|
5183
|
+
}
|
|
5184
|
+
function parseNplurals(pluralFormsValue) {
|
|
5185
|
+
const match = NPLURALS.exec(pluralFormsValue);
|
|
5186
|
+
return match?.[1] === void 0 ? void 0 : Number(match[1]);
|
|
5187
|
+
}
|
|
5188
|
+
function defaultPluralFormsExpression(nplurals) {
|
|
5189
|
+
if (nplurals <= 1) {
|
|
5190
|
+
return "nplurals=1; plural=0;";
|
|
5191
|
+
}
|
|
5192
|
+
if (nplurals === 2) {
|
|
5193
|
+
return "nplurals=2; plural=(n != 1);";
|
|
5194
|
+
}
|
|
5195
|
+
return `nplurals=${nplurals}; plural=(n < ${nplurals - 1} ? n : ${nplurals - 1});`;
|
|
5196
|
+
}
|
|
5197
|
+
function joinSelfTerminated(lines, terminator) {
|
|
5198
|
+
return lines.map((line) => `${line}${terminator}`).join("");
|
|
5199
|
+
}
|
|
5200
|
+
function isCommentLine(line) {
|
|
5201
|
+
return line.trimStart().startsWith("#");
|
|
5202
|
+
}
|
|
5203
|
+
function isObsoleteLine(line) {
|
|
5204
|
+
return line.trimStart().startsWith("#~");
|
|
5205
|
+
}
|
|
5206
|
+
function isExtractedCommentLine(line) {
|
|
5207
|
+
return line.trimStart().startsWith("#.");
|
|
5208
|
+
}
|
|
5209
|
+
function extractedCommentText(line) {
|
|
5210
|
+
const rest = line.trimStart().slice(2);
|
|
5211
|
+
return rest.startsWith(" ") ? rest.slice(1) : rest;
|
|
5212
|
+
}
|
|
5213
|
+
function parseQuotedOnLine(line, lineNo) {
|
|
5214
|
+
const trimmed = line.trim();
|
|
5215
|
+
if (trimmed[0] !== '"') {
|
|
5216
|
+
throw new AdapterError(
|
|
5217
|
+
"INVALID_STRUCTURE",
|
|
5218
|
+
`The .po/.pot file expected a quoted string (line ${lineNo}).`
|
|
5219
|
+
);
|
|
5220
|
+
}
|
|
5221
|
+
let i = 1;
|
|
5222
|
+
let raw = "";
|
|
5223
|
+
while (i < trimmed.length && trimmed[i] !== '"') {
|
|
5224
|
+
if (trimmed[i] === "\\") {
|
|
5225
|
+
const next = trimmed[i + 1];
|
|
5226
|
+
if (next === void 0) {
|
|
5227
|
+
throw new AdapterError(
|
|
5228
|
+
"INVALID_STRUCTURE",
|
|
5229
|
+
`The .po/.pot file has an unterminated escape in a quoted string (line ${lineNo}).`
|
|
5230
|
+
);
|
|
5231
|
+
}
|
|
5232
|
+
raw += trimmed[i] + next;
|
|
5233
|
+
i += 2;
|
|
5234
|
+
continue;
|
|
5235
|
+
}
|
|
5236
|
+
raw += trimmed[i];
|
|
5237
|
+
i += 1;
|
|
5238
|
+
}
|
|
5239
|
+
if (trimmed[i] !== '"') {
|
|
5240
|
+
throw new AdapterError(
|
|
5241
|
+
"INVALID_STRUCTURE",
|
|
5242
|
+
`The .po/.pot file has an unterminated quoted string (line ${lineNo}).`
|
|
5243
|
+
);
|
|
5244
|
+
}
|
|
5245
|
+
if (i !== trimmed.length - 1) {
|
|
5246
|
+
throw new AdapterError(
|
|
5247
|
+
"INVALID_STRUCTURE",
|
|
5248
|
+
`The .po/.pot file has unexpected content after a quoted string (line ${lineNo}).`
|
|
5249
|
+
);
|
|
5250
|
+
}
|
|
5251
|
+
return decodeCString(raw, lineNo);
|
|
5252
|
+
}
|
|
5253
|
+
function isBareQuotedLine(line) {
|
|
5254
|
+
return line.trim().startsWith('"');
|
|
5255
|
+
}
|
|
5256
|
+
function gatherStringValue(lines, start, keyword) {
|
|
5257
|
+
const firstLine = (lines[start] ?? "").trim();
|
|
5258
|
+
const rest = firstLine.slice(keyword.length).trim();
|
|
5259
|
+
let decoded = parseQuotedOnLine(rest, start + 1);
|
|
5260
|
+
let i = start + 1;
|
|
5261
|
+
while (i < lines.length && isBareQuotedLine(lines[i] ?? "")) {
|
|
5262
|
+
decoded += parseQuotedOnLine((lines[i] ?? "").trim(), i + 1);
|
|
5263
|
+
i += 1;
|
|
5264
|
+
}
|
|
5265
|
+
return { decoded, nextIndex: i };
|
|
5266
|
+
}
|
|
5267
|
+
var MSGSTR_INDEX = /^msgstr\[(\d+)\]/;
|
|
5268
|
+
function parseLeadingComments(lines, start) {
|
|
5269
|
+
const descriptionParts = [];
|
|
5270
|
+
let i = start;
|
|
5271
|
+
while (i < lines.length && isCommentLine(lines[i] ?? "")) {
|
|
5272
|
+
const line = lines[i] ?? "";
|
|
5273
|
+
if (isExtractedCommentLine(line)) {
|
|
5274
|
+
descriptionParts.push(extractedCommentText(line));
|
|
5275
|
+
}
|
|
5276
|
+
i += 1;
|
|
5277
|
+
}
|
|
5278
|
+
return {
|
|
5279
|
+
description: descriptionParts.length > 0 ? descriptionParts.join("\n") : void 0,
|
|
5280
|
+
next: i
|
|
5281
|
+
};
|
|
5282
|
+
}
|
|
5283
|
+
function parseOptionalMsgctxt(lines, start) {
|
|
5284
|
+
if (!/^msgctxt\s+"/.test((lines[start] ?? "").trim())) {
|
|
5285
|
+
return { msgctxt: void 0, next: start };
|
|
5286
|
+
}
|
|
5287
|
+
const result = gatherStringValue(lines, start, "msgctxt");
|
|
5288
|
+
return { msgctxt: result.decoded, next: result.nextIndex };
|
|
3224
5289
|
}
|
|
3225
|
-
function
|
|
3226
|
-
const
|
|
3227
|
-
|
|
5290
|
+
function parsePluralMsgstrs(lines, start, msgid) {
|
|
5291
|
+
const values = /* @__PURE__ */ new Map();
|
|
5292
|
+
let i = start;
|
|
5293
|
+
while (/^msgstr\[\d+\]\s*"/.test((lines[i] ?? "").trim())) {
|
|
5294
|
+
const trimmed = (lines[i] ?? "").trim();
|
|
5295
|
+
const match = MSGSTR_INDEX.exec(trimmed);
|
|
5296
|
+
const index = Number(match?.[1] ?? "0");
|
|
5297
|
+
if (index !== values.size) {
|
|
5298
|
+
throw new AdapterError(
|
|
5299
|
+
"INVALID_STRUCTURE",
|
|
5300
|
+
`The .po/.pot file has a non-contiguous plural index for msgid "${msgid}": expected msgstr[${values.size}], found msgstr[${index}] (line ${i + 1}).`
|
|
5301
|
+
);
|
|
5302
|
+
}
|
|
5303
|
+
const result = gatherStringValue(lines, i, `msgstr[${index}]`);
|
|
5304
|
+
values.set(index, result.decoded);
|
|
5305
|
+
i = result.nextIndex;
|
|
5306
|
+
}
|
|
5307
|
+
if (values.size === 0) {
|
|
3228
5308
|
throw new AdapterError(
|
|
3229
5309
|
"INVALID_STRUCTURE",
|
|
3230
|
-
|
|
5310
|
+
`The .po/.pot file is missing msgstr[n] forms for the plural msgid "${msgid}" (line ${start + 1}).`
|
|
3231
5311
|
);
|
|
3232
5312
|
}
|
|
3233
|
-
return
|
|
5313
|
+
return { values, next: i };
|
|
3234
5314
|
}
|
|
3235
|
-
function
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
5315
|
+
function parseRecord(lines, start, terminator) {
|
|
5316
|
+
const comments = parseLeadingComments(lines, start);
|
|
5317
|
+
let i = comments.next;
|
|
5318
|
+
if (i >= lines.length) {
|
|
5319
|
+
throw new AdapterError(
|
|
5320
|
+
"INVALID_STRUCTURE",
|
|
5321
|
+
`The .po/.pot file ends after a comment block with no entry following it (line ${i}).`
|
|
5322
|
+
);
|
|
5323
|
+
}
|
|
5324
|
+
const ctx = parseOptionalMsgctxt(lines, i);
|
|
5325
|
+
i = ctx.next;
|
|
5326
|
+
if (!/^msgid\s+"/.test((lines[i] ?? "").trim())) {
|
|
5327
|
+
throw new AdapterError(
|
|
5328
|
+
"INVALID_STRUCTURE",
|
|
5329
|
+
`The .po/.pot file expected "msgid" (line ${i + 1}).`
|
|
5330
|
+
);
|
|
5331
|
+
}
|
|
5332
|
+
const msgidResult = gatherStringValue(lines, i, "msgid");
|
|
5333
|
+
i = msgidResult.nextIndex;
|
|
5334
|
+
let msgidPlural;
|
|
5335
|
+
if (/^msgid_plural\s+"/.test((lines[i] ?? "").trim())) {
|
|
5336
|
+
const result = gatherStringValue(lines, i, "msgid_plural");
|
|
5337
|
+
msgidPlural = result.decoded;
|
|
5338
|
+
i = result.nextIndex;
|
|
5339
|
+
}
|
|
5340
|
+
const prefixRaw = joinSelfTerminated(lines.slice(start, i), terminator);
|
|
5341
|
+
if (msgidPlural !== void 0) {
|
|
5342
|
+
const plural = parsePluralMsgstrs(lines, i, msgidResult.decoded);
|
|
5343
|
+
return {
|
|
5344
|
+
msgctxt: ctx.msgctxt,
|
|
5345
|
+
msgid: msgidResult.decoded,
|
|
5346
|
+
msgidPlural,
|
|
5347
|
+
description: comments.description,
|
|
5348
|
+
prefixRaw,
|
|
5349
|
+
singularValue: void 0,
|
|
5350
|
+
pluralValues: plural.values,
|
|
5351
|
+
entryEndIndex: plural.next
|
|
5352
|
+
};
|
|
5353
|
+
}
|
|
5354
|
+
if (!/^msgstr\s+"/.test((lines[i] ?? "").trim())) {
|
|
5355
|
+
throw new AdapterError(
|
|
5356
|
+
"INVALID_STRUCTURE",
|
|
5357
|
+
`The .po/.pot file expected "msgstr" for msgid "${msgidResult.decoded}" (line ${i + 1}).`
|
|
5358
|
+
);
|
|
5359
|
+
}
|
|
5360
|
+
const msgstrResult = gatherStringValue(lines, i, "msgstr");
|
|
5361
|
+
return {
|
|
5362
|
+
msgctxt: ctx.msgctxt,
|
|
5363
|
+
msgid: msgidResult.decoded,
|
|
5364
|
+
msgidPlural: void 0,
|
|
5365
|
+
description: comments.description,
|
|
5366
|
+
prefixRaw,
|
|
5367
|
+
singularValue: msgstrResult.decoded,
|
|
5368
|
+
pluralValues: void 0,
|
|
5369
|
+
entryEndIndex: msgstrResult.nextIndex
|
|
5370
|
+
};
|
|
5371
|
+
}
|
|
5372
|
+
function isHeaderRecord(record) {
|
|
5373
|
+
return record.msgctxt === void 0 && record.msgid === "" && record.msgidPlural === void 0;
|
|
5374
|
+
}
|
|
5375
|
+
function scanPo(content) {
|
|
5376
|
+
const terminator = detectLineTerminator(content);
|
|
5377
|
+
const lines = splitPhysicalLines(content);
|
|
5378
|
+
const nodes = [];
|
|
5379
|
+
let raw = [];
|
|
5380
|
+
let headerFields;
|
|
5381
|
+
const flushRaw = () => {
|
|
5382
|
+
if (raw.length > 0) {
|
|
5383
|
+
nodes.push({ kind: "raw", text: joinSelfTerminated(raw, terminator) });
|
|
5384
|
+
raw = [];
|
|
5385
|
+
}
|
|
5386
|
+
};
|
|
5387
|
+
let i = 0;
|
|
5388
|
+
while (i < lines.length) {
|
|
5389
|
+
const line = lines[i] ?? "";
|
|
5390
|
+
if (line.trim() === "") {
|
|
5391
|
+
raw.push(line);
|
|
5392
|
+
i += 1;
|
|
5393
|
+
continue;
|
|
5394
|
+
}
|
|
5395
|
+
if (isObsoleteLine(line)) {
|
|
5396
|
+
while (i < lines.length && isObsoleteLine(lines[i] ?? "")) {
|
|
5397
|
+
raw.push(lines[i] ?? "");
|
|
5398
|
+
i += 1;
|
|
5399
|
+
}
|
|
5400
|
+
continue;
|
|
5401
|
+
}
|
|
5402
|
+
const record = parseRecord(lines, i, terminator);
|
|
5403
|
+
if (isHeaderRecord(record)) {
|
|
5404
|
+
flushRaw();
|
|
5405
|
+
nodes.push({
|
|
5406
|
+
kind: "raw",
|
|
5407
|
+
text: joinSelfTerminated(lines.slice(i, record.entryEndIndex), terminator)
|
|
5408
|
+
});
|
|
5409
|
+
headerFields = parseHeaderFields(record.singularValue ?? "");
|
|
5410
|
+
i = record.entryEndIndex;
|
|
5411
|
+
continue;
|
|
3240
5412
|
}
|
|
5413
|
+
flushRaw();
|
|
5414
|
+
nodes.push({
|
|
5415
|
+
kind: "entry",
|
|
5416
|
+
prefixRaw: record.prefixRaw,
|
|
5417
|
+
msgctxt: record.msgctxt,
|
|
5418
|
+
msgid: record.msgid,
|
|
5419
|
+
msgidPlural: record.msgidPlural,
|
|
5420
|
+
description: record.description,
|
|
5421
|
+
singularValue: record.singularValue,
|
|
5422
|
+
pluralValues: record.pluralValues
|
|
5423
|
+
});
|
|
5424
|
+
i = record.entryEndIndex;
|
|
3241
5425
|
}
|
|
3242
|
-
|
|
5426
|
+
flushRaw();
|
|
5427
|
+
return { nodes, terminator, headerFields };
|
|
3243
5428
|
}
|
|
3244
|
-
function
|
|
3245
|
-
|
|
5429
|
+
function assertPluralIndicesInRange(node, pluralValues, nplurals) {
|
|
5430
|
+
if (nplurals === void 0) {
|
|
5431
|
+
throw new AdapterError(
|
|
5432
|
+
"INVALID_STRUCTURE",
|
|
5433
|
+
`The msgid "${node.msgid}" has plural forms, but the file has no "Plural-Forms" header declaring nplurals.`
|
|
5434
|
+
);
|
|
5435
|
+
}
|
|
5436
|
+
const maxIndex = Math.max(...pluralValues.keys());
|
|
5437
|
+
if (maxIndex >= nplurals) {
|
|
5438
|
+
throw new AdapterError(
|
|
5439
|
+
"INVALID_STRUCTURE",
|
|
5440
|
+
`The msgid "${node.msgid}" has msgstr[${maxIndex}], but the header declares nplurals=${nplurals} (valid indices 0..${nplurals - 1}).`
|
|
5441
|
+
);
|
|
5442
|
+
}
|
|
5443
|
+
return maxIndex;
|
|
3246
5444
|
}
|
|
3247
|
-
function
|
|
3248
|
-
|
|
5445
|
+
function addPluralEntries(entries, node, pluralValues, namespace, nplurals) {
|
|
5446
|
+
assertPluralIndicesInRange(node, pluralValues, nplurals);
|
|
5447
|
+
for (const [index, value] of pluralValues) {
|
|
5448
|
+
const key = composeKey(node.msgctxt, node.msgid, index);
|
|
5449
|
+
entries.set(key, {
|
|
5450
|
+
key,
|
|
5451
|
+
namespace,
|
|
5452
|
+
value,
|
|
5453
|
+
placeholders: extractGettextPlaceholders(value),
|
|
5454
|
+
isPlural: true,
|
|
5455
|
+
...node.msgidPlural !== void 0 ? { meaning: node.msgidPlural } : {},
|
|
5456
|
+
...node.description !== void 0 ? { description: node.description } : {}
|
|
5457
|
+
});
|
|
5458
|
+
}
|
|
3249
5459
|
}
|
|
3250
|
-
function
|
|
3251
|
-
|
|
5460
|
+
function addSingularEntry(entries, node, namespace) {
|
|
5461
|
+
const key = composeKey(node.msgctxt, node.msgid);
|
|
5462
|
+
const value = node.singularValue ?? "";
|
|
5463
|
+
entries.set(key, {
|
|
5464
|
+
key,
|
|
5465
|
+
namespace,
|
|
5466
|
+
value,
|
|
5467
|
+
placeholders: extractGettextPlaceholders(value),
|
|
5468
|
+
isPlural: false,
|
|
5469
|
+
...node.description !== void 0 ? { description: node.description } : {}
|
|
5470
|
+
});
|
|
5471
|
+
}
|
|
5472
|
+
function headerNplurals(doc) {
|
|
5473
|
+
if (doc.headerFields === void 0) {
|
|
3252
5474
|
return void 0;
|
|
3253
5475
|
}
|
|
3254
|
-
|
|
3255
|
-
return typeof description === "string" ? description : void 0;
|
|
5476
|
+
return parseNplurals(doc.headerFields.get("Plural-Forms") ?? "");
|
|
3256
5477
|
}
|
|
3257
|
-
function
|
|
3258
|
-
const
|
|
3259
|
-
const
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
if (
|
|
5478
|
+
function parsePoEntries(content, namespace) {
|
|
5479
|
+
const doc = scanPo(content);
|
|
5480
|
+
const nplurals = headerNplurals(doc);
|
|
5481
|
+
const entries = /* @__PURE__ */ new Map();
|
|
5482
|
+
for (const node of doc.nodes) {
|
|
5483
|
+
if (node.kind !== "entry") {
|
|
3263
5484
|
continue;
|
|
3264
5485
|
}
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
5486
|
+
if (node.pluralValues !== void 0) {
|
|
5487
|
+
addPluralEntries(entries, node, node.pluralValues, namespace, nplurals);
|
|
5488
|
+
continue;
|
|
3268
5489
|
}
|
|
5490
|
+
addSingularEntry(entries, node, namespace);
|
|
3269
5491
|
}
|
|
3270
|
-
return
|
|
5492
|
+
return entries;
|
|
3271
5493
|
}
|
|
3272
|
-
function
|
|
3273
|
-
const
|
|
5494
|
+
function groupEntries2(entries) {
|
|
5495
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3274
5496
|
for (const [key, entry] of entries) {
|
|
3275
|
-
|
|
5497
|
+
const decomposed = decomposeKey(key);
|
|
5498
|
+
const groupKey = composeKey(decomposed.msgctxt, decomposed.msgid);
|
|
5499
|
+
let group = groups.get(groupKey);
|
|
5500
|
+
if (group === void 0) {
|
|
5501
|
+
group = {
|
|
5502
|
+
msgctxt: decomposed.msgctxt,
|
|
5503
|
+
msgid: decomposed.msgid,
|
|
5504
|
+
msgidPlural: void 0,
|
|
5505
|
+
values: /* @__PURE__ */ new Map()
|
|
5506
|
+
};
|
|
5507
|
+
groups.set(groupKey, group);
|
|
5508
|
+
}
|
|
5509
|
+
if (decomposed.pluralIndex !== void 0 && entry.meaning !== void 0) {
|
|
5510
|
+
group.msgidPlural = entry.meaning;
|
|
5511
|
+
}
|
|
5512
|
+
group.values.set(decomposed.pluralIndex, entry.value);
|
|
3276
5513
|
}
|
|
3277
|
-
return
|
|
5514
|
+
return groups;
|
|
3278
5515
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
}
|
|
3284
|
-
|
|
3285
|
-
|
|
5516
|
+
function renderNewGroup(group, terminator) {
|
|
5517
|
+
const lines = [];
|
|
5518
|
+
if (group.msgctxt !== void 0) {
|
|
5519
|
+
lines.push(`msgctxt "${encodeCString(group.msgctxt)}"`);
|
|
5520
|
+
}
|
|
5521
|
+
lines.push(`msgid "${encodeCString(group.msgid)}"`);
|
|
5522
|
+
const pluralPairs = [...group.values.entries()].filter((pair) => pair[0] !== void 0).sort((a, b) => a[0] - b[0]);
|
|
5523
|
+
if (pluralPairs.length > 0) {
|
|
5524
|
+
lines.push(`msgid_plural "${encodeCString(group.msgidPlural ?? group.msgid)}"`);
|
|
5525
|
+
for (const [index, value] of pluralPairs) {
|
|
5526
|
+
lines.push(`msgstr[${index}] "${encodeCString(value)}"`);
|
|
3286
5527
|
}
|
|
3287
|
-
|
|
5528
|
+
} else {
|
|
5529
|
+
const singular = group.values.get(void 0);
|
|
5530
|
+
lines.push(`msgstr "${encodeCString(singular ?? "")}"`);
|
|
3288
5531
|
}
|
|
3289
|
-
return
|
|
5532
|
+
return `${joinSelfTerminated(lines, terminator)}${terminator}`;
|
|
3290
5533
|
}
|
|
3291
|
-
|
|
3292
|
-
const
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
if (
|
|
3299
|
-
|
|
3300
|
-
out.set(key, translated);
|
|
3301
|
-
} else if (isMetadataKey(key) || typeof value === "string") {
|
|
3302
|
-
out.set(key, value);
|
|
5534
|
+
function renderMsgstrLine(index, value, terminator) {
|
|
5535
|
+
const keyword = index === void 0 ? "msgstr" : `msgstr[${index}]`;
|
|
5536
|
+
return `${keyword} "${encodeCString(value)}"${terminator}`;
|
|
5537
|
+
}
|
|
5538
|
+
function appendNewEntries(entries, emitted, terminator) {
|
|
5539
|
+
const pending = /* @__PURE__ */ new Map();
|
|
5540
|
+
for (const [key, entry] of entries) {
|
|
5541
|
+
if (!emitted.has(key)) {
|
|
5542
|
+
pending.set(key, entry);
|
|
3303
5543
|
}
|
|
3304
5544
|
}
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
5545
|
+
if (pending.size === 0) {
|
|
5546
|
+
return "";
|
|
5547
|
+
}
|
|
5548
|
+
let out = "";
|
|
5549
|
+
for (const group of groupEntries2(pending).values()) {
|
|
5550
|
+
out += renderNewGroup(group, terminator);
|
|
5551
|
+
}
|
|
5552
|
+
return out;
|
|
5553
|
+
}
|
|
5554
|
+
function rewriteDocument(doc, entries) {
|
|
5555
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
5556
|
+
const parts = [];
|
|
5557
|
+
for (const node of doc.nodes) {
|
|
5558
|
+
if (node.kind === "raw") {
|
|
5559
|
+
parts.push(node.text);
|
|
5560
|
+
continue;
|
|
5561
|
+
}
|
|
5562
|
+
if (node.pluralValues !== void 0) {
|
|
5563
|
+
const survivors = [...node.pluralValues.keys()].map((index) => ({ index, key: composeKey(node.msgctxt, node.msgid, index) })).map(({ index, key: key2 }) => ({ index, key: key2, entry: entries.get(key2) })).filter(
|
|
5564
|
+
(candidate) => candidate.entry !== void 0
|
|
5565
|
+
).sort((a, b) => a.index - b.index);
|
|
5566
|
+
if (survivors.length === 0) {
|
|
5567
|
+
continue;
|
|
5568
|
+
}
|
|
5569
|
+
parts.push(node.prefixRaw);
|
|
5570
|
+
for (const { index, key: key2, entry: entry2 } of survivors) {
|
|
5571
|
+
parts.push(renderMsgstrLine(index, entry2.value, doc.terminator));
|
|
5572
|
+
emitted.add(key2);
|
|
5573
|
+
}
|
|
5574
|
+
continue;
|
|
5575
|
+
}
|
|
5576
|
+
const key = composeKey(node.msgctxt, node.msgid);
|
|
5577
|
+
const entry = entries.get(key);
|
|
5578
|
+
if (entry === void 0) {
|
|
5579
|
+
continue;
|
|
5580
|
+
}
|
|
5581
|
+
parts.push(node.prefixRaw);
|
|
5582
|
+
parts.push(renderMsgstrLine(void 0, entry.value, doc.terminator));
|
|
5583
|
+
emitted.add(key);
|
|
5584
|
+
}
|
|
5585
|
+
return parts.join("") + appendNewEntries(entries, emitted, doc.terminator);
|
|
5586
|
+
}
|
|
5587
|
+
function synthesizeFromScratch(entries) {
|
|
5588
|
+
const terminator = "\n";
|
|
5589
|
+
const groups = groupEntries2(entries);
|
|
5590
|
+
let maxIndex = -1;
|
|
5591
|
+
for (const group of groups.values()) {
|
|
5592
|
+
for (const index of group.values.keys()) {
|
|
5593
|
+
if (index !== void 0 && index > maxIndex) {
|
|
5594
|
+
maxIndex = index;
|
|
5595
|
+
}
|
|
3308
5596
|
}
|
|
3309
5597
|
}
|
|
5598
|
+
const headerLines = [
|
|
5599
|
+
'msgid ""',
|
|
5600
|
+
'msgstr ""',
|
|
5601
|
+
'"Content-Type: text/plain; charset=UTF-8\\n"',
|
|
5602
|
+
'"Content-Transfer-Encoding: 8bit\\n"'
|
|
5603
|
+
];
|
|
5604
|
+
if (maxIndex >= 0) {
|
|
5605
|
+
headerLines.push(`"Plural-Forms: ${defaultPluralFormsExpression(maxIndex + 1)}\\n"`);
|
|
5606
|
+
}
|
|
5607
|
+
let out = `${joinSelfTerminated(headerLines, terminator)}${terminator}`;
|
|
5608
|
+
for (const group of groups.values()) {
|
|
5609
|
+
out += renderNewGroup(group, terminator);
|
|
5610
|
+
}
|
|
3310
5611
|
return out;
|
|
3311
5612
|
}
|
|
3312
|
-
function
|
|
3313
|
-
|
|
5613
|
+
async function readDestination2(filePath, fs) {
|
|
5614
|
+
let outcome;
|
|
5615
|
+
try {
|
|
5616
|
+
outcome = await readBoundedFile(fs, filePath);
|
|
5617
|
+
} catch (error) {
|
|
5618
|
+
if (isEnoent(error)) {
|
|
5619
|
+
return void 0;
|
|
5620
|
+
}
|
|
5621
|
+
throw new AdapterError("INVALID_STRUCTURE", "The destination file could not be read.");
|
|
5622
|
+
}
|
|
5623
|
+
const content = outcomeToContent(outcome, "The destination path is not a regular file.");
|
|
5624
|
+
return scanPo(content);
|
|
3314
5625
|
}
|
|
3315
|
-
function
|
|
3316
|
-
|
|
5626
|
+
async function serializePoEntries(entries, filePath, fs) {
|
|
5627
|
+
const doc = await readDestination2(filePath, fs);
|
|
5628
|
+
return doc === void 0 ? synthesizeFromScratch(entries) : rewriteDocument(doc, entries);
|
|
5629
|
+
}
|
|
5630
|
+
function createGettextAdapter(fs = nodeAdapterFs) {
|
|
5631
|
+
return createFlatFileAdapter({
|
|
3317
5632
|
fs,
|
|
3318
|
-
format: "
|
|
3319
|
-
extensions: [".
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
extractPlaceholders: icuPlaceholders,
|
|
3324
|
-
deriveEntry: icuDeriveEntry,
|
|
3325
|
-
computeInvalidIcuKeys: icuInvalidKeys,
|
|
3326
|
-
validateMessage: icuIsValid,
|
|
3327
|
-
comparePlaceholders: compareIcuPlaceholders,
|
|
3328
|
-
buildWriteTree: buildArbWriteTree,
|
|
3329
|
-
deriveDescriptions: extractArbDescriptions
|
|
5633
|
+
format: "gettext-po",
|
|
5634
|
+
extensions: [".po", ".pot"],
|
|
5635
|
+
parseEntries: parsePoEntries,
|
|
5636
|
+
serializeEntries: serializePoEntries,
|
|
5637
|
+
extractPlaceholders: extractGettextPlaceholders
|
|
3330
5638
|
});
|
|
3331
5639
|
}
|
|
3332
5640
|
function createJsonFileAdapter(options) {
|
|
@@ -3378,23 +5686,6 @@ function extractDoubleBracePlaceholders(value) {
|
|
|
3378
5686
|
function extractI18nextPlaceholders(value) {
|
|
3379
5687
|
return scanTokens(value, I18NEXT_PATTERN);
|
|
3380
5688
|
}
|
|
3381
|
-
var PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/;
|
|
3382
|
-
function isPluralKey(key) {
|
|
3383
|
-
return PLURAL_SUFFIX.test(key);
|
|
3384
|
-
}
|
|
3385
|
-
function pluralCategoryOf(key) {
|
|
3386
|
-
const match = PLURAL_SUFFIX.exec(key);
|
|
3387
|
-
return match?.[1];
|
|
3388
|
-
}
|
|
3389
|
-
function pluralBaseKey(key) {
|
|
3390
|
-
if (!isPluralKey(key)) {
|
|
3391
|
-
return void 0;
|
|
3392
|
-
}
|
|
3393
|
-
return key.replace(PLURAL_SUFFIX, "");
|
|
3394
|
-
}
|
|
3395
|
-
function makePluralKey(baseKey, category) {
|
|
3396
|
-
return `${baseKey}_${category}`;
|
|
3397
|
-
}
|
|
3398
5689
|
function createI18nextJsonAdapter(fs = nodeAdapterFs) {
|
|
3399
5690
|
return createJsonFileAdapter({
|
|
3400
5691
|
fs,
|
|
@@ -3495,45 +5786,7 @@ function createNgxTranslateJsonAdapter(fs = nodeAdapterFs) {
|
|
|
3495
5786
|
validateTree: assertNotMixed,
|
|
3496
5787
|
buildWriteTree: buildNgxWriteTree,
|
|
3497
5788
|
keyMode: "path-notation"
|
|
3498
|
-
});
|
|
3499
|
-
}
|
|
3500
|
-
function toEntries2(content, namespace, parseEntries) {
|
|
3501
|
-
try {
|
|
3502
|
-
return parseEntries(content, namespace);
|
|
3503
|
-
} catch (error) {
|
|
3504
|
-
rethrowStructured(error, "The file could not be parsed.");
|
|
3505
|
-
}
|
|
3506
|
-
}
|
|
3507
|
-
function createFlatFileAdapter(options) {
|
|
3508
|
-
const {
|
|
3509
|
-
format,
|
|
3510
|
-
extensions,
|
|
3511
|
-
sniff,
|
|
3512
|
-
parseEntries,
|
|
3513
|
-
serializeEntries,
|
|
3514
|
-
extractPlaceholders,
|
|
3515
|
-
validateMessage,
|
|
3516
|
-
computeInvalidIcuKeys,
|
|
3517
|
-
fs = nodeAdapterFs
|
|
3518
|
-
} = options;
|
|
3519
|
-
return {
|
|
3520
|
-
format,
|
|
3521
|
-
canHandle: buildCanHandle(extensions, sniff),
|
|
3522
|
-
extractPlaceholders,
|
|
3523
|
-
validateMessage: validateMessage ?? (() => true),
|
|
3524
|
-
async read(filePath, locale) {
|
|
3525
|
-
const content = await readFileContent(fs, filePath);
|
|
3526
|
-
const namespace = namespaceOf(filePath);
|
|
3527
|
-
const entries = toEntries2(content, namespace, parseEntries);
|
|
3528
|
-
const resource = { locale, namespace, format, entries };
|
|
3529
|
-
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
3530
|
-
return { resource, invalidIcuKeys, excludedLeafPaths: [] };
|
|
3531
|
-
},
|
|
3532
|
-
async write(resource, filePath) {
|
|
3533
|
-
const data = await serializeEntries(resource.entries, filePath, fs);
|
|
3534
|
-
await fs.writeFileAtomic(filePath, data);
|
|
3535
|
-
}
|
|
3536
|
-
};
|
|
5789
|
+
});
|
|
3537
5790
|
}
|
|
3538
5791
|
var SUBMESSAGE_TYPES = /* @__PURE__ */ new Set(["plural", "select", "selectordinal", "choice"]);
|
|
3539
5792
|
var ARGUMENT_NAME = /^(?:\d+|[A-Za-z_$][\w$-]*)$/;
|
|
@@ -3633,28 +5886,11 @@ function extractPropertiesPlaceholders(value) {
|
|
|
3633
5886
|
scanRange(value, matchingBraces(value), 0, value.length, true, out);
|
|
3634
5887
|
return out;
|
|
3635
5888
|
}
|
|
3636
|
-
var
|
|
5889
|
+
var UNICODE_ESCAPE3 = /^[0-9a-fA-F]{4}$/;
|
|
3637
5890
|
var LEADING_WHITESPACE = /^[ \t\f]+/;
|
|
3638
|
-
var TRAILING_TERMINATOR = /(?:\r\n|\r|\n)$/;
|
|
3639
|
-
function detectLineTerminator(content) {
|
|
3640
|
-
if (content.includes("\r\n")) {
|
|
3641
|
-
return "\r\n";
|
|
3642
|
-
}
|
|
3643
|
-
return content.includes("\r") ? "\r" : "\n";
|
|
3644
|
-
}
|
|
3645
5891
|
function isPropertiesWhitespace(char) {
|
|
3646
5892
|
return char === " " || char === " " || char === "\f";
|
|
3647
5893
|
}
|
|
3648
|
-
function splitPhysicalLines(content) {
|
|
3649
|
-
if (content === "") {
|
|
3650
|
-
return [];
|
|
3651
|
-
}
|
|
3652
|
-
const lines = content.split(/\r\n|\r|\n/);
|
|
3653
|
-
if (TRAILING_TERMINATOR.test(content)) {
|
|
3654
|
-
lines.pop();
|
|
3655
|
-
}
|
|
3656
|
-
return lines;
|
|
3657
|
-
}
|
|
3658
5894
|
function countTrailingBackslashes(line) {
|
|
3659
5895
|
let count = 0;
|
|
3660
5896
|
for (let i = line.length - 1; i >= 0 && line[i] === "\\"; i -= 1) {
|
|
@@ -3668,7 +5904,7 @@ function isContinued(line) {
|
|
|
3668
5904
|
function isBlankLine(line) {
|
|
3669
5905
|
return line.trim() === "";
|
|
3670
5906
|
}
|
|
3671
|
-
function
|
|
5907
|
+
function isCommentLine2(line) {
|
|
3672
5908
|
const trimmed = line.replace(LEADING_WHITESPACE, "");
|
|
3673
5909
|
return trimmed.startsWith("#") || trimmed.startsWith("!");
|
|
3674
5910
|
}
|
|
@@ -3687,7 +5923,7 @@ function joinContinuation(lines, start) {
|
|
|
3687
5923
|
pieces.push(current);
|
|
3688
5924
|
return { logical: pieces.join(""), nextIndex: index + 1 };
|
|
3689
5925
|
}
|
|
3690
|
-
function
|
|
5926
|
+
function decodeSimpleEscape2(char) {
|
|
3691
5927
|
switch (char) {
|
|
3692
5928
|
case "t":
|
|
3693
5929
|
return " ";
|
|
@@ -3701,9 +5937,9 @@ function decodeSimpleEscape(char) {
|
|
|
3701
5937
|
return char;
|
|
3702
5938
|
}
|
|
3703
5939
|
}
|
|
3704
|
-
function
|
|
5940
|
+
function decodeUnicodeEscape3(raw, at) {
|
|
3705
5941
|
const hex = raw.slice(at, at + 4);
|
|
3706
|
-
if (!
|
|
5942
|
+
if (!UNICODE_ESCAPE3.test(hex)) {
|
|
3707
5943
|
throw new AdapterError("INVALID_STRUCTURE", "The file has a malformed unicode escape.");
|
|
3708
5944
|
}
|
|
3709
5945
|
return String.fromCharCode(Number.parseInt(hex, 16));
|
|
@@ -3723,11 +5959,11 @@ function decodeEscapes(raw) {
|
|
|
3723
5959
|
break;
|
|
3724
5960
|
}
|
|
3725
5961
|
if (next === "u") {
|
|
3726
|
-
out +=
|
|
5962
|
+
out += decodeUnicodeEscape3(raw, i + 2);
|
|
3727
5963
|
i += 6;
|
|
3728
5964
|
continue;
|
|
3729
5965
|
}
|
|
3730
|
-
out +=
|
|
5966
|
+
out += decodeSimpleEscape2(next);
|
|
3731
5967
|
i += 2;
|
|
3732
5968
|
}
|
|
3733
5969
|
return out;
|
|
@@ -3775,7 +6011,7 @@ function parseItems(content) {
|
|
|
3775
6011
|
let i = 0;
|
|
3776
6012
|
while (i < lines.length) {
|
|
3777
6013
|
const line = lines[i] ?? "";
|
|
3778
|
-
if (isBlankLine(line) ||
|
|
6014
|
+
if (isBlankLine(line) || isCommentLine2(line)) {
|
|
3779
6015
|
items.push({ kind: "raw", text: line });
|
|
3780
6016
|
i += 1;
|
|
3781
6017
|
continue;
|
|
@@ -3804,7 +6040,7 @@ function parsePropertiesEntries(content, namespace) {
|
|
|
3804
6040
|
function unicodeEscape(code) {
|
|
3805
6041
|
return `\\u${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
3806
6042
|
}
|
|
3807
|
-
function
|
|
6043
|
+
function escapeChar4(char, isFirst, isKey) {
|
|
3808
6044
|
switch (char) {
|
|
3809
6045
|
case "\\":
|
|
3810
6046
|
return "\\\\";
|
|
@@ -3830,7 +6066,7 @@ function escapeString(input, isKey) {
|
|
|
3830
6066
|
for (let i = 0; i < input.length; i += 1) {
|
|
3831
6067
|
const char = input[i];
|
|
3832
6068
|
if (char !== void 0) {
|
|
3833
|
-
out +=
|
|
6069
|
+
out += escapeChar4(char, i === 0, isKey);
|
|
3834
6070
|
}
|
|
3835
6071
|
}
|
|
3836
6072
|
return out;
|
|
@@ -3952,20 +6188,354 @@ function createVueI18nJsonAdapter(fs = nodeAdapterFs) {
|
|
|
3952
6188
|
})
|
|
3953
6189
|
});
|
|
3954
6190
|
}
|
|
6191
|
+
var PLURAL_CATEGORIES2 = [
|
|
6192
|
+
"zero",
|
|
6193
|
+
"one",
|
|
6194
|
+
"two",
|
|
6195
|
+
"few",
|
|
6196
|
+
"many",
|
|
6197
|
+
"other"
|
|
6198
|
+
];
|
|
6199
|
+
var PLURAL_CATEGORY_SET2 = new Set(PLURAL_CATEGORIES2);
|
|
6200
|
+
function isRecord3(value) {
|
|
6201
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6202
|
+
}
|
|
6203
|
+
function parseXcstringsDocument(content, filePath) {
|
|
6204
|
+
let parsed;
|
|
6205
|
+
try {
|
|
6206
|
+
parsed = JSON.parse(content);
|
|
6207
|
+
} catch {
|
|
6208
|
+
throw new AdapterError("INVALID_JSON", `${filePath} is not valid JSON.`);
|
|
6209
|
+
}
|
|
6210
|
+
if (!isRecord3(parsed)) {
|
|
6211
|
+
throw new AdapterError(
|
|
6212
|
+
"INVALID_STRUCTURE",
|
|
6213
|
+
`${filePath}: the top level of an .xcstrings catalogue must be a JSON object.`
|
|
6214
|
+
);
|
|
6215
|
+
}
|
|
6216
|
+
const sourceLanguage = parsed.sourceLanguage;
|
|
6217
|
+
if (typeof sourceLanguage !== "string" || sourceLanguage.length === 0) {
|
|
6218
|
+
throw new AdapterError(
|
|
6219
|
+
"INVALID_STRUCTURE",
|
|
6220
|
+
`${filePath}: the catalogue has no "sourceLanguage" string field.`
|
|
6221
|
+
);
|
|
6222
|
+
}
|
|
6223
|
+
const strings = parsed.strings;
|
|
6224
|
+
if (!isRecord3(strings)) {
|
|
6225
|
+
throw new AdapterError(
|
|
6226
|
+
"INVALID_STRUCTURE",
|
|
6227
|
+
`${filePath}: the catalogue has no "strings" object field.`
|
|
6228
|
+
);
|
|
6229
|
+
}
|
|
6230
|
+
return { raw: parsed, sourceLanguage, strings };
|
|
6231
|
+
}
|
|
6232
|
+
function entryObjectOf(rawEntry, filePath, key) {
|
|
6233
|
+
if (!isRecord3(rawEntry)) {
|
|
6234
|
+
throw new AdapterError(
|
|
6235
|
+
"INVALID_STRUCTURE",
|
|
6236
|
+
`${filePath}: the entry "${key}" must be a JSON object.`
|
|
6237
|
+
);
|
|
6238
|
+
}
|
|
6239
|
+
return rawEntry;
|
|
6240
|
+
}
|
|
6241
|
+
function localizationsOf(entry, filePath, key) {
|
|
6242
|
+
const localizations = entry.localizations;
|
|
6243
|
+
if (localizations === void 0) {
|
|
6244
|
+
return {};
|
|
6245
|
+
}
|
|
6246
|
+
if (!isRecord3(localizations)) {
|
|
6247
|
+
throw new AdapterError(
|
|
6248
|
+
"INVALID_STRUCTURE",
|
|
6249
|
+
`${filePath}: the entry "${key}" has a "localizations" field that is not a JSON object.`
|
|
6250
|
+
);
|
|
6251
|
+
}
|
|
6252
|
+
return localizations;
|
|
6253
|
+
}
|
|
6254
|
+
function shouldTranslate(entry) {
|
|
6255
|
+
return entry.shouldTranslate !== false;
|
|
6256
|
+
}
|
|
6257
|
+
function stringUnitValueOf(stringUnit, filePath, where) {
|
|
6258
|
+
if (!isRecord3(stringUnit) || typeof stringUnit.value !== "string") {
|
|
6259
|
+
throw new AdapterError(
|
|
6260
|
+
"INVALID_STRUCTURE",
|
|
6261
|
+
`${filePath}: ${where} has a "stringUnit" without a string "value" field.`
|
|
6262
|
+
);
|
|
6263
|
+
}
|
|
6264
|
+
return stringUnit.value;
|
|
6265
|
+
}
|
|
6266
|
+
function pluralVariationsOf(loc) {
|
|
6267
|
+
const variations = loc.variations;
|
|
6268
|
+
if (!isRecord3(variations)) {
|
|
6269
|
+
return void 0;
|
|
6270
|
+
}
|
|
6271
|
+
return variations.plural;
|
|
6272
|
+
}
|
|
6273
|
+
function localizationValueOf(loc, filePath, key, locale) {
|
|
6274
|
+
if (!isRecord3(loc)) {
|
|
6275
|
+
throw new AdapterError(
|
|
6276
|
+
"INVALID_STRUCTURE",
|
|
6277
|
+
`${filePath}: the entry "${key}"'s "${locale}" localization must be a JSON object.`
|
|
6278
|
+
);
|
|
6279
|
+
}
|
|
6280
|
+
const plural = pluralVariationsOf(loc);
|
|
6281
|
+
if (plural !== void 0) {
|
|
6282
|
+
if (!isRecord3(plural)) {
|
|
6283
|
+
throw new AdapterError(
|
|
6284
|
+
"INVALID_STRUCTURE",
|
|
6285
|
+
`${filePath}: the entry "${key}"'s "${locale}" localization has a "variations.plural" field that is not a JSON object.`
|
|
6286
|
+
);
|
|
6287
|
+
}
|
|
6288
|
+
const categories = /* @__PURE__ */ new Map();
|
|
6289
|
+
for (const [category, variation] of Object.entries(plural)) {
|
|
6290
|
+
if (!PLURAL_CATEGORY_SET2.has(category)) {
|
|
6291
|
+
throw new AdapterError(
|
|
6292
|
+
"INVALID_STRUCTURE",
|
|
6293
|
+
`${filePath}: the entry "${key}"'s "${locale}" localization has an unsupported plural category "${category}"; expected one of ${PLURAL_CATEGORIES2.join(", ")}.`
|
|
6294
|
+
);
|
|
6295
|
+
}
|
|
6296
|
+
const value = stringUnitValueOf(
|
|
6297
|
+
isRecord3(variation) ? variation.stringUnit : void 0,
|
|
6298
|
+
filePath,
|
|
6299
|
+
`the entry "${key}"'s "${locale}" "${category}" plural category`
|
|
6300
|
+
);
|
|
6301
|
+
categories.set(category, value);
|
|
6302
|
+
}
|
|
6303
|
+
if (categories.size === 0) {
|
|
6304
|
+
throw new AdapterError(
|
|
6305
|
+
"INVALID_STRUCTURE",
|
|
6306
|
+
`${filePath}: the entry "${key}"'s "${locale}" localization has a "variations.plural" object with no plural categories.`
|
|
6307
|
+
);
|
|
6308
|
+
}
|
|
6309
|
+
return { kind: "plural", categories };
|
|
6310
|
+
}
|
|
6311
|
+
if (loc.stringUnit !== void 0) {
|
|
6312
|
+
return {
|
|
6313
|
+
kind: "plain",
|
|
6314
|
+
value: stringUnitValueOf(
|
|
6315
|
+
loc.stringUnit,
|
|
6316
|
+
filePath,
|
|
6317
|
+
`the entry "${key}"'s "${locale}" localization`
|
|
6318
|
+
)
|
|
6319
|
+
};
|
|
6320
|
+
}
|
|
6321
|
+
throw new AdapterError(
|
|
6322
|
+
"INVALID_STRUCTURE",
|
|
6323
|
+
`${filePath}: the entry "${key}"'s "${locale}" localization has neither a "stringUnit" nor a "variations.plural" field; verbatra does not support device-class or width variants without one of those as a fallback.`
|
|
6324
|
+
);
|
|
6325
|
+
}
|
|
6326
|
+
function plainEntry(key, namespace, value, extractPlaceholders) {
|
|
6327
|
+
return { key, namespace, value, placeholders: extractPlaceholders(value), isPlural: false };
|
|
6328
|
+
}
|
|
6329
|
+
function pluralEntry(baseKey, category, namespace, value, extractPlaceholders) {
|
|
6330
|
+
const key = makePluralKey(baseKey, category);
|
|
6331
|
+
return [key, { key, namespace, value, placeholders: extractPlaceholders(value), isPlural: true }];
|
|
6332
|
+
}
|
|
6333
|
+
function addPresentEntry(out, loc, key, namespace, locale, filePath, extractPlaceholders) {
|
|
6334
|
+
const content = localizationValueOf(loc, filePath, key, locale);
|
|
6335
|
+
if (content.kind === "plain") {
|
|
6336
|
+
out.set(key, plainEntry(key, namespace, content.value, extractPlaceholders));
|
|
6337
|
+
return;
|
|
6338
|
+
}
|
|
6339
|
+
for (const [category, value] of content.categories) {
|
|
6340
|
+
const [pluralKey, entry] = pluralEntry(key, category, namespace, value, extractPlaceholders);
|
|
6341
|
+
out.set(pluralKey, entry);
|
|
6342
|
+
}
|
|
6343
|
+
}
|
|
6344
|
+
function readXcstringsLocale(doc, locale, namespace, filePath, extractPlaceholders) {
|
|
6345
|
+
const entries = /* @__PURE__ */ new Map();
|
|
6346
|
+
const excludedLeafPaths = [];
|
|
6347
|
+
for (const [key, rawEntry] of Object.entries(doc.strings)) {
|
|
6348
|
+
const entryObject = entryObjectOf(rawEntry, filePath, key);
|
|
6349
|
+
if (!shouldTranslate(entryObject)) {
|
|
6350
|
+
excludedLeafPaths.push(key);
|
|
6351
|
+
continue;
|
|
6352
|
+
}
|
|
6353
|
+
const localizations = localizationsOf(entryObject, filePath, key);
|
|
6354
|
+
const loc = localizations[locale];
|
|
6355
|
+
if (loc === void 0) {
|
|
6356
|
+
if (locale === doc.sourceLanguage) {
|
|
6357
|
+
entries.set(key, plainEntry(key, namespace, key, extractPlaceholders));
|
|
6358
|
+
}
|
|
6359
|
+
continue;
|
|
6360
|
+
}
|
|
6361
|
+
addPresentEntry(entries, loc, key, namespace, locale, filePath, extractPlaceholders);
|
|
6362
|
+
}
|
|
6363
|
+
return { entries, excludedLeafPaths };
|
|
6364
|
+
}
|
|
6365
|
+
function groupByKey(entries) {
|
|
6366
|
+
const singular = /* @__PURE__ */ new Map();
|
|
6367
|
+
const plural = /* @__PURE__ */ new Map();
|
|
6368
|
+
for (const [key, entry] of entries) {
|
|
6369
|
+
const baseKey = entry.isPlural ? pluralBaseKey(key) : void 0;
|
|
6370
|
+
const category = entry.isPlural ? pluralCategoryOf(key) : void 0;
|
|
6371
|
+
if (baseKey !== void 0 && category !== void 0) {
|
|
6372
|
+
const group = plural.get(baseKey) ?? /* @__PURE__ */ new Map();
|
|
6373
|
+
group.set(category, entry.value);
|
|
6374
|
+
plural.set(baseKey, group);
|
|
6375
|
+
} else {
|
|
6376
|
+
singular.set(key, entry.value);
|
|
6377
|
+
}
|
|
6378
|
+
}
|
|
6379
|
+
return { singular, plural };
|
|
6380
|
+
}
|
|
6381
|
+
function currentPlainValue(loc) {
|
|
6382
|
+
if (typeof loc !== "object" || loc === null) {
|
|
6383
|
+
return void 0;
|
|
6384
|
+
}
|
|
6385
|
+
const stringUnit = loc.stringUnit;
|
|
6386
|
+
if (typeof stringUnit !== "object" || stringUnit === null) {
|
|
6387
|
+
return void 0;
|
|
6388
|
+
}
|
|
6389
|
+
const value = stringUnit.value;
|
|
6390
|
+
return typeof value === "string" ? value : void 0;
|
|
6391
|
+
}
|
|
6392
|
+
function currentPluralValues(loc) {
|
|
6393
|
+
if (typeof loc !== "object" || loc === null) {
|
|
6394
|
+
return void 0;
|
|
6395
|
+
}
|
|
6396
|
+
const variations = loc.variations;
|
|
6397
|
+
if (typeof variations !== "object" || variations === null) {
|
|
6398
|
+
return void 0;
|
|
6399
|
+
}
|
|
6400
|
+
const plural = variations.plural;
|
|
6401
|
+
if (typeof plural !== "object" || plural === null) {
|
|
6402
|
+
return void 0;
|
|
6403
|
+
}
|
|
6404
|
+
const out = /* @__PURE__ */ new Map();
|
|
6405
|
+
for (const [category, variation] of Object.entries(plural)) {
|
|
6406
|
+
const value = currentPlainValue(variation);
|
|
6407
|
+
if (value === void 0) {
|
|
6408
|
+
return void 0;
|
|
6409
|
+
}
|
|
6410
|
+
out.set(category, value);
|
|
6411
|
+
}
|
|
6412
|
+
return out;
|
|
6413
|
+
}
|
|
6414
|
+
function pluralValuesEqual(current, wanted) {
|
|
6415
|
+
if (current === void 0 || current.size !== wanted.size) {
|
|
6416
|
+
return false;
|
|
6417
|
+
}
|
|
6418
|
+
for (const [category, value] of wanted) {
|
|
6419
|
+
if (current.get(category) !== value) {
|
|
6420
|
+
return false;
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
6423
|
+
return true;
|
|
6424
|
+
}
|
|
6425
|
+
function buildPluralLocalization(categories) {
|
|
6426
|
+
const plural = {};
|
|
6427
|
+
for (const [category, value] of categories) {
|
|
6428
|
+
plural[category] = { stringUnit: { state: "translated", value } };
|
|
6429
|
+
}
|
|
6430
|
+
return { variations: { plural } };
|
|
6431
|
+
}
|
|
6432
|
+
function buildPlainLocalization(value) {
|
|
6433
|
+
return { stringUnit: { state: "translated", value } };
|
|
6434
|
+
}
|
|
6435
|
+
function deleteLocalization(entry, locale) {
|
|
6436
|
+
const localizations = entry.localizations;
|
|
6437
|
+
if (typeof localizations !== "object" || localizations === null) {
|
|
6438
|
+
return;
|
|
6439
|
+
}
|
|
6440
|
+
delete localizations[locale];
|
|
6441
|
+
}
|
|
6442
|
+
function writeLocalization(entry, locale, filePath, key, content) {
|
|
6443
|
+
const localizations = localizationsOf(entry, filePath, key);
|
|
6444
|
+
if (entry.localizations === void 0) {
|
|
6445
|
+
entry.localizations = localizations;
|
|
6446
|
+
}
|
|
6447
|
+
localizations[locale] = content;
|
|
6448
|
+
}
|
|
6449
|
+
function patchKey(entry, key, locale, filePath, touched) {
|
|
6450
|
+
const loc = localizationsOf(entry, filePath, key)[locale];
|
|
6451
|
+
const singularValue = touched.singular.get(key);
|
|
6452
|
+
if (singularValue !== void 0) {
|
|
6453
|
+
const isPlural = currentPluralValues(loc) !== void 0;
|
|
6454
|
+
const currentValue = isPlural ? void 0 : currentPlainValue(loc) ?? "";
|
|
6455
|
+
if (currentValue === singularValue) {
|
|
6456
|
+
return;
|
|
6457
|
+
}
|
|
6458
|
+
writeLocalization(entry, locale, filePath, key, buildPlainLocalization(singularValue));
|
|
6459
|
+
return;
|
|
6460
|
+
}
|
|
6461
|
+
const pluralCategories = touched.plural.get(key);
|
|
6462
|
+
if (pluralCategories !== void 0) {
|
|
6463
|
+
if (pluralValuesEqual(currentPluralValues(loc), pluralCategories)) {
|
|
6464
|
+
return;
|
|
6465
|
+
}
|
|
6466
|
+
writeLocalization(entry, locale, filePath, key, buildPluralLocalization(pluralCategories));
|
|
6467
|
+
return;
|
|
6468
|
+
}
|
|
6469
|
+
deleteLocalization(entry, locale);
|
|
6470
|
+
}
|
|
6471
|
+
function patchXcstringsDocument(doc, locale, resource, filePath) {
|
|
6472
|
+
const touched = groupByKey(resource.entries);
|
|
6473
|
+
for (const [key, rawEntry] of Object.entries(doc.strings)) {
|
|
6474
|
+
const entry = entryObjectOf(rawEntry, filePath, key);
|
|
6475
|
+
if (!shouldTranslate(entry)) {
|
|
6476
|
+
continue;
|
|
6477
|
+
}
|
|
6478
|
+
patchKey(entry, key, locale, filePath, touched);
|
|
6479
|
+
}
|
|
6480
|
+
}
|
|
6481
|
+
var EXTENSIONS = [".xcstrings"];
|
|
6482
|
+
function createAppleXcstringsAdapter(fs = nodeAdapterFs) {
|
|
6483
|
+
return {
|
|
6484
|
+
format: "apple-xcstrings",
|
|
6485
|
+
canHandle: buildCanHandle(EXTENSIONS),
|
|
6486
|
+
extractPlaceholders: extractAppleStringsPlaceholders,
|
|
6487
|
+
validateMessage: () => true,
|
|
6488
|
+
async read(filePath, locale) {
|
|
6489
|
+
const content = await readFileContent(fs, filePath);
|
|
6490
|
+
const doc = parseXcstringsDocument(content, filePath);
|
|
6491
|
+
const namespace = namespaceOf(filePath);
|
|
6492
|
+
const { entries, excludedLeafPaths } = readXcstringsLocale(
|
|
6493
|
+
doc,
|
|
6494
|
+
locale,
|
|
6495
|
+
namespace,
|
|
6496
|
+
filePath,
|
|
6497
|
+
extractAppleStringsPlaceholders
|
|
6498
|
+
);
|
|
6499
|
+
return {
|
|
6500
|
+
resource: { locale, namespace, format: "apple-xcstrings", entries },
|
|
6501
|
+
invalidIcuKeys: [],
|
|
6502
|
+
excludedLeafPaths
|
|
6503
|
+
};
|
|
6504
|
+
},
|
|
6505
|
+
async write(resource, filePath) {
|
|
6506
|
+
let content;
|
|
6507
|
+
try {
|
|
6508
|
+
content = await readFileContent(fs, filePath);
|
|
6509
|
+
} catch (error) {
|
|
6510
|
+
if (isEnoent(error)) {
|
|
6511
|
+
throw new AdapterError(
|
|
6512
|
+
"INVALID_STRUCTURE",
|
|
6513
|
+
`${filePath}: the catalogue does not exist. verbatra does not create a new .xcstrings catalogue; create it in Xcode first.`
|
|
6514
|
+
);
|
|
6515
|
+
}
|
|
6516
|
+
throw error;
|
|
6517
|
+
}
|
|
6518
|
+
const doc = parseXcstringsDocument(content, filePath);
|
|
6519
|
+
patchXcstringsDocument(doc, resource.locale, resource, filePath);
|
|
6520
|
+
await fs.writeFileAtomic(filePath, `${JSON.stringify(doc.raw, null, 2)}
|
|
6521
|
+
`);
|
|
6522
|
+
}
|
|
6523
|
+
};
|
|
6524
|
+
}
|
|
3955
6525
|
var XLIFF_PATTERN = /<(?:x|g|bx|ex|ph|it|mrk)\b[^>]*>|\{[^{}]+\}/g;
|
|
3956
6526
|
function extractXliffPlaceholders(value) {
|
|
3957
6527
|
return scanTokens(value, XLIFF_PATTERN);
|
|
3958
6528
|
}
|
|
3959
|
-
var
|
|
3960
|
-
var
|
|
3961
|
-
function
|
|
3962
|
-
return node.nodeType ===
|
|
6529
|
+
var ELEMENT_NODE3 = 1;
|
|
6530
|
+
var TEXT_NODE2 = 3;
|
|
6531
|
+
function isElement3(node) {
|
|
6532
|
+
return node.nodeType === ELEMENT_NODE3;
|
|
3963
6533
|
}
|
|
3964
|
-
function
|
|
3965
|
-
return Array.from(parent.childNodes).filter(
|
|
6534
|
+
function elementChildren3(parent) {
|
|
6535
|
+
return Array.from(parent.childNodes).filter(isElement3);
|
|
3966
6536
|
}
|
|
3967
6537
|
function childByName(parent, name) {
|
|
3968
|
-
return
|
|
6538
|
+
return elementChildren3(parent).find((el) => el.localName === name) ?? null;
|
|
3969
6539
|
}
|
|
3970
6540
|
function collectByTag(root, name) {
|
|
3971
6541
|
return Array.from(root.getElementsByTagName(name));
|
|
@@ -3973,21 +6543,21 @@ function collectByTag(root, name) {
|
|
|
3973
6543
|
function unitKey(element, index) {
|
|
3974
6544
|
return element.getAttribute("id") ?? element.getAttribute("resname") ?? `unit-${index}`;
|
|
3975
6545
|
}
|
|
3976
|
-
function
|
|
6546
|
+
function onFatal3(level) {
|
|
3977
6547
|
if (level === "fatalError") {
|
|
3978
6548
|
throw new Error("malformed XML");
|
|
3979
6549
|
}
|
|
3980
6550
|
}
|
|
3981
|
-
function
|
|
6551
|
+
function assertNoDoctype2(content) {
|
|
3982
6552
|
if (/<!DOCTYPE/i.test(content) || /<!ENTITY/i.test(content)) {
|
|
3983
6553
|
throw new AdapterError("INVALID_XML", "XLIFF with a DTD or entity declaration is rejected.");
|
|
3984
6554
|
}
|
|
3985
6555
|
}
|
|
3986
6556
|
function parseXml(content) {
|
|
3987
|
-
|
|
6557
|
+
assertNoDoctype2(content);
|
|
3988
6558
|
let doc;
|
|
3989
6559
|
try {
|
|
3990
|
-
doc = new xmldom.DOMParser({ onError:
|
|
6560
|
+
doc = new xmldom.DOMParser({ onError: onFatal3 }).parseFromString(content, "text/xml");
|
|
3991
6561
|
} catch {
|
|
3992
6562
|
throw new AdapterError("INVALID_XML", "The file is not valid XML.");
|
|
3993
6563
|
}
|
|
@@ -4034,7 +6604,7 @@ function walkXliff20(root) {
|
|
|
4034
6604
|
collectByTag(root, "unit").forEach((unit, index) => {
|
|
4035
6605
|
const baseKey = unitKey(unit, index);
|
|
4036
6606
|
const description = unitDescription(unit);
|
|
4037
|
-
const segments =
|
|
6607
|
+
const segments = elementChildren3(unit).filter((el) => el.localName === "segment");
|
|
4038
6608
|
segments.forEach((segment, segIndex) => {
|
|
4039
6609
|
const source = childByName(segment, "source");
|
|
4040
6610
|
if (source !== null) {
|
|
@@ -4090,12 +6660,19 @@ function parseXliffEntries(content, namespace) {
|
|
|
4090
6660
|
}
|
|
4091
6661
|
return out;
|
|
4092
6662
|
}
|
|
4093
|
-
|
|
6663
|
+
function destinationReadErrorMessage(error) {
|
|
6664
|
+
if (isEnoent(error)) {
|
|
6665
|
+
return "The destination XLIFF file does not exist.";
|
|
6666
|
+
}
|
|
6667
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
6668
|
+
return `The destination XLIFF file could not be read: ${reason}`;
|
|
6669
|
+
}
|
|
6670
|
+
async function readDestination3(filePath, fs) {
|
|
4094
6671
|
let outcome;
|
|
4095
6672
|
try {
|
|
4096
6673
|
outcome = await readBoundedFile(fs, filePath);
|
|
4097
|
-
} catch {
|
|
4098
|
-
throw new AdapterError("INVALID_STRUCTURE",
|
|
6674
|
+
} catch (error) {
|
|
6675
|
+
throw new AdapterError("INVALID_STRUCTURE", destinationReadErrorMessage(error));
|
|
4099
6676
|
}
|
|
4100
6677
|
return outcomeToContent(outcome, "The destination path is not a regular file.");
|
|
4101
6678
|
}
|
|
@@ -4119,14 +6696,31 @@ function isAllowedInlineElement(element) {
|
|
|
4119
6696
|
return hasAllowedNamespace && XLIFF_INLINE_ELEMENTS.has(element.localName ?? "");
|
|
4120
6697
|
}
|
|
4121
6698
|
function isAllowedFragmentNode(node) {
|
|
4122
|
-
if (node.nodeType ===
|
|
6699
|
+
if (node.nodeType === TEXT_NODE2) {
|
|
4123
6700
|
return true;
|
|
4124
6701
|
}
|
|
4125
|
-
return
|
|
6702
|
+
return isElement3(node) && isAllowedInlineElement(node);
|
|
4126
6703
|
}
|
|
4127
6704
|
function allDescendantNodes(node) {
|
|
4128
|
-
const
|
|
4129
|
-
|
|
6705
|
+
const result = [];
|
|
6706
|
+
const stack = [{ node, depth: 1 }];
|
|
6707
|
+
while (stack.length > 0) {
|
|
6708
|
+
const top = stack.pop();
|
|
6709
|
+
if (top === void 0) {
|
|
6710
|
+
break;
|
|
6711
|
+
}
|
|
6712
|
+
if (top.depth > MAX_DEPTH) {
|
|
6713
|
+
throw new AdapterError(
|
|
6714
|
+
"MAX_DEPTH_EXCEEDED",
|
|
6715
|
+
"The translated value nests inline elements too deeply."
|
|
6716
|
+
);
|
|
6717
|
+
}
|
|
6718
|
+
for (const child of Array.from(top.node.childNodes)) {
|
|
6719
|
+
result.push(child);
|
|
6720
|
+
stack.push({ node: child, depth: top.depth + 1 });
|
|
6721
|
+
}
|
|
6722
|
+
}
|
|
6723
|
+
return result;
|
|
4130
6724
|
}
|
|
4131
6725
|
function hasDisallowedNode(root) {
|
|
4132
6726
|
return allDescendantNodes(root).some((node) => !isAllowedFragmentNode(node));
|
|
@@ -4152,7 +6746,7 @@ function sanitizeInlineAttributes(root) {
|
|
|
4152
6746
|
}
|
|
4153
6747
|
}
|
|
4154
6748
|
function fragmentNodes(parser, value) {
|
|
4155
|
-
|
|
6749
|
+
assertNoDoctype2(value);
|
|
4156
6750
|
try {
|
|
4157
6751
|
const root = parser.parseFromString(`<wrapper>${value}</wrapper>`, "text/xml").documentElement;
|
|
4158
6752
|
if (root === null || hasDisallowedNode(root)) {
|
|
@@ -4160,7 +6754,10 @@ function fragmentNodes(parser, value) {
|
|
|
4160
6754
|
}
|
|
4161
6755
|
sanitizeInlineAttributes(root);
|
|
4162
6756
|
return Array.from(root.childNodes);
|
|
4163
|
-
} catch {
|
|
6757
|
+
} catch (error) {
|
|
6758
|
+
if (error instanceof AdapterError) {
|
|
6759
|
+
throw error;
|
|
6760
|
+
}
|
|
4164
6761
|
return null;
|
|
4165
6762
|
}
|
|
4166
6763
|
}
|
|
@@ -4178,8 +6775,8 @@ function setTargetValue(doc, parser, element, value) {
|
|
|
4178
6775
|
}
|
|
4179
6776
|
}
|
|
4180
6777
|
async function serializeXliffEntries(entries, filePath, fs) {
|
|
4181
|
-
const { doc, root } = parseXml(await
|
|
4182
|
-
const parser = new xmldom.DOMParser({ onError:
|
|
6778
|
+
const { doc, root } = parseXml(await readDestination3(filePath, fs));
|
|
6779
|
+
const parser = new xmldom.DOMParser({ onError: onFatal3 });
|
|
4183
6780
|
for (const unit of walkUnits(root)) {
|
|
4184
6781
|
const entry = entries.get(unit.key);
|
|
4185
6782
|
if (entry !== void 0) {
|
|
@@ -4252,7 +6849,7 @@ function createYamlAdapter(fs = nodeAdapterFs) {
|
|
|
4252
6849
|
});
|
|
4253
6850
|
}
|
|
4254
6851
|
function createDefaultRegistry(fs = nodeAdapterFs) {
|
|
4255
|
-
return new AdapterRegistry().register(createI18nextJsonAdapter(fs)).register(createVueI18nJsonAdapter(fs)).register(createNextIntlJsonAdapter(fs)).register(createNgxTranslateJsonAdapter(fs)).register(createXliffAdapter(fs)).register(createYamlAdapter(fs)).register(createArbAdapter(fs)).register(createPropertiesAdapter(fs));
|
|
6852
|
+
return new AdapterRegistry().register(createI18nextJsonAdapter(fs)).register(createVueI18nJsonAdapter(fs)).register(createNextIntlJsonAdapter(fs)).register(createNgxTranslateJsonAdapter(fs)).register(createXliffAdapter(fs)).register(createYamlAdapter(fs)).register(createArbAdapter(fs)).register(createPropertiesAdapter(fs)).register(createAppleStringsAdapter(fs)).register(createAppleXcstringsAdapter(fs)).register(createAndroidXmlAdapter(fs)).register(createGettextAdapter(fs));
|
|
4256
6853
|
}
|
|
4257
6854
|
function enoent(path) {
|
|
4258
6855
|
return Object.assign(new Error(`ENOENT: no such file or directory, open '${path}'`), {
|
|
@@ -4639,7 +7236,7 @@ async function editEntry(input, deps = {}) {
|
|
|
4639
7236
|
`The key "${input.key}" was not found in the source resource.`
|
|
4640
7237
|
);
|
|
4641
7238
|
}
|
|
4642
|
-
return withLocaleWriteLock(cwd, locale, fs, async () => {
|
|
7239
|
+
return withLocaleWriteLock(cwd, writeLockKeyFor(config.format, locale), fs, async () => {
|
|
4643
7240
|
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
4644
7241
|
const gate = gateCandidateValue(sourceEntry, input.value, adapter);
|
|
4645
7242
|
if (!gate.accepted) {
|
|
@@ -4784,6 +7381,41 @@ function diffLocaleSnapshots(previous, current) {
|
|
|
4784
7381
|
return { added, changed, removed };
|
|
4785
7382
|
}
|
|
4786
7383
|
|
|
7384
|
+
// src/flow/locale-values.ts
|
|
7385
|
+
function mergeValues(source, target) {
|
|
7386
|
+
const keys = /* @__PURE__ */ new Set([...source.entries.keys(), ...target.entries.keys()]);
|
|
7387
|
+
const values = {};
|
|
7388
|
+
for (const key of keys) {
|
|
7389
|
+
const sourceEntry = source.entries.get(key);
|
|
7390
|
+
const targetEntry = target.entries.get(key);
|
|
7391
|
+
values[key] = {
|
|
7392
|
+
...sourceEntry !== void 0 ? { source: sourceEntry.value } : {},
|
|
7393
|
+
...targetEntry !== void 0 ? { target: targetEntry.value } : {}
|
|
7394
|
+
};
|
|
7395
|
+
}
|
|
7396
|
+
return values;
|
|
7397
|
+
}
|
|
7398
|
+
async function localeValues(input, deps = {}) {
|
|
7399
|
+
const config = input.config;
|
|
7400
|
+
const cwd = input.cwd ?? process.cwd();
|
|
7401
|
+
const fs = deps.fs ?? defaultFs;
|
|
7402
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry, deps.fs);
|
|
7403
|
+
const resolver = createLocalePathResolver(cwd, config);
|
|
7404
|
+
const source = await readSourceResource(config, resolver, fs, adapter);
|
|
7405
|
+
return Promise.all(
|
|
7406
|
+
selectLocales(config, input.locales).map(async (locale) => {
|
|
7407
|
+
const target = await readTargetResource({
|
|
7408
|
+
resolver,
|
|
7409
|
+
format: config.format,
|
|
7410
|
+
locale,
|
|
7411
|
+
adapter,
|
|
7412
|
+
fs
|
|
7413
|
+
});
|
|
7414
|
+
return { locale, values: mergeValues(source.resource, target) };
|
|
7415
|
+
})
|
|
7416
|
+
);
|
|
7417
|
+
}
|
|
7418
|
+
|
|
4787
7419
|
// src/flow/lock-state.ts
|
|
4788
7420
|
function toLockLocaleState(locale, keyCount, diff2) {
|
|
4789
7421
|
return {
|
|
@@ -4863,7 +7495,7 @@ async function retranslateEntry(input, deps = {}) {
|
|
|
4863
7495
|
);
|
|
4864
7496
|
}
|
|
4865
7497
|
const provider = selectProvider(config.provider, deps.createProvider);
|
|
4866
|
-
return withLocaleWriteLock(cwd, locale, fs, async () => {
|
|
7498
|
+
return withLocaleWriteLock(cwd, writeLockKeyFor(config.format, locale), fs, async () => {
|
|
4867
7499
|
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
4868
7500
|
const result = await provider.translateBatch(
|
|
4869
7501
|
buildTranslateRequest(
|
|
@@ -5915,7 +8547,7 @@ async function runLiveLocale(context, targetLocale) {
|
|
|
5915
8547
|
};
|
|
5916
8548
|
return withLocaleWriteLock(
|
|
5917
8549
|
context.cwd,
|
|
5918
|
-
targetLocale,
|
|
8550
|
+
writeLockKeyFor(context.config.format, targetLocale),
|
|
5919
8551
|
context.fs,
|
|
5920
8552
|
async () => {
|
|
5921
8553
|
const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
|
|
@@ -6030,7 +8662,7 @@ function resolveRunConcurrency(value, dryRun, config) {
|
|
|
6030
8662
|
}
|
|
6031
8663
|
return concurrency;
|
|
6032
8664
|
}
|
|
6033
|
-
async function
|
|
8665
|
+
async function translate3(input, deps = {}) {
|
|
6034
8666
|
const config = input.config;
|
|
6035
8667
|
const cwd = input.cwd ?? process.cwd();
|
|
6036
8668
|
const dryRun = input.dryRun ?? false;
|
|
@@ -6321,14 +8953,15 @@ function applyColumnGeometry(sheet) {
|
|
|
6321
8953
|
function writeRow(sheet, sheetRow) {
|
|
6322
8954
|
const row = sheet.addRow([]);
|
|
6323
8955
|
row.getCell(COLUMN.key).value = sheetRow.key;
|
|
6324
|
-
row.getCell(COLUMN.source).value = sheetRow.source;
|
|
6325
|
-
row.getCell(COLUMN.current).value = sheetRow.currentTarget;
|
|
8956
|
+
row.getCell(COLUMN.source).value = escapeFormulaLead(sheetRow.source);
|
|
8957
|
+
row.getCell(COLUMN.current).value = escapeFormulaLead(sheetRow.currentTarget);
|
|
6326
8958
|
row.getCell(COLUMN.status).value = sheetRow.status;
|
|
6327
8959
|
const translationCell = row.getCell(COLUMN.translation);
|
|
6328
8960
|
translationCell.numFmt = TEXT_NUMBER_FORMAT;
|
|
6329
|
-
|
|
8961
|
+
const translation = escapeFormulaLead(sheetRow.translation);
|
|
8962
|
+
translationCell.value = translation === "" ? null : translation;
|
|
6330
8963
|
row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
|
|
6331
|
-
row.getCell(COLUMN.context).value = sheetRow.context;
|
|
8964
|
+
row.getCell(COLUMN.context).value = escapeFormulaLead(sheetRow.context);
|
|
6332
8965
|
row.getCell(COLUMN.reviewStatus).value = sheetRow.reviewStatus;
|
|
6333
8966
|
row.getCell(COLUMN.reviewReasons).value = sheetRow.reviewReasons;
|
|
6334
8967
|
for (let column = COLUMN.key; column <= COLUMN.reviewReasons; column += 1) {
|
|
@@ -6639,7 +9272,7 @@ function readDelimited(input, options = {}) {
|
|
|
6639
9272
|
duplicateKeys: into.duplicates
|
|
6640
9273
|
};
|
|
6641
9274
|
}
|
|
6642
|
-
function
|
|
9275
|
+
function assertNoDoctype3(name, xml) {
|
|
6643
9276
|
if (/<!DOCTYPE/i.test(xml) || /<!ENTITY/i.test(xml)) {
|
|
6644
9277
|
throw new ExchangeError(
|
|
6645
9278
|
"WORKBOOK_INVALID",
|
|
@@ -6712,7 +9345,7 @@ async function guardWorkbookBytes(bytes, limits) {
|
|
|
6712
9345
|
const remaining = limits.maxDecompressedBytes - actualTotal;
|
|
6713
9346
|
const { raw, content } = await streamEntryBounded(file, remaining);
|
|
6714
9347
|
actualTotal += raw;
|
|
6715
|
-
|
|
9348
|
+
assertNoDoctype3(file.name, content);
|
|
6716
9349
|
}
|
|
6717
9350
|
}
|
|
6718
9351
|
function cellString(cell) {
|
|
@@ -6728,19 +9361,31 @@ function cellString(cell) {
|
|
|
6728
9361
|
}
|
|
6729
9362
|
return typeof cell.text === "string" ? cell.text : "";
|
|
6730
9363
|
}
|
|
9364
|
+
var FORMULA_GUARDED_COLUMNS = /* @__PURE__ */ new Set([
|
|
9365
|
+
COLUMN.source,
|
|
9366
|
+
COLUMN.current,
|
|
9367
|
+
COLUMN.translation,
|
|
9368
|
+
COLUMN.context
|
|
9369
|
+
]);
|
|
9370
|
+
var LEGACY_HEADER_COLUMN_COUNT = COLUMN.sourceHash;
|
|
6731
9371
|
function assertHeader(sheet) {
|
|
6732
9372
|
const header = sheet.getRow(HEADER_ROW);
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
9373
|
+
for (let column = 1; column <= LEGACY_HEADER_COLUMN_COUNT; column += 1) {
|
|
9374
|
+
const label = cellString(header.getCell(column));
|
|
9375
|
+
if (label !== HEADERS[column - 1]) {
|
|
9376
|
+
throw new ExchangeError(
|
|
9377
|
+
"WORKBOOK_INVALID",
|
|
9378
|
+
`The sheet "${sheet.name}" has an unexpected header label in column ${column} (expected "${HEADERS[column - 1]}").`
|
|
9379
|
+
);
|
|
9380
|
+
}
|
|
6740
9381
|
}
|
|
6741
9382
|
}
|
|
6742
9383
|
function sheetRowCells(row) {
|
|
6743
|
-
return HEADERS.map((_, index) =>
|
|
9384
|
+
return HEADERS.map((_, index) => {
|
|
9385
|
+
const column = index + 1;
|
|
9386
|
+
const value = cellString(row.getCell(column));
|
|
9387
|
+
return FORMULA_GUARDED_COLUMNS.has(column) ? unescapeFormulaLead(value) : value;
|
|
9388
|
+
});
|
|
6744
9389
|
}
|
|
6745
9390
|
function assertRowCellCap(row, sheetName, limits) {
|
|
6746
9391
|
if (row.cellCount > limits.maxCellsPerRow) {
|
|
@@ -6967,6 +9612,7 @@ async function exportWorkbook(input, deps = {}) {
|
|
|
6967
9612
|
await writeDelimitedFiles(fs, path$1, format, sheets);
|
|
6968
9613
|
} else {
|
|
6969
9614
|
const model = { sheets };
|
|
9615
|
+
await fs.mkdir?.(path.dirname(path$1));
|
|
6970
9616
|
await fs.writeBytes(path$1, await buildWorkbook(model));
|
|
6971
9617
|
}
|
|
6972
9618
|
return {
|
|
@@ -7127,7 +9773,12 @@ async function readDelimitedText(path, fs) {
|
|
|
7127
9773
|
async function collectDelimitedSources(path$1, config, fs, format) {
|
|
7128
9774
|
const single = await readDelimitedText(path$1, fs);
|
|
7129
9775
|
if (single !== void 0) {
|
|
7130
|
-
|
|
9776
|
+
const locale = path.basename(path$1, `.${format}`);
|
|
9777
|
+
return {
|
|
9778
|
+
sources: [{ locale, text: single }],
|
|
9779
|
+
staleLocales: [],
|
|
9780
|
+
expectedLocales: [locale]
|
|
9781
|
+
};
|
|
7131
9782
|
}
|
|
7132
9783
|
const exported = await readExportedLocales(fs, path$1, format);
|
|
7133
9784
|
const sources = [];
|
|
@@ -7149,7 +9800,7 @@ async function collectDelimitedSources(path$1, config, fs, format) {
|
|
|
7149
9800
|
`No ${format} file was found at ${path$1}, and it holds no <locale>.${format} file for any configured target locale.`
|
|
7150
9801
|
);
|
|
7151
9802
|
}
|
|
7152
|
-
return { sources, staleLocales };
|
|
9803
|
+
return { sources, staleLocales, expectedLocales: config.targetLocales };
|
|
7153
9804
|
}
|
|
7154
9805
|
function parseDelimitedSources(sources, format) {
|
|
7155
9806
|
const sheets = [];
|
|
@@ -7169,10 +9820,15 @@ async function readImportData(path, config, fs, format) {
|
|
|
7169
9820
|
const handoff = await collectDelimitedSources(path, config, fs, format);
|
|
7170
9821
|
return {
|
|
7171
9822
|
data: parseDelimitedSources(handoff.sources, format),
|
|
7172
|
-
staleLocales: handoff.staleLocales
|
|
9823
|
+
staleLocales: handoff.staleLocales,
|
|
9824
|
+
expectedLocales: handoff.expectedLocales
|
|
7173
9825
|
};
|
|
7174
9826
|
}
|
|
7175
|
-
return {
|
|
9827
|
+
return {
|
|
9828
|
+
data: await readWorkbook(await readWorkbookBytes(path, fs)),
|
|
9829
|
+
staleLocales: [],
|
|
9830
|
+
expectedLocales: config.targetLocales
|
|
9831
|
+
};
|
|
7176
9832
|
} catch (error) {
|
|
7177
9833
|
if (error instanceof SdkError) {
|
|
7178
9834
|
throw error;
|
|
@@ -7237,11 +9893,11 @@ var StaleHandoffFileError = class extends Error {
|
|
|
7237
9893
|
this.name = "StaleHandoffFileError";
|
|
7238
9894
|
}
|
|
7239
9895
|
};
|
|
7240
|
-
function absentLocaleFailures(
|
|
9896
|
+
function absentLocaleFailures(expectedLocales, sheets, format, staleLocales) {
|
|
7241
9897
|
const present = new Set(sheets.map((sheet) => sheet.locale));
|
|
7242
9898
|
const stale = new Set(staleLocales);
|
|
7243
9899
|
const failures = [];
|
|
7244
|
-
for (const locale of
|
|
9900
|
+
for (const locale of expectedLocales) {
|
|
7245
9901
|
if (present.has(locale)) {
|
|
7246
9902
|
continue;
|
|
7247
9903
|
}
|
|
@@ -7250,7 +9906,7 @@ function absentLocaleFailures(config, sheets, format, staleLocales) {
|
|
|
7250
9906
|
}
|
|
7251
9907
|
return failures;
|
|
7252
9908
|
}
|
|
7253
|
-
function
|
|
9909
|
+
function lineOf2(reported) {
|
|
7254
9910
|
return reported.line === void 0 ? {} : { line: reported.line };
|
|
7255
9911
|
}
|
|
7256
9912
|
async function runSheet(ctx, sheet, lock) {
|
|
@@ -7275,8 +9931,8 @@ async function runSheet(ctx, sheet, lock) {
|
|
|
7275
9931
|
baseline,
|
|
7276
9932
|
adapter: ctx.adapter,
|
|
7277
9933
|
sourceInvalidIcuKeys: ctx.sourceInvalidIcuKeys,
|
|
7278
|
-
malformedRows: ctx.malformedRows.filter((problem) => problem.locale === sheet.locale).map((problem) => ({ row: problem.row, column: problem.column, ...
|
|
7279
|
-
duplicateKeys: ctx.duplicateKeys.filter((duplicate) => duplicate.locale === sheet.locale).map((duplicate) => ({ key: duplicate.key, row: duplicate.row, ...
|
|
9934
|
+
malformedRows: ctx.malformedRows.filter((problem) => problem.locale === sheet.locale).map((problem) => ({ row: problem.row, column: problem.column, ...lineOf2(problem) })),
|
|
9935
|
+
duplicateKeys: ctx.duplicateKeys.filter((duplicate) => duplicate.locale === sheet.locale).map((duplicate) => ({ key: duplicate.key, row: duplicate.row, ...lineOf2(duplicate) }))
|
|
7280
9936
|
});
|
|
7281
9937
|
if (ctx.dryRun) {
|
|
7282
9938
|
return { summary, lockEntries: {}, cacheAdditions: {} };
|
|
@@ -7311,7 +9967,7 @@ async function importWorkbook(input, deps = {}) {
|
|
|
7311
9967
|
const resolver = createLocalePathResolver(cwd, config);
|
|
7312
9968
|
const source = await readSourceResource(config, resolver, fs, adapter);
|
|
7313
9969
|
const format = input.format ?? DEFAULT_EXCHANGE_FORMAT;
|
|
7314
|
-
const { data, staleLocales } = await readImportData(
|
|
9970
|
+
const { data, staleLocales, expectedLocales } = await readImportData(
|
|
7315
9971
|
path.resolve(cwd, input.workbook),
|
|
7316
9972
|
config,
|
|
7317
9973
|
fs,
|
|
@@ -7339,15 +9995,20 @@ async function importWorkbook(input, deps = {}) {
|
|
|
7339
9995
|
if (dryRun) {
|
|
7340
9996
|
summary = (await runSheet(ctx, sheet, lock)).summary;
|
|
7341
9997
|
} else {
|
|
7342
|
-
summary = await withLocaleWriteLock(
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
9998
|
+
summary = await withLocaleWriteLock(
|
|
9999
|
+
cwd,
|
|
10000
|
+
writeLockKeyFor(config.format, sheet.locale),
|
|
10001
|
+
fs,
|
|
10002
|
+
async () => {
|
|
10003
|
+
const result = await runSheet(ctx, sheet, lock);
|
|
10004
|
+
await updateLockFileLocale(cwd, fs, sheet.locale, {
|
|
10005
|
+
mode: "replace",
|
|
10006
|
+
entries: result.lockEntries
|
|
10007
|
+
});
|
|
10008
|
+
collectSheetAdditions(cacheAdditions, sheet.locale, result.cacheAdditions);
|
|
10009
|
+
return result.summary;
|
|
10010
|
+
}
|
|
10011
|
+
);
|
|
7351
10012
|
}
|
|
7352
10013
|
summaries.push(summary);
|
|
7353
10014
|
} catch (error) {
|
|
@@ -7357,7 +10018,7 @@ async function importWorkbook(input, deps = {}) {
|
|
|
7357
10018
|
summaries.push(failureSummary(sheet.locale, error));
|
|
7358
10019
|
}
|
|
7359
10020
|
}
|
|
7360
|
-
summaries.push(...absentLocaleFailures(
|
|
10021
|
+
summaries.push(...absentLocaleFailures(expectedLocales, data.sheets, format, staleLocales));
|
|
7361
10022
|
if (!dryRun) {
|
|
7362
10023
|
await feedTranslationMemory(cwd, fs, computeFingerprint(config), cacheAdditions);
|
|
7363
10024
|
}
|
|
@@ -7372,18 +10033,57 @@ async function loadLockFile(input = {}, deps = {}) {
|
|
|
7372
10033
|
return readLockFile(lockFilePath(cwd), fs);
|
|
7373
10034
|
}
|
|
7374
10035
|
|
|
10036
|
+
// src/redact.ts
|
|
10037
|
+
var REDACTED2 = "[REDACTED]";
|
|
10038
|
+
var KEY_PATTERNS2 = [
|
|
10039
|
+
/\bsk-[A-Za-z0-9_-]{8,}/g,
|
|
10040
|
+
/AIza[0-9A-Za-z_-]{35}/g,
|
|
10041
|
+
/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(?::fx)?/g
|
|
10042
|
+
];
|
|
10043
|
+
var PROVIDER_ENV_VAR_NAMES = [
|
|
10044
|
+
"ANTHROPIC_API_KEY",
|
|
10045
|
+
"OPENAI_API_KEY",
|
|
10046
|
+
"GEMINI_API_KEY",
|
|
10047
|
+
"DEEPL_API_KEY",
|
|
10048
|
+
"GOOGLE_TRANSLATE_API_KEY",
|
|
10049
|
+
"OPENAI_COMPATIBLE_API_KEY"
|
|
10050
|
+
];
|
|
10051
|
+
function escapeForRegExp2(value) {
|
|
10052
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10053
|
+
}
|
|
10054
|
+
function scrubConfiguredEnvValues(text) {
|
|
10055
|
+
let out = text;
|
|
10056
|
+
for (const name of PROVIDER_ENV_VAR_NAMES) {
|
|
10057
|
+
const value = process.env[name];
|
|
10058
|
+
if (value !== void 0 && value.length > 0) {
|
|
10059
|
+
out = out.replace(new RegExp(escapeForRegExp2(value), "g"), REDACTED2);
|
|
10060
|
+
}
|
|
10061
|
+
}
|
|
10062
|
+
return out;
|
|
10063
|
+
}
|
|
10064
|
+
function redact2(text) {
|
|
10065
|
+
let out = text;
|
|
10066
|
+
for (const pattern of KEY_PATTERNS2) {
|
|
10067
|
+
out = out.replace(pattern, REDACTED2);
|
|
10068
|
+
}
|
|
10069
|
+
return scrubConfiguredEnvValues(out);
|
|
10070
|
+
}
|
|
10071
|
+
|
|
7375
10072
|
// src/scaffolding.ts
|
|
7376
10073
|
var scaffoldingMetadata = {
|
|
7377
10074
|
/** The environment variable each scaffoldable provider reads its API key from. */
|
|
7378
10075
|
providerEnv: PROVIDER_ENV,
|
|
7379
|
-
/**
|
|
10076
|
+
/**
|
|
10077
|
+
* A reasonable default model to prefill per language-model provider. DeepL and Google Cloud
|
|
10078
|
+
* Translation have none, since neither takes a model.
|
|
10079
|
+
*/
|
|
7380
10080
|
scaffoldModels: SCAFFOLD_MODELS,
|
|
7381
10081
|
/**
|
|
7382
10082
|
* The option key each language-model provider takes its output token limit under, since they do
|
|
7383
10083
|
* not agree: Anthropic calls it `maxTokens` and the others `maxOutputTokens`. A generator that
|
|
7384
10084
|
* prefills a token limit must read the key from here rather than assume one, because
|
|
7385
10085
|
* {@link verbatraConfigSchema} validates each provider's options strictly and rejects the wrong
|
|
7386
|
-
* one. DeepL
|
|
10086
|
+
* one. DeepL and Google Cloud Translation have no entry, since neither takes a token limit.
|
|
7387
10087
|
*/
|
|
7388
10088
|
providerTokenLimitKeys: SCAFFOLD_TOKEN_LIMIT_KEYS,
|
|
7389
10089
|
/** Every i18n file format the SDK can read and write. */
|
|
@@ -7400,7 +10100,7 @@ var defaultCreateWatcher = (paths) => {
|
|
|
7400
10100
|
};
|
|
7401
10101
|
};
|
|
7402
10102
|
function defaultRunTranslate(deps) {
|
|
7403
|
-
return (input) =>
|
|
10103
|
+
return (input) => translate3(input, {
|
|
7404
10104
|
...deps.adapterRegistry !== void 0 ? { adapterRegistry: deps.adapterRegistry } : {},
|
|
7405
10105
|
...deps.createProvider !== void 0 ? { createProvider: deps.createProvider } : {},
|
|
7406
10106
|
...deps.fs !== void 0 ? { fs: deps.fs } : {}
|
|
@@ -7521,13 +10221,15 @@ exports.keyValue = keyValue;
|
|
|
7521
10221
|
exports.loadConfig = loadConfig;
|
|
7522
10222
|
exports.loadConfigWithMeta = loadConfigWithMeta;
|
|
7523
10223
|
exports.loadLockFile = loadLockFile;
|
|
10224
|
+
exports.localeValues = localeValues;
|
|
7524
10225
|
exports.lockState = lockState;
|
|
7525
10226
|
exports.readGlossaryFile = readGlossaryFile;
|
|
7526
10227
|
exports.readLocaleFileSnapshot = readLocaleFileSnapshot;
|
|
10228
|
+
exports.redact = redact2;
|
|
7527
10229
|
exports.retranslateEntry = retranslateEntry;
|
|
7528
10230
|
exports.runStatus = runStatus;
|
|
7529
10231
|
exports.scaffoldingMetadata = scaffoldingMetadata;
|
|
7530
|
-
exports.translate =
|
|
10232
|
+
exports.translate = translate3;
|
|
7531
10233
|
exports.updateGlossaryTerm = updateGlossaryTerm;
|
|
7532
10234
|
exports.verbatraConfigSchema = verbatraConfigSchema;
|
|
7533
10235
|
exports.watch = watch;
|