@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.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { dirname, resolve, join, basename, relative, isAbsolute, sep
|
|
1
|
+
import { dirname, resolve, join, basename, extname, relative, isAbsolute, sep } from 'path';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
import { mkdir, rm, access, open, writeFile, rename } from 'fs/promises';
|
|
5
|
-
import { existsSync } from 'fs';
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
6
7
|
import { cosmiconfig } from 'cosmiconfig';
|
|
7
8
|
import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
|
|
9
|
+
import { createRequire } from 'module';
|
|
8
10
|
import Anthropic from '@anthropic-ai/sdk';
|
|
9
11
|
import * as deepl from 'deepl-node';
|
|
10
|
-
import { createRequire } from 'module';
|
|
11
12
|
import log from 'loglevel';
|
|
12
13
|
import { GoogleGenAI } from '@google/genai';
|
|
13
14
|
import OpenAI from 'openai';
|
|
15
|
+
import { XMLSerializer, DOMParser, DOMImplementation } from '@xmldom/xmldom';
|
|
14
16
|
import { parse, TYPE } from '@formatjs/icu-messageformat-parser';
|
|
15
|
-
import { XMLSerializer, DOMParser } from '@xmldom/xmldom';
|
|
16
17
|
import { stringify, parse as parse$1 } from 'yaml';
|
|
17
18
|
import ExcelJS from 'exceljs';
|
|
18
19
|
import JSZip from 'jszip';
|
|
@@ -267,9 +268,20 @@ var defaultFs = {
|
|
|
267
268
|
await mkdir(path, { recursive: true });
|
|
268
269
|
}
|
|
269
270
|
};
|
|
271
|
+
|
|
272
|
+
// src/locale-path/shared-catalogue-format.ts
|
|
273
|
+
var SHARED_CATALOGUE_FORMATS = /* @__PURE__ */ new Set([
|
|
274
|
+
"apple-xcstrings"
|
|
275
|
+
]);
|
|
276
|
+
function isSharedCatalogueFormat(format) {
|
|
277
|
+
return SHARED_CATALOGUE_FORMATS.has(format);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/lock/locale-write-lock.ts
|
|
270
281
|
var LOCAL_DIR_NAME = ".verbatra-local";
|
|
271
282
|
var LOCK_FILE_GUARD_STEM = "_lockfile";
|
|
272
283
|
var GLOSSARY_GUARD_STEM = "_glossary";
|
|
284
|
+
var SHARED_CATALOGUE_STEM = "_catalogue";
|
|
273
285
|
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
274
286
|
var DEFAULT_ACQUIRE_TIMEOUT_MS = 10 * 6e4;
|
|
275
287
|
var WAIT_NOTICE_INTERVAL_MS = 1e3;
|
|
@@ -280,6 +292,9 @@ function lockPath(cwd, stem) {
|
|
|
280
292
|
function localeLockPath(cwd, locale) {
|
|
281
293
|
return lockPath(cwd, locale);
|
|
282
294
|
}
|
|
295
|
+
function writeLockKeyFor(format, locale) {
|
|
296
|
+
return isSharedCatalogueFormat(format) ? SHARED_CATALOGUE_STEM : locale;
|
|
297
|
+
}
|
|
283
298
|
function lockFileGuardPath(cwd) {
|
|
284
299
|
return lockPath(cwd, LOCK_FILE_GUARD_STEM);
|
|
285
300
|
}
|
|
@@ -500,6 +515,67 @@ async function updateGlossaryTerm(input, deps = {}) {
|
|
|
500
515
|
return entries;
|
|
501
516
|
});
|
|
502
517
|
}
|
|
518
|
+
var SELF_ALIASED_PACKAGES = ["@verbatra/sdk", "@verbatra/cli"];
|
|
519
|
+
var ENTRY_CONDITION_ORDER = ["import", "node", "default", "require"];
|
|
520
|
+
function resolvePathsFromRunningModule(packageName) {
|
|
521
|
+
return createRequire(import.meta.url).resolve.paths(packageName) ?? [];
|
|
522
|
+
}
|
|
523
|
+
function readPackageManifestFromDisk(packageRoot) {
|
|
524
|
+
try {
|
|
525
|
+
return JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
526
|
+
} catch {
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
var defaultModuleAliasDeps = {
|
|
531
|
+
resolvePaths: resolvePathsFromRunningModule,
|
|
532
|
+
readPackageManifest: readPackageManifestFromDisk
|
|
533
|
+
};
|
|
534
|
+
function resolveRootExportPath(manifest) {
|
|
535
|
+
const exportsField = manifest.exports;
|
|
536
|
+
if (typeof exportsField === "string") {
|
|
537
|
+
return exportsField;
|
|
538
|
+
}
|
|
539
|
+
if (typeof exportsField === "object" && exportsField !== null) {
|
|
540
|
+
const rootExport = exportsField["."];
|
|
541
|
+
if (typeof rootExport === "string") {
|
|
542
|
+
return rootExport;
|
|
543
|
+
}
|
|
544
|
+
if (typeof rootExport === "object" && rootExport !== null) {
|
|
545
|
+
for (const condition of ENTRY_CONDITION_ORDER) {
|
|
546
|
+
const candidate = rootExport[condition];
|
|
547
|
+
if (typeof candidate === "string") {
|
|
548
|
+
return candidate;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return manifest.main;
|
|
554
|
+
}
|
|
555
|
+
function findPackageEntry(packageName, deps) {
|
|
556
|
+
for (const candidateDir of deps.resolvePaths(packageName)) {
|
|
557
|
+
const packageRoot = join(candidateDir, packageName);
|
|
558
|
+
const manifest = deps.readPackageManifest(packageRoot);
|
|
559
|
+
if (manifest === void 0) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const relativeEntry = resolveRootExportPath(manifest);
|
|
563
|
+
if (relativeEntry !== void 0) {
|
|
564
|
+
return join(packageRoot, relativeEntry);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return void 0;
|
|
568
|
+
}
|
|
569
|
+
function resolveSelfPackageAliases(deps = defaultModuleAliasDeps) {
|
|
570
|
+
const aliases = {};
|
|
571
|
+
for (const packageName of SELF_ALIASED_PACKAGES) {
|
|
572
|
+
const entry = findPackageEntry(packageName, deps);
|
|
573
|
+
if (entry !== void 0) {
|
|
574
|
+
aliases[packageName] = entry;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return aliases;
|
|
578
|
+
}
|
|
503
579
|
async function resolveGlossary(glossary, baseDir, fs) {
|
|
504
580
|
if (glossary === void 0) {
|
|
505
581
|
return { glossary: void 0, provenance: { source: "none" } };
|
|
@@ -581,7 +657,11 @@ var SUPPORTED_FORMATS = [
|
|
|
581
657
|
"xliff",
|
|
582
658
|
"yaml",
|
|
583
659
|
"arb",
|
|
584
|
-
"properties"
|
|
660
|
+
"properties",
|
|
661
|
+
"apple-strings",
|
|
662
|
+
"apple-xcstrings",
|
|
663
|
+
"android-xml",
|
|
664
|
+
"gettext-po"
|
|
585
665
|
];
|
|
586
666
|
var supportedFormatSchema = z.enum(SUPPORTED_FORMATS);
|
|
587
667
|
var translationEntrySchema = z.object({
|
|
@@ -1282,7 +1362,8 @@ var PROVIDER_ENV = {
|
|
|
1282
1362
|
anthropic: "ANTHROPIC_API_KEY",
|
|
1283
1363
|
openai: "OPENAI_API_KEY",
|
|
1284
1364
|
gemini: "GEMINI_API_KEY",
|
|
1285
|
-
deepl: "DEEPL_API_KEY"
|
|
1365
|
+
deepl: "DEEPL_API_KEY",
|
|
1366
|
+
"google-translate": "GOOGLE_TRANSLATE_API_KEY"
|
|
1286
1367
|
};
|
|
1287
1368
|
function readRequiredEnv(name) {
|
|
1288
1369
|
const value = process.env[name];
|
|
@@ -1303,6 +1384,9 @@ function requireGeminiKey() {
|
|
|
1303
1384
|
function requireDeepLKey() {
|
|
1304
1385
|
return readRequiredEnv(PROVIDER_ENV.deepl);
|
|
1305
1386
|
}
|
|
1387
|
+
function requireGoogleTranslateKey() {
|
|
1388
|
+
return readRequiredEnv(PROVIDER_ENV["google-translate"]);
|
|
1389
|
+
}
|
|
1306
1390
|
var OPENAI_COMPATIBLE_ENV_VAR = "OPENAI_COMPATIBLE_API_KEY";
|
|
1307
1391
|
var OPENAI_COMPATIBLE_KEY_PLACEHOLDER = "local";
|
|
1308
1392
|
function resolveOpenAiCompatibleKey(customEnvVar) {
|
|
@@ -1843,11 +1927,287 @@ function createMechanism2(client, config) {
|
|
|
1843
1927
|
}
|
|
1844
1928
|
};
|
|
1845
1929
|
}
|
|
1930
|
+
var googleTranslateConfigSchema = z.object({}).extend(requestTimeoutConfigSchema.shape);
|
|
1931
|
+
var GOOGLE_TRANSLATE_ENDPOINT = "https://translation.googleapis.com/language/translate/v2";
|
|
1932
|
+
var GOOGLE_TRANSLATE_ENDPOINT_HOST = "translation.googleapis.com";
|
|
1933
|
+
async function parseJsonBody(response) {
|
|
1934
|
+
try {
|
|
1935
|
+
return await response.json();
|
|
1936
|
+
} catch {
|
|
1937
|
+
return void 0;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function createDefaultClient4() {
|
|
1941
|
+
const apiKey = requireGoogleTranslateKey();
|
|
1942
|
+
const client = {
|
|
1943
|
+
translate: async (texts, sourceLang, targetLang, signal) => {
|
|
1944
|
+
const response = await fetch(
|
|
1945
|
+
`${GOOGLE_TRANSLATE_ENDPOINT}?key=${encodeURIComponent(apiKey)}`,
|
|
1946
|
+
{
|
|
1947
|
+
method: "POST",
|
|
1948
|
+
headers: { "content-type": "application/json" },
|
|
1949
|
+
body: JSON.stringify({
|
|
1950
|
+
q: texts,
|
|
1951
|
+
source: sourceLang,
|
|
1952
|
+
target: targetLang,
|
|
1953
|
+
format: "text"
|
|
1954
|
+
}),
|
|
1955
|
+
signal
|
|
1956
|
+
}
|
|
1957
|
+
);
|
|
1958
|
+
return { status: response.status, body: await parseJsonBody(response) };
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
return { client };
|
|
1962
|
+
}
|
|
1963
|
+
var GOOGLE_TRANSLATE_MAX_REQUEST_BYTES = 100 * 1024;
|
|
1964
|
+
var GOOGLE_TRANSLATE_PAYLOAD_OVERHEAD_RESERVE_BYTES = 2 * 1024;
|
|
1965
|
+
var GOOGLE_TRANSLATE_MAX_TEXT_PAYLOAD_BYTES = GOOGLE_TRANSLATE_MAX_REQUEST_BYTES - GOOGLE_TRANSLATE_PAYLOAD_OVERHEAD_RESERVE_BYTES;
|
|
1966
|
+
var textEncoder = new TextEncoder();
|
|
1967
|
+
function estimateJsonBytes(text) {
|
|
1968
|
+
return textEncoder.encode(JSON.stringify(text)).length;
|
|
1969
|
+
}
|
|
1970
|
+
function chunkTextsForGoogleTranslate(texts) {
|
|
1971
|
+
const chunks = [];
|
|
1972
|
+
let current = [];
|
|
1973
|
+
let currentBytes = 0;
|
|
1974
|
+
for (const text of texts) {
|
|
1975
|
+
const textBytes = estimateJsonBytes(text);
|
|
1976
|
+
const startsNewChunk = current.length > 0 && currentBytes + textBytes > GOOGLE_TRANSLATE_MAX_TEXT_PAYLOAD_BYTES;
|
|
1977
|
+
if (startsNewChunk) {
|
|
1978
|
+
chunks.push(current);
|
|
1979
|
+
current = [];
|
|
1980
|
+
currentBytes = 0;
|
|
1981
|
+
}
|
|
1982
|
+
current.push(text);
|
|
1983
|
+
currentBytes += textBytes;
|
|
1984
|
+
}
|
|
1985
|
+
if (current.length > 0) {
|
|
1986
|
+
chunks.push(current);
|
|
1987
|
+
}
|
|
1988
|
+
return chunks;
|
|
1989
|
+
}
|
|
1990
|
+
var BCP47_LIKE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
1991
|
+
function assertValidGoogleTranslateLocale(locale, role) {
|
|
1992
|
+
if (!BCP47_LIKE.test(locale)) {
|
|
1993
|
+
throw new ProviderError(
|
|
1994
|
+
"INVALID_REQUEST",
|
|
1995
|
+
`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").`
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
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.";
|
|
2000
|
+
function partitionByPlaceholders2(entries) {
|
|
2001
|
+
const protectable = [];
|
|
2002
|
+
const unprotectable = [];
|
|
2003
|
+
for (const entry of entries) {
|
|
2004
|
+
if (entry.placeholders.length > 0) {
|
|
2005
|
+
unprotectable.push(entry);
|
|
2006
|
+
} else {
|
|
2007
|
+
protectable.push(entry);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
return { protectable, unprotectable };
|
|
2011
|
+
}
|
|
2012
|
+
var FORMALITY_DOWNGRADED_MESSAGE2 = "Formality was not applied: Cloud Translation Basic (v2) has no formality control.";
|
|
2013
|
+
var GLOSSARY_IGNORED_MESSAGE2 = "The supplied glossary term map was not applied: Cloud Translation Basic (v2) does not support a glossary.";
|
|
2014
|
+
function buildTranslateNotices(input) {
|
|
2015
|
+
const notices = [];
|
|
2016
|
+
if (input.tone === "formal" || input.tone === "informal") {
|
|
2017
|
+
notices.push({ code: "FORMALITY_DOWNGRADED", message: FORMALITY_DOWNGRADED_MESSAGE2 });
|
|
2018
|
+
}
|
|
2019
|
+
if (input.genericGlossarySupplied) {
|
|
2020
|
+
notices.push({ code: "GLOSSARY_IGNORED", message: GLOSSARY_IGNORED_MESSAGE2 });
|
|
2021
|
+
}
|
|
2022
|
+
return notices;
|
|
2023
|
+
}
|
|
2024
|
+
var successSchema = z.object({
|
|
2025
|
+
data: z.object({
|
|
2026
|
+
translations: z.array(z.object({ translatedText: z.string() }))
|
|
2027
|
+
})
|
|
2028
|
+
});
|
|
2029
|
+
var errorDetailSchema = z.object({
|
|
2030
|
+
reason: z.string().optional()
|
|
2031
|
+
});
|
|
2032
|
+
var errorSchema = z.object({
|
|
2033
|
+
error: z.object({
|
|
2034
|
+
errors: z.array(errorDetailSchema).optional()
|
|
2035
|
+
})
|
|
2036
|
+
});
|
|
2037
|
+
var QUOTA_REASONS = /* @__PURE__ */ new Set([
|
|
2038
|
+
"dailyLimitExceeded",
|
|
2039
|
+
"dailyLimitExceededUnreg",
|
|
2040
|
+
"userRateLimitExceeded",
|
|
2041
|
+
"rateLimitExceeded",
|
|
2042
|
+
"quotaExceeded"
|
|
2043
|
+
]);
|
|
2044
|
+
var AUTH_FAILED_STATUSES2 = /* @__PURE__ */ new Set([401, 403]);
|
|
2045
|
+
var SERVER_OUTAGE_MIN_STATUS2 = 500;
|
|
2046
|
+
var SERVER_OUTAGE_MAX_STATUS2 = 599;
|
|
2047
|
+
var RATE_LIMITED_STATUS2 = 429;
|
|
2048
|
+
var INVALID_REQUEST_STATUS = 400;
|
|
2049
|
+
var MALFORMED_RESPONSE_MESSAGE = "Google Cloud Translation returned a response verbatra could not parse.";
|
|
2050
|
+
var MISMATCH_MESSAGE2 = "The provider returned a mismatched number of translations.";
|
|
2051
|
+
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.";
|
|
2052
|
+
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.";
|
|
2053
|
+
var INVALID_REQUEST_MESSAGE = "Google Cloud Translation rejected the request as malformed or unsupported: check the source and target locale codes and retry.";
|
|
2054
|
+
var PROVIDER_UNAVAILABLE_MESSAGE2 = "Google Cloud Translation is currently unavailable.";
|
|
2055
|
+
var PROVIDER_ERROR_MESSAGE = "The translation provider request failed.";
|
|
2056
|
+
function extractReasons(body) {
|
|
2057
|
+
const parsed = errorSchema.safeParse(body);
|
|
2058
|
+
if (!parsed.success) {
|
|
2059
|
+
return [];
|
|
2060
|
+
}
|
|
2061
|
+
return (parsed.data.error.errors ?? []).map((detail) => detail.reason).filter((reason) => reason !== void 0);
|
|
2062
|
+
}
|
|
2063
|
+
function classifyErrorStatus(status, reasons) {
|
|
2064
|
+
if (status === RATE_LIMITED_STATUS2) {
|
|
2065
|
+
return { code: "RATE_LIMITED", message: RATE_LIMITED_MESSAGE2 };
|
|
2066
|
+
}
|
|
2067
|
+
if (status === INVALID_REQUEST_STATUS) {
|
|
2068
|
+
return { code: "INVALID_REQUEST", message: INVALID_REQUEST_MESSAGE };
|
|
2069
|
+
}
|
|
2070
|
+
if (AUTH_FAILED_STATUSES2.has(status)) {
|
|
2071
|
+
if (reasons.some((reason) => QUOTA_REASONS.has(reason))) {
|
|
2072
|
+
return { code: "RATE_LIMITED", message: RATE_LIMITED_MESSAGE2 };
|
|
2073
|
+
}
|
|
2074
|
+
return { code: "AUTH_FAILED", message: AUTH_FAILED_MESSAGE2 };
|
|
2075
|
+
}
|
|
2076
|
+
if (status >= SERVER_OUTAGE_MIN_STATUS2 && status <= SERVER_OUTAGE_MAX_STATUS2) {
|
|
2077
|
+
return { code: "PROVIDER_UNAVAILABLE", message: PROVIDER_UNAVAILABLE_MESSAGE2 };
|
|
2078
|
+
}
|
|
2079
|
+
return { code: "PROVIDER_ERROR", message: PROVIDER_ERROR_MESSAGE };
|
|
2080
|
+
}
|
|
2081
|
+
function isSuccessStatus(status) {
|
|
2082
|
+
return status >= 200 && status < 300;
|
|
2083
|
+
}
|
|
2084
|
+
function parseGoogleTranslateHttpResult(status, body) {
|
|
2085
|
+
if (isSuccessStatus(status)) {
|
|
2086
|
+
const parsed = successSchema.safeParse(body);
|
|
2087
|
+
if (!parsed.success) {
|
|
2088
|
+
throw new ProviderError("INVALID_RESPONSE", MALFORMED_RESPONSE_MESSAGE);
|
|
2089
|
+
}
|
|
2090
|
+
return parsed.data.data.translations.map((translation) => translation.translatedText);
|
|
2091
|
+
}
|
|
2092
|
+
const { code, message } = classifyErrorStatus(status, extractReasons(body));
|
|
2093
|
+
throw new ProviderError(code, message);
|
|
2094
|
+
}
|
|
2095
|
+
function zipResults2(entries, translatedTexts) {
|
|
2096
|
+
const values = /* @__PURE__ */ new Map();
|
|
2097
|
+
const integrityInputs = [];
|
|
2098
|
+
const resultIter = translatedTexts[Symbol.iterator]();
|
|
2099
|
+
for (const entry of entries) {
|
|
2100
|
+
const next = resultIter.next();
|
|
2101
|
+
if (next.done === true) {
|
|
2102
|
+
throw new ProviderError("INVALID_RESPONSE", MISMATCH_MESSAGE2);
|
|
2103
|
+
}
|
|
2104
|
+
const translatedValue = next.value;
|
|
2105
|
+
values.set(entry.key, translatedValue);
|
|
2106
|
+
integrityInputs.push({ key: entry.key, sourceValue: entry.value, translatedValue });
|
|
2107
|
+
}
|
|
2108
|
+
if (resultIter.next().done === false) {
|
|
2109
|
+
throw new ProviderError("INVALID_RESPONSE", MISMATCH_MESSAGE2);
|
|
2110
|
+
}
|
|
2111
|
+
return { values, integrityInputs };
|
|
2112
|
+
}
|
|
2113
|
+
var PROVIDER_ID4 = "google-translate";
|
|
2114
|
+
function createGoogleTranslateProvider(config, deps = {}) {
|
|
2115
|
+
const validConfig = googleTranslateConfigSchema.parse(config);
|
|
2116
|
+
const bundle = resolveClient2(deps);
|
|
2117
|
+
return {
|
|
2118
|
+
id: PROVIDER_ID4,
|
|
2119
|
+
kind: "machine-translation",
|
|
2120
|
+
supportsGlossary: false,
|
|
2121
|
+
translateBatch: (request) => translate2(bundle, validConfig, request)
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
function resolveClient2(deps) {
|
|
2125
|
+
if (deps.client !== void 0) {
|
|
2126
|
+
return { client: deps.client };
|
|
2127
|
+
}
|
|
2128
|
+
return createDefaultClient4();
|
|
2129
|
+
}
|
|
2130
|
+
async function translate2(bundle, config, request) {
|
|
2131
|
+
const data = validateRequest(request);
|
|
2132
|
+
assertValidGoogleTranslateLocale(data.sourceLocale, "source");
|
|
2133
|
+
assertValidGoogleTranslateLocale(data.targetLocale, "target");
|
|
2134
|
+
const { protectable, unprotectable } = partitionByPlaceholders2(data.entries);
|
|
2135
|
+
const genericGlossarySupplied = request.glossary !== void 0 && Object.keys(request.glossary).length > 0;
|
|
2136
|
+
const notices = buildTranslateNotices({
|
|
2137
|
+
...data.tone !== void 0 ? { tone: data.tone } : {},
|
|
2138
|
+
genericGlossarySupplied
|
|
2139
|
+
});
|
|
2140
|
+
const { values, integrity } = await translateProtectable2(
|
|
2141
|
+
bundle.client,
|
|
2142
|
+
data,
|
|
2143
|
+
protectable,
|
|
2144
|
+
request.extractPlaceholders,
|
|
2145
|
+
request.comparePlaceholders,
|
|
2146
|
+
config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
2147
|
+
request.signal
|
|
2148
|
+
);
|
|
2149
|
+
if (unprotectable.length > 0) {
|
|
2150
|
+
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE2 });
|
|
2151
|
+
}
|
|
2152
|
+
const reviewFlags = applyProviderDegraded(
|
|
2153
|
+
buildEntryReviewFlags(
|
|
2154
|
+
protectable,
|
|
2155
|
+
values,
|
|
2156
|
+
integrity,
|
|
2157
|
+
data.sourceLocale,
|
|
2158
|
+
data.targetLocale,
|
|
2159
|
+
request.glossary
|
|
2160
|
+
),
|
|
2161
|
+
notices,
|
|
2162
|
+
[...values.keys()]
|
|
2163
|
+
);
|
|
2164
|
+
return { values, integrity, notices, reviewFlags };
|
|
2165
|
+
}
|
|
2166
|
+
async function translateProtectable2(client, data, protectable, extract, compare, timeoutMs, signal) {
|
|
2167
|
+
if (protectable.length === 0) {
|
|
2168
|
+
return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
|
|
2169
|
+
}
|
|
2170
|
+
const texts = protectable.map((entry) => entry.value);
|
|
2171
|
+
const translatedTexts = await callClientChunked2(
|
|
2172
|
+
client,
|
|
2173
|
+
texts,
|
|
2174
|
+
data.sourceLocale,
|
|
2175
|
+
data.targetLocale,
|
|
2176
|
+
timeoutMs,
|
|
2177
|
+
signal
|
|
2178
|
+
);
|
|
2179
|
+
const { values, integrityInputs } = zipResults2(protectable, translatedTexts);
|
|
2180
|
+
const integrity = checkBatchIntegrity(integrityInputs, extract, compare);
|
|
2181
|
+
return { values, integrity };
|
|
2182
|
+
}
|
|
2183
|
+
function callClient3(client, texts, sourceLang, targetLang, timeoutMs, signal) {
|
|
2184
|
+
return withRequestTimeout(
|
|
2185
|
+
timeoutMs,
|
|
2186
|
+
signal,
|
|
2187
|
+
(guardedSignal) => client.translate(texts, sourceLang, targetLang, guardedSignal),
|
|
2188
|
+
{ endpointHost: GOOGLE_TRANSLATE_ENDPOINT_HOST }
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
async function callClientChunked2(client, texts, sourceLang, targetLang, timeoutMs, signal) {
|
|
2192
|
+
const results = [];
|
|
2193
|
+
for (const chunk2 of chunkTextsForGoogleTranslate(texts)) {
|
|
2194
|
+
const { status, body } = await callClient3(
|
|
2195
|
+
client,
|
|
2196
|
+
chunk2,
|
|
2197
|
+
sourceLang,
|
|
2198
|
+
targetLang,
|
|
2199
|
+
timeoutMs,
|
|
2200
|
+
signal
|
|
2201
|
+
);
|
|
2202
|
+
results.push(...parseGoogleTranslateHttpResult(status, body));
|
|
2203
|
+
}
|
|
2204
|
+
return results;
|
|
2205
|
+
}
|
|
1846
2206
|
var openAiConfigSchema = z.object({
|
|
1847
2207
|
model: z.string().min(1),
|
|
1848
2208
|
maxOutputTokens: z.number().int().positive()
|
|
1849
2209
|
}).extend(requestTimeoutConfigSchema.shape);
|
|
1850
|
-
function
|
|
2210
|
+
function createDefaultClient5() {
|
|
1851
2211
|
const sdk = new OpenAI({ apiKey: requireOpenAiKey(), logLevel: "off" });
|
|
1852
2212
|
return {
|
|
1853
2213
|
chat: {
|
|
@@ -1970,13 +2330,13 @@ function extractOpenAiResult(completion, tolerant = false) {
|
|
|
1970
2330
|
const usage = toUsage(completion.usage?.prompt_tokens, completion.usage?.completion_tokens);
|
|
1971
2331
|
return usage === void 0 ? { raw } : { raw, usage };
|
|
1972
2332
|
}
|
|
1973
|
-
var
|
|
2333
|
+
var PROVIDER_ID5 = "openai";
|
|
1974
2334
|
function createOpenAiProvider(config, deps = {}) {
|
|
1975
2335
|
const validConfig = openAiConfigSchema.parse(config);
|
|
1976
|
-
const client = deps.client ??
|
|
2336
|
+
const client = deps.client ?? createDefaultClient5();
|
|
1977
2337
|
const mechanism = createMechanism3(client, validConfig);
|
|
1978
2338
|
return {
|
|
1979
|
-
id:
|
|
2339
|
+
id: PROVIDER_ID5,
|
|
1980
2340
|
kind: "llm",
|
|
1981
2341
|
supportsGlossary: true,
|
|
1982
2342
|
translateBatch: (request) => runLlmTranslation(request, mechanism)
|
|
@@ -1987,12 +2347,12 @@ function createMechanism3(client, config) {
|
|
|
1987
2347
|
return {
|
|
1988
2348
|
translate: async ({ payloadJson, signal }) => {
|
|
1989
2349
|
const body = buildOpenAiRequest(config, payloadJson);
|
|
1990
|
-
const completion = await
|
|
2350
|
+
const completion = await callClient4(client, body, timeoutMs, signal);
|
|
1991
2351
|
return extractOpenAiResult(completion);
|
|
1992
2352
|
}
|
|
1993
2353
|
};
|
|
1994
2354
|
}
|
|
1995
|
-
function
|
|
2355
|
+
function callClient4(client, body, timeoutMs, signal) {
|
|
1996
2356
|
return withRequestTimeout(
|
|
1997
2357
|
timeoutMs,
|
|
1998
2358
|
signal,
|
|
@@ -2021,7 +2381,7 @@ function endpointContextOf(baseUrl) {
|
|
|
2021
2381
|
return void 0;
|
|
2022
2382
|
}
|
|
2023
2383
|
}
|
|
2024
|
-
function
|
|
2384
|
+
function createDefaultClient6(config) {
|
|
2025
2385
|
const sdk = new OpenAI({
|
|
2026
2386
|
apiKey: resolveOpenAiCompatibleKey(config.apiKeyEnvVar),
|
|
2027
2387
|
baseURL: config.baseUrl,
|
|
@@ -2038,13 +2398,13 @@ function createDefaultClient5(config) {
|
|
|
2038
2398
|
}
|
|
2039
2399
|
};
|
|
2040
2400
|
}
|
|
2041
|
-
var
|
|
2401
|
+
var PROVIDER_ID6 = "openai-compatible";
|
|
2042
2402
|
function createOpenAiCompatibleProvider(config, deps = {}) {
|
|
2043
2403
|
const validConfig = openAiCompatibleConfigSchema.parse(config);
|
|
2044
|
-
const client = deps.client ??
|
|
2404
|
+
const client = deps.client ?? createDefaultClient6(validConfig);
|
|
2045
2405
|
const mechanism = createMechanism4(client, validConfig);
|
|
2046
2406
|
return {
|
|
2047
|
-
id:
|
|
2407
|
+
id: PROVIDER_ID6,
|
|
2048
2408
|
kind: "llm",
|
|
2049
2409
|
supportsGlossary: true,
|
|
2050
2410
|
translateBatch: (request) => runLlmTranslation(request, mechanism)
|
|
@@ -2056,12 +2416,12 @@ function createMechanism4(client, config) {
|
|
|
2056
2416
|
return {
|
|
2057
2417
|
translate: async ({ payloadJson, signal }) => {
|
|
2058
2418
|
const body = buildOpenAiRequest(config, payloadJson, "strict-schema", "max_tokens");
|
|
2059
|
-
const completion = await
|
|
2419
|
+
const completion = await callClient5(client, body, timeoutMs, signal, endpoint);
|
|
2060
2420
|
return extractOpenAiResult(completion, true);
|
|
2061
2421
|
}
|
|
2062
2422
|
};
|
|
2063
2423
|
}
|
|
2064
|
-
function
|
|
2424
|
+
function callClient5(client, body, timeoutMs, signal, endpoint) {
|
|
2065
2425
|
return withRequestTimeout(
|
|
2066
2426
|
timeoutMs,
|
|
2067
2427
|
signal,
|
|
@@ -2084,6 +2444,10 @@ var providerConfigSchema = z.discriminatedUnion("id", [
|
|
|
2084
2444
|
z.object({ id: z.literal("openai"), options: openAiConfigSchema.strict() }),
|
|
2085
2445
|
z.object({ id: z.literal("gemini"), options: geminiConfigSchema.strict() }),
|
|
2086
2446
|
z.object({ id: z.literal("deepl"), options: deepLConfigSchema.strict() }),
|
|
2447
|
+
z.object({
|
|
2448
|
+
id: z.literal("google-translate"),
|
|
2449
|
+
options: googleTranslateConfigSchema.strict()
|
|
2450
|
+
}),
|
|
2087
2451
|
z.object({
|
|
2088
2452
|
id: z.literal("openai-compatible"),
|
|
2089
2453
|
options: openAiCompatibleConfigSchema.strict()
|
|
@@ -2094,6 +2458,7 @@ var providerFactories = {
|
|
|
2094
2458
|
openai: (options) => createOpenAiProvider(options),
|
|
2095
2459
|
gemini: (options) => createGeminiProvider(options),
|
|
2096
2460
|
deepl: (options) => createDeepLProvider(options),
|
|
2461
|
+
"google-translate": (options) => createGoogleTranslateProvider(options),
|
|
2097
2462
|
"openai-compatible": (options) => createOpenAiCompatibleProvider(options)
|
|
2098
2463
|
};
|
|
2099
2464
|
var PROVIDER_IDS = Object.keys(providerFactories);
|
|
@@ -2136,10 +2501,16 @@ var verbatraConfigSchema = z.strictObject({
|
|
|
2136
2501
|
maxBatchSize: z.number().int().positive().optional(),
|
|
2137
2502
|
maxTokens: z.number().int().positive().optional(),
|
|
2138
2503
|
budgetBehavior: z.enum(["warn", "stop"]).optional()
|
|
2139
|
-
}).refine(
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2504
|
+
}).refine(
|
|
2505
|
+
(config) => {
|
|
2506
|
+
const sourceKey = config.sourceLocale.toLowerCase();
|
|
2507
|
+
return !config.targetLocales.some((locale) => locale.toLowerCase() === sourceKey);
|
|
2508
|
+
},
|
|
2509
|
+
{
|
|
2510
|
+
message: "targetLocales must not include the source locale",
|
|
2511
|
+
path: ["targetLocales"]
|
|
2512
|
+
}
|
|
2513
|
+
).refine((config) => findCaseInsensitiveDuplicate(config.targetLocales) === void 0, {
|
|
2143
2514
|
error: (issue) => {
|
|
2144
2515
|
const duplicate = findCaseInsensitiveDuplicate(
|
|
2145
2516
|
issue.input.targetLocales
|
|
@@ -2164,6 +2535,50 @@ var SEARCH_PLACES = [
|
|
|
2164
2535
|
`${MODULE_NAME}.config.cjs`,
|
|
2165
2536
|
`${MODULE_NAME}.config.ts`
|
|
2166
2537
|
];
|
|
2538
|
+
function isAncestorOrSelf(ancestor, startDir) {
|
|
2539
|
+
const target = resolve(ancestor);
|
|
2540
|
+
let dir = resolve(startDir);
|
|
2541
|
+
while (true) {
|
|
2542
|
+
if (dir === target) {
|
|
2543
|
+
return true;
|
|
2544
|
+
}
|
|
2545
|
+
const parent = dirname(dir);
|
|
2546
|
+
if (parent === dir) {
|
|
2547
|
+
return false;
|
|
2548
|
+
}
|
|
2549
|
+
dir = parent;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
function findSearchStopDir(startDir) {
|
|
2553
|
+
let dir = resolve(startDir);
|
|
2554
|
+
while (true) {
|
|
2555
|
+
if (existsSync(join(dir, ".git"))) {
|
|
2556
|
+
return dir;
|
|
2557
|
+
}
|
|
2558
|
+
const parent = dirname(dir);
|
|
2559
|
+
if (parent === dir) {
|
|
2560
|
+
break;
|
|
2561
|
+
}
|
|
2562
|
+
dir = parent;
|
|
2563
|
+
}
|
|
2564
|
+
const home = resolve(homedir());
|
|
2565
|
+
return isAncestorOrSelf(home, startDir) ? home : resolve(startDir);
|
|
2566
|
+
}
|
|
2567
|
+
function collectSearchChain(startDir, stopDir) {
|
|
2568
|
+
const stop = resolve(stopDir);
|
|
2569
|
+
const chain = /* @__PURE__ */ new Set();
|
|
2570
|
+
let dir = resolve(startDir);
|
|
2571
|
+
while (dir !== stop) {
|
|
2572
|
+
chain.add(dir);
|
|
2573
|
+
const parent = dirname(dir);
|
|
2574
|
+
if (parent === dir) {
|
|
2575
|
+
break;
|
|
2576
|
+
}
|
|
2577
|
+
dir = parent;
|
|
2578
|
+
}
|
|
2579
|
+
chain.add(stop);
|
|
2580
|
+
return chain;
|
|
2581
|
+
}
|
|
2167
2582
|
function formatIssues(error) {
|
|
2168
2583
|
return error.issues.map((issue) => {
|
|
2169
2584
|
const path = issue.path.join(".");
|
|
@@ -2213,20 +2628,30 @@ async function loadConfigWithMeta(options = {}) {
|
|
|
2213
2628
|
const { config: config2, glossary: glossary2 } = await finalizeConfig(parsed2, options.cwd ?? process.cwd(), fs);
|
|
2214
2629
|
return { config: config2, source: { kind: "override" }, glossary: glossary2 };
|
|
2215
2630
|
}
|
|
2631
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2632
|
+
const stopDir = findSearchStopDir(cwd);
|
|
2216
2633
|
const explorer = cosmiconfig(MODULE_NAME, {
|
|
2217
2634
|
searchPlaces: SEARCH_PLACES,
|
|
2218
|
-
loaders: { ".ts": TypeScriptLoader() }
|
|
2635
|
+
loaders: { ".ts": TypeScriptLoader({ alias: resolveSelfPackageAliases() }) },
|
|
2636
|
+
searchStrategy: "global",
|
|
2637
|
+
stopDir
|
|
2219
2638
|
});
|
|
2220
2639
|
if (options.configPath !== void 0) {
|
|
2221
2640
|
return loadExplicitWithMeta(explorer, options.configPath, options.cwd, fs);
|
|
2222
2641
|
}
|
|
2223
2642
|
let result;
|
|
2224
2643
|
try {
|
|
2225
|
-
result = await explorer.search(
|
|
2644
|
+
result = await explorer.search(cwd);
|
|
2226
2645
|
} catch (error) {
|
|
2227
2646
|
const detail = errorMessage(error);
|
|
2228
2647
|
throw new SdkError("CONFIG_INVALID", `Failed to load the verbatra configuration: ${detail}`);
|
|
2229
2648
|
}
|
|
2649
|
+
if (result !== null && result.isEmpty !== true) {
|
|
2650
|
+
const resultDir = dirname(resolve(result.filepath));
|
|
2651
|
+
if (!collectSearchChain(cwd, stopDir).has(resultDir)) {
|
|
2652
|
+
result = null;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2230
2655
|
if (result === null || result.isEmpty === true) {
|
|
2231
2656
|
throw new SdkError(
|
|
2232
2657
|
"CONFIG_NOT_FOUND",
|
|
@@ -2287,10 +2712,17 @@ function buildForwardMap(config, pathFor) {
|
|
|
2287
2712
|
}
|
|
2288
2713
|
return forward;
|
|
2289
2714
|
}
|
|
2715
|
+
function sharedCataloguePathFor(cwd, pattern) {
|
|
2716
|
+
const path = resolve(cwd, expandPattern(pattern, ""));
|
|
2717
|
+
return () => path;
|
|
2718
|
+
}
|
|
2290
2719
|
function createLocalePathResolver(cwd, config) {
|
|
2291
2720
|
const style = config.files.localeStyle ?? DEFAULT_LOCALE_STYLE;
|
|
2292
2721
|
const pattern = config.files.pattern;
|
|
2293
2722
|
validatePattern(pattern, style);
|
|
2723
|
+
if (isSharedCatalogueFormat(config.format)) {
|
|
2724
|
+
return { pathFor: sharedCataloguePathFor(cwd, pattern), localeFor: () => void 0 };
|
|
2725
|
+
}
|
|
2294
2726
|
const pathFor = (locale) => resolve(cwd, expandPattern(pattern, safeSpelling(locale, style, config.sourceLocale)));
|
|
2295
2727
|
const forward = buildForwardMap(config, pathFor);
|
|
2296
2728
|
return {
|
|
@@ -2461,36 +2893,1315 @@ var nodeAdapterFs = {
|
|
|
2461
2893
|
readBounded: nodeReadBounded,
|
|
2462
2894
|
writeFileAtomic: (path, data) => atomicWriteFile(path, data, nodeOps)
|
|
2463
2895
|
};
|
|
2464
|
-
var
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
case TYPE.date:
|
|
2471
|
-
case TYPE.time:
|
|
2472
|
-
case TYPE.select:
|
|
2473
|
-
case TYPE.plural:
|
|
2474
|
-
return `{${element.value}}`;
|
|
2475
|
-
case TYPE.tag:
|
|
2476
|
-
return `<${element.value}>`;
|
|
2477
|
-
default:
|
|
2478
|
-
return void 0;
|
|
2896
|
+
var AdapterError = class extends Error {
|
|
2897
|
+
code;
|
|
2898
|
+
constructor(code, message) {
|
|
2899
|
+
super(message);
|
|
2900
|
+
this.name = "AdapterError";
|
|
2901
|
+
this.code = code;
|
|
2479
2902
|
}
|
|
2903
|
+
};
|
|
2904
|
+
var MAX_DEPTH = 100;
|
|
2905
|
+
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
2906
|
+
function stripLeadingBom(content) {
|
|
2907
|
+
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
2480
2908
|
}
|
|
2481
|
-
function
|
|
2482
|
-
|
|
2483
|
-
|
|
2909
|
+
async function readBoundedFile(fs, filePath) {
|
|
2910
|
+
const outcome = await fs.readBounded(filePath, MAX_INPUT_BYTES);
|
|
2911
|
+
return outcome.kind === "ok" ? { kind: "ok", content: stripLeadingBom(outcome.content) } : outcome;
|
|
2912
|
+
}
|
|
2913
|
+
function outcomeToContent(outcome, notAFileMessage) {
|
|
2914
|
+
if (outcome.kind === "not-a-file") {
|
|
2915
|
+
throw new AdapterError("INVALID_STRUCTURE", notAFileMessage);
|
|
2484
2916
|
}
|
|
2485
|
-
if (
|
|
2486
|
-
|
|
2917
|
+
if (outcome.kind === "too-large") {
|
|
2918
|
+
throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
|
|
2487
2919
|
}
|
|
2488
|
-
return
|
|
2489
|
-
}
|
|
2490
|
-
function addCount(target, token, count) {
|
|
2491
|
-
target.set(token, (target.get(token) ?? 0) + count);
|
|
2920
|
+
return outcome.content;
|
|
2492
2921
|
}
|
|
2493
|
-
function
|
|
2922
|
+
async function readFileContent(fs, filePath) {
|
|
2923
|
+
const outcome = await readBoundedFile(fs, filePath);
|
|
2924
|
+
return outcomeToContent(outcome, "The path is not a regular file.");
|
|
2925
|
+
}
|
|
2926
|
+
var LINE_TERMINATOR_SPLIT = /\r\n|\r|\n/;
|
|
2927
|
+
var TRAILING_LINE_TERMINATOR = /(?:\r\n|\r|\n)$/;
|
|
2928
|
+
function detectLineTerminator(content) {
|
|
2929
|
+
if (content.includes("\r\n")) {
|
|
2930
|
+
return "\r\n";
|
|
2931
|
+
}
|
|
2932
|
+
return content.includes("\r") ? "\r" : "\n";
|
|
2933
|
+
}
|
|
2934
|
+
function splitPhysicalLines(content) {
|
|
2935
|
+
if (content === "") {
|
|
2936
|
+
return [];
|
|
2937
|
+
}
|
|
2938
|
+
const lines = content.split(LINE_TERMINATOR_SPLIT);
|
|
2939
|
+
if (TRAILING_LINE_TERMINATOR.test(content)) {
|
|
2940
|
+
lines.pop();
|
|
2941
|
+
}
|
|
2942
|
+
return lines;
|
|
2943
|
+
}
|
|
2944
|
+
function scanTokens(value, pattern, extract = (match) => match[0]) {
|
|
2945
|
+
const result = [];
|
|
2946
|
+
for (const match of value.matchAll(pattern)) {
|
|
2947
|
+
const token = extract(match);
|
|
2948
|
+
if (token !== void 0) {
|
|
2949
|
+
result.push(token);
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
return result;
|
|
2953
|
+
}
|
|
2954
|
+
function namespaceOf(filePath) {
|
|
2955
|
+
return basename(filePath, extname(filePath));
|
|
2956
|
+
}
|
|
2957
|
+
function isEnoent(error) {
|
|
2958
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2959
|
+
}
|
|
2960
|
+
function rethrowStructured(error, message) {
|
|
2961
|
+
if (error instanceof AdapterError) {
|
|
2962
|
+
throw error;
|
|
2963
|
+
}
|
|
2964
|
+
throw new AdapterError("INVALID_STRUCTURE", message);
|
|
2965
|
+
}
|
|
2966
|
+
function computeIcu(entries, compute) {
|
|
2967
|
+
if (!compute) {
|
|
2968
|
+
return [];
|
|
2969
|
+
}
|
|
2970
|
+
try {
|
|
2971
|
+
return compute(entries);
|
|
2972
|
+
} catch (error) {
|
|
2973
|
+
rethrowStructured(error, "The file could not be analyzed for message validity.");
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
function buildCanHandle(extensions, sniff) {
|
|
2977
|
+
return (filePath, sample) => {
|
|
2978
|
+
if (!extensions.includes(extname(filePath).toLowerCase())) {
|
|
2979
|
+
return false;
|
|
2980
|
+
}
|
|
2981
|
+
return sample === void 0 || sniff === void 0 || sniff(sample);
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
async function toEntries(content, namespace, filePath, fs, parseEntries) {
|
|
2985
|
+
try {
|
|
2986
|
+
return await parseEntries(content, namespace, filePath, fs);
|
|
2987
|
+
} catch (error) {
|
|
2988
|
+
rethrowStructured(error, "The file could not be parsed.");
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
function createFlatFileAdapter(options) {
|
|
2992
|
+
const {
|
|
2993
|
+
format,
|
|
2994
|
+
extensions,
|
|
2995
|
+
sniff,
|
|
2996
|
+
parseEntries,
|
|
2997
|
+
serializeEntries,
|
|
2998
|
+
extractPlaceholders,
|
|
2999
|
+
validateMessage,
|
|
3000
|
+
computeInvalidIcuKeys,
|
|
3001
|
+
fs = nodeAdapterFs
|
|
3002
|
+
} = options;
|
|
3003
|
+
return {
|
|
3004
|
+
format,
|
|
3005
|
+
canHandle: buildCanHandle(extensions, sniff),
|
|
3006
|
+
extractPlaceholders,
|
|
3007
|
+
validateMessage: validateMessage ?? (() => true),
|
|
3008
|
+
async read(filePath, locale) {
|
|
3009
|
+
const content = await readFileContent(fs, filePath);
|
|
3010
|
+
const namespace = namespaceOf(filePath);
|
|
3011
|
+
const entries = await toEntries(content, namespace, filePath, fs, parseEntries);
|
|
3012
|
+
const resource = { locale, namespace, format, entries };
|
|
3013
|
+
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
3014
|
+
return { resource, invalidIcuKeys, excludedLeafPaths: [] };
|
|
3015
|
+
},
|
|
3016
|
+
async write(resource, filePath) {
|
|
3017
|
+
const data = await serializeEntries(resource.entries, filePath, fs);
|
|
3018
|
+
await fs.writeFileAtomic(filePath, data);
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
var UNICODE_ESCAPE = /^[0-9a-fA-F]{4}$/;
|
|
3023
|
+
function decodeUnicodeEscape(raw, at) {
|
|
3024
|
+
const hex = raw.slice(at, at + 4);
|
|
3025
|
+
if (!UNICODE_ESCAPE.test(hex)) {
|
|
3026
|
+
throw new AdapterError("INVALID_STRUCTURE", "The file has a malformed \\u unicode escape.");
|
|
3027
|
+
}
|
|
3028
|
+
return String.fromCharCode(Number.parseInt(hex, 16));
|
|
3029
|
+
}
|
|
3030
|
+
function decodeSimpleEscape(char) {
|
|
3031
|
+
switch (char) {
|
|
3032
|
+
case "n":
|
|
3033
|
+
return "\n";
|
|
3034
|
+
case "t":
|
|
3035
|
+
return " ";
|
|
3036
|
+
case "'":
|
|
3037
|
+
return "'";
|
|
3038
|
+
case '"':
|
|
3039
|
+
return '"';
|
|
3040
|
+
case "\\":
|
|
3041
|
+
return "\\";
|
|
3042
|
+
case "@":
|
|
3043
|
+
return "@";
|
|
3044
|
+
case "?":
|
|
3045
|
+
return "?";
|
|
3046
|
+
default:
|
|
3047
|
+
return char;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
function decodeAndroidEscapes(raw) {
|
|
3051
|
+
const pieces = [];
|
|
3052
|
+
let i = 0;
|
|
3053
|
+
while (i < raw.length) {
|
|
3054
|
+
const char = raw[i];
|
|
3055
|
+
if (char !== "\\") {
|
|
3056
|
+
pieces.push(char ?? "");
|
|
3057
|
+
i += 1;
|
|
3058
|
+
continue;
|
|
3059
|
+
}
|
|
3060
|
+
const next = raw[i + 1];
|
|
3061
|
+
if (next === void 0) {
|
|
3062
|
+
i += 1;
|
|
3063
|
+
continue;
|
|
3064
|
+
}
|
|
3065
|
+
if (next === "u") {
|
|
3066
|
+
pieces.push(decodeUnicodeEscape(raw, i + 2));
|
|
3067
|
+
i += 6;
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
3070
|
+
pieces.push(decodeSimpleEscape(next));
|
|
3071
|
+
i += 2;
|
|
3072
|
+
}
|
|
3073
|
+
return pieces.join("");
|
|
3074
|
+
}
|
|
3075
|
+
function escapeChar(char, isFirst) {
|
|
3076
|
+
switch (char) {
|
|
3077
|
+
case "\\":
|
|
3078
|
+
return "\\\\";
|
|
3079
|
+
case "'":
|
|
3080
|
+
return "\\'";
|
|
3081
|
+
case '"':
|
|
3082
|
+
return '\\"';
|
|
3083
|
+
case "\n":
|
|
3084
|
+
return "\\n";
|
|
3085
|
+
case " ":
|
|
3086
|
+
return "\\t";
|
|
3087
|
+
case "@":
|
|
3088
|
+
return isFirst ? "\\@" : "@";
|
|
3089
|
+
case "?":
|
|
3090
|
+
return isFirst ? "\\?" : "?";
|
|
3091
|
+
default:
|
|
3092
|
+
return char;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
function encodeAndroidEscapes(value) {
|
|
3096
|
+
const pieces = [];
|
|
3097
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
3098
|
+
const char = value[i];
|
|
3099
|
+
if (char !== void 0) {
|
|
3100
|
+
pieces.push(escapeChar(char, i === 0));
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
return pieces.join("");
|
|
3104
|
+
}
|
|
3105
|
+
var RESOURCE_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3106
|
+
function assertValidResourceName(name, tag) {
|
|
3107
|
+
if (!RESOURCE_NAME.test(name)) {
|
|
3108
|
+
throw new AdapterError(
|
|
3109
|
+
"INVALID_STRUCTURE",
|
|
3110
|
+
`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.`
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
var PRINTF_TOKEN = /%(?:(\d+)\$)?[-+0#]*(?:[1-9]\d*)?(?:\.\d+)?(?:hh|h|ll|l|q|z|j|t|L)?([A-Za-z@])|%%/g;
|
|
3115
|
+
function extractPrintfPlaceholders(value, options) {
|
|
3116
|
+
const out = [];
|
|
3117
|
+
for (const match of value.matchAll(PRINTF_TOKEN)) {
|
|
3118
|
+
if (match[0] === "%%") {
|
|
3119
|
+
out.push("%%");
|
|
3120
|
+
continue;
|
|
3121
|
+
}
|
|
3122
|
+
const conversion = match[2];
|
|
3123
|
+
if (conversion === void 0 || !options.conversions.has(conversion)) {
|
|
3124
|
+
continue;
|
|
3125
|
+
}
|
|
3126
|
+
const position = match[1];
|
|
3127
|
+
out.push(position === void 0 ? `%${conversion}` : `%${position}$${conversion}`);
|
|
3128
|
+
}
|
|
3129
|
+
return out;
|
|
3130
|
+
}
|
|
3131
|
+
var ANDROID_PRINTF_CONVERSIONS = /* @__PURE__ */ new Set([
|
|
3132
|
+
"s",
|
|
3133
|
+
"S",
|
|
3134
|
+
"d",
|
|
3135
|
+
"f",
|
|
3136
|
+
"x",
|
|
3137
|
+
"X",
|
|
3138
|
+
"o",
|
|
3139
|
+
"c",
|
|
3140
|
+
"C",
|
|
3141
|
+
"b",
|
|
3142
|
+
"B",
|
|
3143
|
+
"e",
|
|
3144
|
+
"E",
|
|
3145
|
+
"g",
|
|
3146
|
+
"G"
|
|
3147
|
+
]);
|
|
3148
|
+
function extractAndroidPlaceholders(value) {
|
|
3149
|
+
return extractPrintfPlaceholders(value, { conversions: ANDROID_PRINTF_CONVERSIONS });
|
|
3150
|
+
}
|
|
3151
|
+
var ANDROID_PLURAL_CATEGORIES = [
|
|
3152
|
+
"zero",
|
|
3153
|
+
"one",
|
|
3154
|
+
"two",
|
|
3155
|
+
"few",
|
|
3156
|
+
"many",
|
|
3157
|
+
"other"
|
|
3158
|
+
];
|
|
3159
|
+
var ANDROID_PLURAL_CATEGORY_SET = new Set(ANDROID_PLURAL_CATEGORIES);
|
|
3160
|
+
var PLURAL_KEY = /^(.*)\[(zero|one|two|few|many|other)\]$/;
|
|
3161
|
+
function isAndroidPluralCategory(value) {
|
|
3162
|
+
return ANDROID_PLURAL_CATEGORY_SET.has(value);
|
|
3163
|
+
}
|
|
3164
|
+
function makeAndroidPluralKey(baseKey, category) {
|
|
3165
|
+
return `${baseKey}[${category}]`;
|
|
3166
|
+
}
|
|
3167
|
+
function androidPluralBaseKey(key) {
|
|
3168
|
+
return PLURAL_KEY.exec(key)?.[1];
|
|
3169
|
+
}
|
|
3170
|
+
function androidPluralCategoryOf(key) {
|
|
3171
|
+
const category = PLURAL_KEY.exec(key)?.[2];
|
|
3172
|
+
return category === void 0 ? void 0 : category;
|
|
3173
|
+
}
|
|
3174
|
+
var ELEMENT_NODE = 1;
|
|
3175
|
+
var TEXT_NODE = 3;
|
|
3176
|
+
var ROOT_TAG = "resources";
|
|
3177
|
+
function onFatal(level, message) {
|
|
3178
|
+
if (level === "fatalError") {
|
|
3179
|
+
throw new Error(message);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
function assertNoDoctype(content) {
|
|
3183
|
+
if (/<!DOCTYPE/i.test(content) || /<!ENTITY/i.test(content)) {
|
|
3184
|
+
throw new AdapterError(
|
|
3185
|
+
"INVALID_XML",
|
|
3186
|
+
"strings.xml with a DTD or entity declaration is rejected."
|
|
3187
|
+
);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
function parseErrorDetail(error) {
|
|
3191
|
+
return error instanceof Error && error.message.length > 0 ? `: ${error.message}` : "";
|
|
3192
|
+
}
|
|
3193
|
+
function parseAndroidXml(content) {
|
|
3194
|
+
assertNoDoctype(content);
|
|
3195
|
+
let doc;
|
|
3196
|
+
try {
|
|
3197
|
+
doc = new DOMParser({ onError: onFatal }).parseFromString(content, "text/xml");
|
|
3198
|
+
} catch (error) {
|
|
3199
|
+
throw new AdapterError("INVALID_XML", `The file is not valid XML${parseErrorDetail(error)}.`);
|
|
3200
|
+
}
|
|
3201
|
+
const root = doc.documentElement;
|
|
3202
|
+
if (root === null || root.localName !== ROOT_TAG) {
|
|
3203
|
+
throw new AdapterError(
|
|
3204
|
+
"INVALID_STRUCTURE",
|
|
3205
|
+
`The file is not an Android resources file: the root element must be <${ROOT_TAG}>.`
|
|
3206
|
+
);
|
|
3207
|
+
}
|
|
3208
|
+
return { doc, root };
|
|
3209
|
+
}
|
|
3210
|
+
function createAndroidXmlDocument() {
|
|
3211
|
+
const doc = new DOMImplementation().createDocument(null, ROOT_TAG, null);
|
|
3212
|
+
const root = doc.documentElement;
|
|
3213
|
+
if (root === null) {
|
|
3214
|
+
throw new AdapterError("INVALID_STRUCTURE", "Could not synthesize a new resources document.");
|
|
3215
|
+
}
|
|
3216
|
+
return { doc, root };
|
|
3217
|
+
}
|
|
3218
|
+
function isElement(node) {
|
|
3219
|
+
return node.nodeType === ELEMENT_NODE;
|
|
3220
|
+
}
|
|
3221
|
+
function elementChildren(parent, tag) {
|
|
3222
|
+
const children = Array.from(parent.childNodes).filter(isElement);
|
|
3223
|
+
return tag === void 0 ? children : children.filter((el) => el.localName === tag);
|
|
3224
|
+
}
|
|
3225
|
+
function singleTextValue(element) {
|
|
3226
|
+
const children = element.childNodes;
|
|
3227
|
+
if (children.length === 0) {
|
|
3228
|
+
return "";
|
|
3229
|
+
}
|
|
3230
|
+
if (children.length === 1) {
|
|
3231
|
+
const only = children.item(0);
|
|
3232
|
+
if (only !== null && only.nodeType === TEXT_NODE) {
|
|
3233
|
+
return only.nodeValue ?? "";
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
return void 0;
|
|
3237
|
+
}
|
|
3238
|
+
function setSingleTextValue(doc, element, value) {
|
|
3239
|
+
while (element.firstChild !== null) {
|
|
3240
|
+
element.removeChild(element.firstChild);
|
|
3241
|
+
}
|
|
3242
|
+
element.appendChild(doc.createTextNode(value));
|
|
3243
|
+
}
|
|
3244
|
+
function isTranslatable(element) {
|
|
3245
|
+
return element.getAttribute("translatable") !== "false";
|
|
3246
|
+
}
|
|
3247
|
+
function setEntry(out, key, namespace, value, isPlural) {
|
|
3248
|
+
if (out.has(key)) {
|
|
3249
|
+
throw new AdapterError(
|
|
3250
|
+
"INVALID_STRUCTURE",
|
|
3251
|
+
`The resolved key "${key}" is defined more than once in the file.`
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
out.set(key, {
|
|
3255
|
+
key,
|
|
3256
|
+
namespace,
|
|
3257
|
+
value,
|
|
3258
|
+
placeholders: extractAndroidPlaceholders(value),
|
|
3259
|
+
isPlural
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
function parseStringElement(element, namespace, out) {
|
|
3263
|
+
const name = element.getAttribute("name") ?? "";
|
|
3264
|
+
assertValidResourceName(name, "string");
|
|
3265
|
+
if (!isTranslatable(element)) {
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
const raw = singleTextValue(element);
|
|
3269
|
+
if (raw === void 0) {
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
setEntry(out, name, namespace, decodeAndroidEscapes(raw), false);
|
|
3273
|
+
}
|
|
3274
|
+
function assertUniqueQuantities(items, name) {
|
|
3275
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3276
|
+
for (const item of items) {
|
|
3277
|
+
const quantity = item.getAttribute("quantity") ?? "";
|
|
3278
|
+
if (seen.has(quantity)) {
|
|
3279
|
+
throw new AdapterError(
|
|
3280
|
+
"INVALID_STRUCTURE",
|
|
3281
|
+
`The <plurals name="${name}"> element has more than one item for quantity "${quantity}".`
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
seen.add(quantity);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
function parsePluralsElement(element, namespace, out) {
|
|
3288
|
+
const name = element.getAttribute("name") ?? "";
|
|
3289
|
+
assertValidResourceName(name, "plurals");
|
|
3290
|
+
if (!isTranslatable(element)) {
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
const items = elementChildren(element, "item");
|
|
3294
|
+
assertUniqueQuantities(items, name);
|
|
3295
|
+
const values = [];
|
|
3296
|
+
for (const item of items) {
|
|
3297
|
+
const quantity = item.getAttribute("quantity") ?? "";
|
|
3298
|
+
if (!isAndroidPluralCategory(quantity)) {
|
|
3299
|
+
throw new AdapterError(
|
|
3300
|
+
"INVALID_STRUCTURE",
|
|
3301
|
+
`The <plurals name="${name}"> element has an item with quantity="${quantity}", which is not one of ${ANDROID_PLURAL_CATEGORIES.join(", ")}.`
|
|
3302
|
+
);
|
|
3303
|
+
}
|
|
3304
|
+
const raw = singleTextValue(item);
|
|
3305
|
+
if (raw === void 0) {
|
|
3306
|
+
return;
|
|
3307
|
+
}
|
|
3308
|
+
values.push([quantity, raw]);
|
|
3309
|
+
}
|
|
3310
|
+
for (const [quantity, raw] of values) {
|
|
3311
|
+
setEntry(
|
|
3312
|
+
out,
|
|
3313
|
+
makeAndroidPluralKey(name, quantity),
|
|
3314
|
+
namespace,
|
|
3315
|
+
decodeAndroidEscapes(raw),
|
|
3316
|
+
true
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
function parseAndroidXmlEntries(content, namespace) {
|
|
3321
|
+
const { root } = parseAndroidXml(content);
|
|
3322
|
+
const out = /* @__PURE__ */ new Map();
|
|
3323
|
+
for (const element of elementChildren(root)) {
|
|
3324
|
+
if (element.localName === "string") {
|
|
3325
|
+
parseStringElement(element, namespace, out);
|
|
3326
|
+
} else if (element.localName === "plurals") {
|
|
3327
|
+
parsePluralsElement(element, namespace, out);
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
return out;
|
|
3331
|
+
}
|
|
3332
|
+
var XML_PROLOG = '<?xml version="1.0" encoding="utf-8"?>\n';
|
|
3333
|
+
function groupEntries(entries) {
|
|
3334
|
+
const singular = /* @__PURE__ */ new Map();
|
|
3335
|
+
const plural = /* @__PURE__ */ new Map();
|
|
3336
|
+
for (const [key, entry] of entries) {
|
|
3337
|
+
const baseKey = androidPluralBaseKey(key);
|
|
3338
|
+
const category = androidPluralCategoryOf(key);
|
|
3339
|
+
if (baseKey !== void 0 && category !== void 0) {
|
|
3340
|
+
const group = plural.get(baseKey) ?? /* @__PURE__ */ new Map();
|
|
3341
|
+
group.set(category, entry);
|
|
3342
|
+
plural.set(baseKey, group);
|
|
3343
|
+
} else {
|
|
3344
|
+
singular.set(key, entry);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
return { singular, plural };
|
|
3348
|
+
}
|
|
3349
|
+
function isTranslatable2(element) {
|
|
3350
|
+
return element.getAttribute("translatable") !== "false";
|
|
3351
|
+
}
|
|
3352
|
+
function appendStringElement(doc, root, name, value) {
|
|
3353
|
+
const element = doc.createElement("string");
|
|
3354
|
+
element.setAttribute("name", name);
|
|
3355
|
+
element.appendChild(doc.createTextNode(encodeAndroidEscapes(value)));
|
|
3356
|
+
root.appendChild(element);
|
|
3357
|
+
}
|
|
3358
|
+
function appendPluralsElement(doc, root, name, categories) {
|
|
3359
|
+
const plurals = doc.createElement("plurals");
|
|
3360
|
+
plurals.setAttribute("name", name);
|
|
3361
|
+
for (const category of ANDROID_PLURAL_CATEGORIES) {
|
|
3362
|
+
const entry = categories.get(category);
|
|
3363
|
+
if (entry === void 0) {
|
|
3364
|
+
continue;
|
|
3365
|
+
}
|
|
3366
|
+
const item = doc.createElement("item");
|
|
3367
|
+
item.setAttribute("quantity", category);
|
|
3368
|
+
item.appendChild(doc.createTextNode(encodeAndroidEscapes(entry.value)));
|
|
3369
|
+
plurals.appendChild(item);
|
|
3370
|
+
}
|
|
3371
|
+
root.appendChild(plurals);
|
|
3372
|
+
}
|
|
3373
|
+
function appendUnmatched(doc, root, grouped, matched) {
|
|
3374
|
+
for (const [key, entry] of grouped.singular) {
|
|
3375
|
+
if (!matched.has(key)) {
|
|
3376
|
+
appendStringElement(doc, root, key, entry.value);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
for (const [baseKey, categories] of grouped.plural) {
|
|
3380
|
+
if (!matched.has(baseKey)) {
|
|
3381
|
+
appendPluralsElement(doc, root, baseKey, categories);
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
function isReadThroughString(element) {
|
|
3386
|
+
return !isTranslatable2(element) || singleTextValue(element) === void 0;
|
|
3387
|
+
}
|
|
3388
|
+
function patchStringElement(doc, element, grouped, matched) {
|
|
3389
|
+
const name = element.getAttribute("name") ?? "";
|
|
3390
|
+
assertValidResourceName(name, "string");
|
|
3391
|
+
const entry = grouped.singular.get(name);
|
|
3392
|
+
if (entry !== void 0) {
|
|
3393
|
+
matched.add(name);
|
|
3394
|
+
element.removeAttribute("translatable");
|
|
3395
|
+
setSingleTextValue(doc, element, encodeAndroidEscapes(entry.value));
|
|
3396
|
+
return;
|
|
3397
|
+
}
|
|
3398
|
+
if (isReadThroughString(element)) {
|
|
3399
|
+
return;
|
|
3400
|
+
}
|
|
3401
|
+
matched.add(name);
|
|
3402
|
+
element.parentNode?.removeChild(element);
|
|
3403
|
+
}
|
|
3404
|
+
function isReadThroughPlurals(items) {
|
|
3405
|
+
return items.some((item) => singleTextValue(item) === void 0);
|
|
3406
|
+
}
|
|
3407
|
+
function patchTranslatablePlurals(doc, element, items, categories) {
|
|
3408
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3409
|
+
for (const item of items) {
|
|
3410
|
+
const quantity = item.getAttribute("quantity") ?? "";
|
|
3411
|
+
const entry = categories.get(quantity);
|
|
3412
|
+
if (entry === void 0) {
|
|
3413
|
+
element.removeChild(item);
|
|
3414
|
+
continue;
|
|
3415
|
+
}
|
|
3416
|
+
setSingleTextValue(doc, item, encodeAndroidEscapes(entry.value));
|
|
3417
|
+
seen.add(quantity);
|
|
3418
|
+
}
|
|
3419
|
+
for (const category of ANDROID_PLURAL_CATEGORIES) {
|
|
3420
|
+
const entry = categories.get(category);
|
|
3421
|
+
if (entry === void 0 || seen.has(category)) {
|
|
3422
|
+
continue;
|
|
3423
|
+
}
|
|
3424
|
+
const item = doc.createElement("item");
|
|
3425
|
+
item.setAttribute("quantity", category);
|
|
3426
|
+
item.appendChild(doc.createTextNode(encodeAndroidEscapes(entry.value)));
|
|
3427
|
+
element.appendChild(item);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
function patchPluralsElement(doc, element, grouped, matched) {
|
|
3431
|
+
const name = element.getAttribute("name") ?? "";
|
|
3432
|
+
assertValidResourceName(name, "plurals");
|
|
3433
|
+
const items = elementChildren(element, "item");
|
|
3434
|
+
const categories = grouped.plural.get(name);
|
|
3435
|
+
if (categories !== void 0) {
|
|
3436
|
+
matched.add(name);
|
|
3437
|
+
element.removeAttribute("translatable");
|
|
3438
|
+
patchTranslatablePlurals(doc, element, items, categories);
|
|
3439
|
+
if (elementChildren(element, "item").length === 0) {
|
|
3440
|
+
element.parentNode?.removeChild(element);
|
|
3441
|
+
}
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
if (!isTranslatable2(element) || isReadThroughPlurals(items)) {
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
matched.add(name);
|
|
3448
|
+
element.parentNode?.removeChild(element);
|
|
3449
|
+
}
|
|
3450
|
+
function patchDocument(doc, root, grouped) {
|
|
3451
|
+
const matched = /* @__PURE__ */ new Set();
|
|
3452
|
+
for (const element of elementChildren(root)) {
|
|
3453
|
+
if (element.localName === "string") {
|
|
3454
|
+
patchStringElement(doc, element, grouped, matched);
|
|
3455
|
+
} else if (element.localName === "plurals") {
|
|
3456
|
+
patchPluralsElement(doc, element, grouped, matched);
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
appendUnmatched(doc, root, grouped, matched);
|
|
3460
|
+
}
|
|
3461
|
+
async function readDestination(filePath, fs) {
|
|
3462
|
+
try {
|
|
3463
|
+
const outcome = await readBoundedFile(fs, filePath);
|
|
3464
|
+
return outcomeToContent(outcome, "The destination path is not a regular file.");
|
|
3465
|
+
} catch (error) {
|
|
3466
|
+
if (isEnoent(error)) {
|
|
3467
|
+
return void 0;
|
|
3468
|
+
}
|
|
3469
|
+
throw error;
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
async function serializeAndroidXmlEntries(entries, filePath, fs) {
|
|
3473
|
+
const grouped = groupEntries(entries);
|
|
3474
|
+
const existing = await readDestination(filePath, fs);
|
|
3475
|
+
if (existing === void 0) {
|
|
3476
|
+
const { doc: doc2, root: root2 } = createAndroidXmlDocument();
|
|
3477
|
+
appendUnmatched(doc2, root2, grouped, /* @__PURE__ */ new Set());
|
|
3478
|
+
return XML_PROLOG + new XMLSerializer().serializeToString(doc2);
|
|
3479
|
+
}
|
|
3480
|
+
const { doc, root } = parseAndroidXml(existing);
|
|
3481
|
+
patchDocument(doc, root, grouped);
|
|
3482
|
+
return new XMLSerializer().serializeToString(doc);
|
|
3483
|
+
}
|
|
3484
|
+
function createAndroidXmlAdapter(fs = nodeAdapterFs) {
|
|
3485
|
+
return createFlatFileAdapter({
|
|
3486
|
+
fs,
|
|
3487
|
+
format: "android-xml",
|
|
3488
|
+
extensions: [".xml"],
|
|
3489
|
+
parseEntries: parseAndroidXmlEntries,
|
|
3490
|
+
serializeEntries: serializeAndroidXmlEntries,
|
|
3491
|
+
extractPlaceholders: extractAndroidPlaceholders
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
var APPLE_PRINTF_CONVERSIONS = /* @__PURE__ */ new Set([
|
|
3495
|
+
"@",
|
|
3496
|
+
"d",
|
|
3497
|
+
"i",
|
|
3498
|
+
"u",
|
|
3499
|
+
"x",
|
|
3500
|
+
"X",
|
|
3501
|
+
"o",
|
|
3502
|
+
"f",
|
|
3503
|
+
"e",
|
|
3504
|
+
"E",
|
|
3505
|
+
"g",
|
|
3506
|
+
"G",
|
|
3507
|
+
"c",
|
|
3508
|
+
"s"
|
|
3509
|
+
]);
|
|
3510
|
+
function extractAppleStringsPlaceholders(value) {
|
|
3511
|
+
return extractPrintfPlaceholders(value, { conversions: APPLE_PRINTF_CONVERSIONS });
|
|
3512
|
+
}
|
|
3513
|
+
var UNICODE_ESCAPE2 = /^[0-9a-fA-F]{4}$/;
|
|
3514
|
+
var TRAILING_BLOCK_COMMENT = /\/\*([\s\S]*?)\*\//g;
|
|
3515
|
+
var ATTACHED_TAIL = /^[ \t]*\r?\n?[ \t]*$/;
|
|
3516
|
+
function isInlineWhitespace(char) {
|
|
3517
|
+
return char === " " || char === " " || char === "\r" || char === "\n";
|
|
3518
|
+
}
|
|
3519
|
+
function skipInlineWhitespace(content, from) {
|
|
3520
|
+
let i = from;
|
|
3521
|
+
while (isInlineWhitespace(content[i])) {
|
|
3522
|
+
i += 1;
|
|
3523
|
+
}
|
|
3524
|
+
return i;
|
|
3525
|
+
}
|
|
3526
|
+
function lineOf(content, position) {
|
|
3527
|
+
return content.slice(0, position).split("\n").length;
|
|
3528
|
+
}
|
|
3529
|
+
function hasUtf16Bom(content) {
|
|
3530
|
+
return content.charCodeAt(0) === 65533 && content.charCodeAt(1) === 65533;
|
|
3531
|
+
}
|
|
3532
|
+
function assertNotUtf16(content) {
|
|
3533
|
+
if (hasUtf16Bom(content)) {
|
|
3534
|
+
throw new AdapterError(
|
|
3535
|
+
"INVALID_STRUCTURE",
|
|
3536
|
+
"The file appears to be UTF-16 encoded. Apple .strings files must be UTF-8."
|
|
3537
|
+
);
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
function decodeUnicodeEscape2(content, at) {
|
|
3541
|
+
const hex = content.slice(at, at + 4);
|
|
3542
|
+
if (!UNICODE_ESCAPE2.test(hex)) {
|
|
3543
|
+
throw new AdapterError(
|
|
3544
|
+
"INVALID_STRUCTURE",
|
|
3545
|
+
`The file has a malformed \\U unicode escape (line ${lineOf(content, at)}).`
|
|
3546
|
+
);
|
|
3547
|
+
}
|
|
3548
|
+
return { char: String.fromCharCode(Number.parseInt(hex, 16)), next: at + 4 };
|
|
3549
|
+
}
|
|
3550
|
+
function decodeEscape(content, at) {
|
|
3551
|
+
const escaped = content[at];
|
|
3552
|
+
switch (escaped) {
|
|
3553
|
+
case '"':
|
|
3554
|
+
return { char: '"', next: at + 1 };
|
|
3555
|
+
case "\\":
|
|
3556
|
+
return { char: "\\", next: at + 1 };
|
|
3557
|
+
case "n":
|
|
3558
|
+
return { char: "\n", next: at + 1 };
|
|
3559
|
+
case "t":
|
|
3560
|
+
return { char: " ", next: at + 1 };
|
|
3561
|
+
case "U":
|
|
3562
|
+
return decodeUnicodeEscape2(content, at + 1);
|
|
3563
|
+
default:
|
|
3564
|
+
throw new AdapterError(
|
|
3565
|
+
"INVALID_STRUCTURE",
|
|
3566
|
+
`The file has an unknown escape sequence "\\${escaped ?? ""}" (line ${lineOf(content, at)}).`
|
|
3567
|
+
);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
function parseQuotedString(content, start) {
|
|
3571
|
+
let i = start + 1;
|
|
3572
|
+
let decoded = "";
|
|
3573
|
+
while (true) {
|
|
3574
|
+
const char = content[i];
|
|
3575
|
+
if (char === void 0) {
|
|
3576
|
+
throw new AdapterError(
|
|
3577
|
+
"INVALID_STRUCTURE",
|
|
3578
|
+
`The file has an unterminated quoted string (line ${lineOf(content, start)}).`
|
|
3579
|
+
);
|
|
3580
|
+
}
|
|
3581
|
+
if (char === '"') {
|
|
3582
|
+
i += 1;
|
|
3583
|
+
break;
|
|
3584
|
+
}
|
|
3585
|
+
if (char === "\\") {
|
|
3586
|
+
const token = decodeEscape(content, i + 1);
|
|
3587
|
+
decoded += token.char;
|
|
3588
|
+
i = token.next;
|
|
3589
|
+
continue;
|
|
3590
|
+
}
|
|
3591
|
+
decoded += char;
|
|
3592
|
+
i += 1;
|
|
3593
|
+
}
|
|
3594
|
+
return { raw: content.slice(start, i), decoded, end: i };
|
|
3595
|
+
}
|
|
3596
|
+
function parseStatement(content, start) {
|
|
3597
|
+
const keyToken = parseQuotedString(content, start);
|
|
3598
|
+
const afterKey = skipInlineWhitespace(content, keyToken.end);
|
|
3599
|
+
if (content[afterKey] !== "=") {
|
|
3600
|
+
throw new AdapterError(
|
|
3601
|
+
"INVALID_STRUCTURE",
|
|
3602
|
+
`The file has a statement missing its = separator (line ${lineOf(content, afterKey)}).`
|
|
3603
|
+
);
|
|
3604
|
+
}
|
|
3605
|
+
const valueStart = skipInlineWhitespace(content, afterKey + 1);
|
|
3606
|
+
if (content[valueStart] !== '"') {
|
|
3607
|
+
throw new AdapterError(
|
|
3608
|
+
"INVALID_STRUCTURE",
|
|
3609
|
+
`The file has a statement with no quoted value (line ${lineOf(content, valueStart)}).`
|
|
3610
|
+
);
|
|
3611
|
+
}
|
|
3612
|
+
const valueToken = parseQuotedString(content, valueStart);
|
|
3613
|
+
const semicolonAt = skipInlineWhitespace(content, valueToken.end);
|
|
3614
|
+
if (content[semicolonAt] !== ";") {
|
|
3615
|
+
throw new AdapterError(
|
|
3616
|
+
"INVALID_STRUCTURE",
|
|
3617
|
+
`The file has a statement missing its ; terminator (line ${lineOf(content, semicolonAt)}).`
|
|
3618
|
+
);
|
|
3619
|
+
}
|
|
3620
|
+
return {
|
|
3621
|
+
key: keyToken.decoded,
|
|
3622
|
+
keyRaw: keyToken.raw,
|
|
3623
|
+
value: valueToken.decoded,
|
|
3624
|
+
betweenKeyAndValue: content.slice(keyToken.end, valueStart),
|
|
3625
|
+
afterValueBeforeSemicolon: content.slice(valueToken.end, semicolonAt),
|
|
3626
|
+
end: semicolonAt + 1
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
function skipComment(content, at) {
|
|
3630
|
+
if (content[at + 1] === "*") {
|
|
3631
|
+
const closeAt = content.indexOf("*/", at + 2);
|
|
3632
|
+
return closeAt === -1 ? content.length : closeAt + 2;
|
|
3633
|
+
}
|
|
3634
|
+
let i = at + 2;
|
|
3635
|
+
while (i < content.length && content[i] !== "\n") {
|
|
3636
|
+
i += 1;
|
|
3637
|
+
}
|
|
3638
|
+
return i;
|
|
3639
|
+
}
|
|
3640
|
+
function isCommentStart(content, at) {
|
|
3641
|
+
return content[at] === "/" && (content[at + 1] === "*" || content[at + 1] === "/");
|
|
3642
|
+
}
|
|
3643
|
+
function splitLeading(leading) {
|
|
3644
|
+
let lastMatch = null;
|
|
3645
|
+
for (const match of leading.matchAll(TRAILING_BLOCK_COMMENT)) {
|
|
3646
|
+
lastMatch = match;
|
|
3647
|
+
}
|
|
3648
|
+
if (lastMatch === null) {
|
|
3649
|
+
return { alwaysPreserved: "", ownedByEntry: leading };
|
|
3650
|
+
}
|
|
3651
|
+
const commentEnd = lastMatch.index + lastMatch[0].length;
|
|
3652
|
+
const tail = leading.slice(commentEnd);
|
|
3653
|
+
if (!ATTACHED_TAIL.test(tail)) {
|
|
3654
|
+
return { alwaysPreserved: leading.slice(0, commentEnd), ownedByEntry: tail };
|
|
3655
|
+
}
|
|
3656
|
+
const inner = (lastMatch[1] ?? "").trim();
|
|
3657
|
+
return {
|
|
3658
|
+
alwaysPreserved: "",
|
|
3659
|
+
ownedByEntry: leading,
|
|
3660
|
+
...inner !== "" ? { description: inner } : {}
|
|
3661
|
+
};
|
|
3662
|
+
}
|
|
3663
|
+
function scanAppleStrings(content) {
|
|
3664
|
+
const nodes = [];
|
|
3665
|
+
let rawStart = 0;
|
|
3666
|
+
let i = 0;
|
|
3667
|
+
while (i < content.length) {
|
|
3668
|
+
const char = content[i];
|
|
3669
|
+
if (char === '"') {
|
|
3670
|
+
const split = splitLeading(content.slice(rawStart, i));
|
|
3671
|
+
const statement = parseStatement(content, i);
|
|
3672
|
+
nodes.push({
|
|
3673
|
+
kind: "entry",
|
|
3674
|
+
alwaysPreserved: split.alwaysPreserved,
|
|
3675
|
+
ownedByEntry: split.ownedByEntry,
|
|
3676
|
+
...split.description !== void 0 ? { description: split.description } : {},
|
|
3677
|
+
key: statement.key,
|
|
3678
|
+
keyRaw: statement.keyRaw,
|
|
3679
|
+
value: statement.value,
|
|
3680
|
+
betweenKeyAndValue: statement.betweenKeyAndValue,
|
|
3681
|
+
afterValueBeforeSemicolon: statement.afterValueBeforeSemicolon
|
|
3682
|
+
});
|
|
3683
|
+
i = statement.end;
|
|
3684
|
+
rawStart = i;
|
|
3685
|
+
continue;
|
|
3686
|
+
}
|
|
3687
|
+
if (isCommentStart(content, i)) {
|
|
3688
|
+
i = skipComment(content, i);
|
|
3689
|
+
continue;
|
|
3690
|
+
}
|
|
3691
|
+
i += 1;
|
|
3692
|
+
}
|
|
3693
|
+
if (rawStart < content.length) {
|
|
3694
|
+
nodes.push({ kind: "text", text: content.slice(rawStart) });
|
|
3695
|
+
}
|
|
3696
|
+
return nodes;
|
|
3697
|
+
}
|
|
3698
|
+
function parseAppleStringsEntries(content, namespace) {
|
|
3699
|
+
assertNotUtf16(content);
|
|
3700
|
+
const map = /* @__PURE__ */ new Map();
|
|
3701
|
+
for (const node of scanAppleStrings(content)) {
|
|
3702
|
+
if (node.kind === "text") {
|
|
3703
|
+
continue;
|
|
3704
|
+
}
|
|
3705
|
+
map.set(node.key, {
|
|
3706
|
+
key: node.key,
|
|
3707
|
+
namespace,
|
|
3708
|
+
value: node.value,
|
|
3709
|
+
placeholders: extractAppleStringsPlaceholders(node.value),
|
|
3710
|
+
isPlural: false,
|
|
3711
|
+
...node.description !== void 0 ? { description: node.description } : {}
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
return map;
|
|
3715
|
+
}
|
|
3716
|
+
function escapeChar2(char) {
|
|
3717
|
+
switch (char) {
|
|
3718
|
+
case "\\":
|
|
3719
|
+
return "\\\\";
|
|
3720
|
+
case '"':
|
|
3721
|
+
return '\\"';
|
|
3722
|
+
case "\n":
|
|
3723
|
+
return "\\n";
|
|
3724
|
+
case " ":
|
|
3725
|
+
return "\\t";
|
|
3726
|
+
default:
|
|
3727
|
+
return char;
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
function escapeValue(value) {
|
|
3731
|
+
let out = "";
|
|
3732
|
+
for (const char of value) {
|
|
3733
|
+
out += escapeChar2(char);
|
|
3734
|
+
}
|
|
3735
|
+
return out;
|
|
3736
|
+
}
|
|
3737
|
+
async function readDestinationNodes(filePath, fs) {
|
|
3738
|
+
let outcome;
|
|
3739
|
+
try {
|
|
3740
|
+
outcome = await readBoundedFile(fs, filePath);
|
|
3741
|
+
} catch (error) {
|
|
3742
|
+
if (isEnoent(error)) {
|
|
3743
|
+
return { nodes: [], terminator: "\n" };
|
|
3744
|
+
}
|
|
3745
|
+
throw new AdapterError("INVALID_STRUCTURE", "The destination file could not be read.");
|
|
3746
|
+
}
|
|
3747
|
+
const content = outcomeToContent(outcome, "The destination path is not a regular file.");
|
|
3748
|
+
assertNotUtf16(content);
|
|
3749
|
+
return { nodes: scanAppleStrings(content), terminator: detectLineTerminator(content) };
|
|
3750
|
+
}
|
|
3751
|
+
function endsWithNewline(text) {
|
|
3752
|
+
return text === "" || text.endsWith("\n") || text.endsWith("\r");
|
|
3753
|
+
}
|
|
3754
|
+
async function serializeAppleStringsEntries(entries, filePath, fs) {
|
|
3755
|
+
const { nodes, terminator } = await readDestinationNodes(filePath, fs);
|
|
3756
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
3757
|
+
const parts = [];
|
|
3758
|
+
for (const node of nodes) {
|
|
3759
|
+
if (node.kind === "text") {
|
|
3760
|
+
parts.push(node.text);
|
|
3761
|
+
continue;
|
|
3762
|
+
}
|
|
3763
|
+
parts.push(node.alwaysPreserved);
|
|
3764
|
+
const entry = entries.get(node.key);
|
|
3765
|
+
if (entry === void 0 || emitted.has(node.key)) {
|
|
3766
|
+
continue;
|
|
3767
|
+
}
|
|
3768
|
+
parts.push(
|
|
3769
|
+
`${node.ownedByEntry}${node.keyRaw}${node.betweenKeyAndValue}"${escapeValue(entry.value)}"${node.afterValueBeforeSemicolon};`
|
|
3770
|
+
);
|
|
3771
|
+
emitted.add(node.key);
|
|
3772
|
+
}
|
|
3773
|
+
let result = parts.join("");
|
|
3774
|
+
const appended = [];
|
|
3775
|
+
for (const [key, entry] of entries) {
|
|
3776
|
+
if (!emitted.has(key)) {
|
|
3777
|
+
appended.push(`"${escapeValue(key)}" = "${escapeValue(entry.value)}";${terminator}`);
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
if (appended.length > 0) {
|
|
3781
|
+
if (!endsWithNewline(result)) {
|
|
3782
|
+
result += terminator;
|
|
3783
|
+
}
|
|
3784
|
+
result += appended.join("");
|
|
3785
|
+
}
|
|
3786
|
+
return result;
|
|
3787
|
+
}
|
|
3788
|
+
var PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/;
|
|
3789
|
+
function isPluralKey(key) {
|
|
3790
|
+
return PLURAL_SUFFIX.test(key);
|
|
3791
|
+
}
|
|
3792
|
+
function pluralCategoryOf(key) {
|
|
3793
|
+
const match = PLURAL_SUFFIX.exec(key);
|
|
3794
|
+
return match?.[1];
|
|
3795
|
+
}
|
|
3796
|
+
function pluralBaseKey(key) {
|
|
3797
|
+
if (!isPluralKey(key)) {
|
|
3798
|
+
return void 0;
|
|
3799
|
+
}
|
|
3800
|
+
return key.replace(PLURAL_SUFFIX, "");
|
|
3801
|
+
}
|
|
3802
|
+
function makePluralKey(baseKey, category) {
|
|
3803
|
+
return `${baseKey}_${category}`;
|
|
3804
|
+
}
|
|
3805
|
+
var ELEMENT_NODE2 = 1;
|
|
3806
|
+
var PLURAL_CATEGORIES = [
|
|
3807
|
+
"zero",
|
|
3808
|
+
"one",
|
|
3809
|
+
"two",
|
|
3810
|
+
"few",
|
|
3811
|
+
"many",
|
|
3812
|
+
"other"
|
|
3813
|
+
];
|
|
3814
|
+
var PLURAL_CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
3815
|
+
var FORMAT_KEY = "NSStringLocalizedFormatKey";
|
|
3816
|
+
var SPEC_TYPE_KEY = "NSStringFormatSpecTypeKey";
|
|
3817
|
+
var VALUE_TYPE_KEY = "NSStringFormatValueTypeKey";
|
|
3818
|
+
var PLURAL_RULE_TYPE = "NSStringPluralRuleType";
|
|
3819
|
+
var VARIABLE_REF = /%#@(\w+)@/;
|
|
3820
|
+
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';
|
|
3821
|
+
function isElement2(node) {
|
|
3822
|
+
return node.nodeType === ELEMENT_NODE2;
|
|
3823
|
+
}
|
|
3824
|
+
function elementChildren2(parent) {
|
|
3825
|
+
return Array.from(parent.childNodes).filter(isElement2);
|
|
3826
|
+
}
|
|
3827
|
+
function doctypeHasInternalSubset(content) {
|
|
3828
|
+
const start = content.search(/<!DOCTYPE/i);
|
|
3829
|
+
if (start === -1) {
|
|
3830
|
+
return false;
|
|
3831
|
+
}
|
|
3832
|
+
for (let i = start; i < content.length; i += 1) {
|
|
3833
|
+
const char = content[i];
|
|
3834
|
+
if (char === "[") {
|
|
3835
|
+
return true;
|
|
3836
|
+
}
|
|
3837
|
+
if (char === ">") {
|
|
3838
|
+
return false;
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
return false;
|
|
3842
|
+
}
|
|
3843
|
+
function assertSafePlist(content, filePath) {
|
|
3844
|
+
if (/<!ENTITY/i.test(content)) {
|
|
3845
|
+
throw new AdapterError(
|
|
3846
|
+
"INVALID_XML",
|
|
3847
|
+
`${filePath}: the plist contains an <!ENTITY> declaration, which is rejected before parsing.`
|
|
3848
|
+
);
|
|
3849
|
+
}
|
|
3850
|
+
if (doctypeHasInternalSubset(content)) {
|
|
3851
|
+
throw new AdapterError(
|
|
3852
|
+
"INVALID_XML",
|
|
3853
|
+
`${filePath}: the plist's <!DOCTYPE> declares an internal subset, which is rejected before parsing.`
|
|
3854
|
+
);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
function onFatal2(level) {
|
|
3858
|
+
if (level === "fatalError") {
|
|
3859
|
+
throw new Error("malformed XML");
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
function parsePlistRoot(content, filePath) {
|
|
3863
|
+
assertSafePlist(content, filePath);
|
|
3864
|
+
let doc;
|
|
3865
|
+
try {
|
|
3866
|
+
doc = new DOMParser({ onError: onFatal2 }).parseFromString(content, "text/xml");
|
|
3867
|
+
} catch {
|
|
3868
|
+
throw new AdapterError("INVALID_XML", `${filePath}: the file is not valid XML.`);
|
|
3869
|
+
}
|
|
3870
|
+
const plistEl = doc.documentElement;
|
|
3871
|
+
if (plistEl === null || plistEl.localName !== "plist") {
|
|
3872
|
+
throw new AdapterError("INVALID_STRUCTURE", `${filePath}: the file is not a plist document.`);
|
|
3873
|
+
}
|
|
3874
|
+
const root = elementChildren2(plistEl).find((el) => el.localName === "dict");
|
|
3875
|
+
if (root === void 0) {
|
|
3876
|
+
throw new AdapterError("INVALID_STRUCTURE", `${filePath}: the plist has no top-level <dict>.`);
|
|
3877
|
+
}
|
|
3878
|
+
return { doc, root };
|
|
3879
|
+
}
|
|
3880
|
+
function dictPairs(dict, filePath, where) {
|
|
3881
|
+
const children = elementChildren2(dict);
|
|
3882
|
+
const pairs = [];
|
|
3883
|
+
for (let i = 0; i < children.length; i += 2) {
|
|
3884
|
+
const keyEl = children[i];
|
|
3885
|
+
const valueEl = children[i + 1];
|
|
3886
|
+
if (keyEl === void 0 || keyEl.localName !== "key" || valueEl === void 0) {
|
|
3887
|
+
throw new AdapterError(
|
|
3888
|
+
"INVALID_STRUCTURE",
|
|
3889
|
+
`${filePath}: ${where} has a <dict> with an odd or malformed key/value structure.`
|
|
3890
|
+
);
|
|
3891
|
+
}
|
|
3892
|
+
pairs.push({ keyText: keyEl.textContent ?? "", valueEl });
|
|
3893
|
+
}
|
|
3894
|
+
return pairs;
|
|
3895
|
+
}
|
|
3896
|
+
function stringValue(el, filePath, where) {
|
|
3897
|
+
if (el.localName !== "string") {
|
|
3898
|
+
throw new AdapterError("INVALID_STRUCTURE", `${filePath}: ${where} expected a <string> value.`);
|
|
3899
|
+
}
|
|
3900
|
+
return el.textContent ?? "";
|
|
3901
|
+
}
|
|
3902
|
+
function parseRuleDict(rulePairs, filePath, pluralKey, variableName) {
|
|
3903
|
+
const specPair = rulePairs.find((pair) => pair.keyText === SPEC_TYPE_KEY);
|
|
3904
|
+
const specType = specPair === void 0 ? void 0 : stringValue(specPair.valueEl, filePath, `${pluralKey}'s ${SPEC_TYPE_KEY}`);
|
|
3905
|
+
if (specType !== PLURAL_RULE_TYPE) {
|
|
3906
|
+
throw new AdapterError(
|
|
3907
|
+
"INVALID_STRUCTURE",
|
|
3908
|
+
`${filePath}: the stringsdict entry "${pluralKey}" has an unsupported ${SPEC_TYPE_KEY} (expected "${PLURAL_RULE_TYPE}", found ${specType === void 0 ? "none" : `"${specType}"`}).`
|
|
3909
|
+
);
|
|
3910
|
+
}
|
|
3911
|
+
const valueTypePair = rulePairs.find((pair) => pair.keyText === VALUE_TYPE_KEY);
|
|
3912
|
+
const valueType = valueTypePair === void 0 ? void 0 : stringValue(valueTypePair.valueEl, filePath, `${pluralKey}'s ${VALUE_TYPE_KEY}`);
|
|
3913
|
+
const categories = /* @__PURE__ */ new Map();
|
|
3914
|
+
for (const pair of rulePairs) {
|
|
3915
|
+
if (pair.keyText === SPEC_TYPE_KEY || pair.keyText === VALUE_TYPE_KEY) {
|
|
3916
|
+
continue;
|
|
3917
|
+
}
|
|
3918
|
+
if (!PLURAL_CATEGORY_SET.has(pair.keyText)) {
|
|
3919
|
+
throw new AdapterError(
|
|
3920
|
+
"INVALID_STRUCTURE",
|
|
3921
|
+
`${filePath}: the stringsdict entry "${pluralKey}" has an unsupported plural category "${pair.keyText}" (variable "${variableName}"); expected one of zero, one, two, few, many, other.`
|
|
3922
|
+
);
|
|
3923
|
+
}
|
|
3924
|
+
categories.set(
|
|
3925
|
+
pair.keyText,
|
|
3926
|
+
stringValue(pair.valueEl, filePath, `${pluralKey}'s "${pair.keyText}" category`)
|
|
3927
|
+
);
|
|
3928
|
+
}
|
|
3929
|
+
if (categories.size === 0) {
|
|
3930
|
+
throw new AdapterError(
|
|
3931
|
+
"INVALID_STRUCTURE",
|
|
3932
|
+
`${filePath}: the stringsdict entry "${pluralKey}" has no plural categories.`
|
|
3933
|
+
);
|
|
3934
|
+
}
|
|
3935
|
+
return { valueType, categories };
|
|
3936
|
+
}
|
|
3937
|
+
function parseEntryDict(entryDict, filePath, pluralKey) {
|
|
3938
|
+
const pairs = dictPairs(entryDict, filePath, `the stringsdict entry "${pluralKey}"`);
|
|
3939
|
+
const formatPair = pairs.find((pair) => pair.keyText === FORMAT_KEY);
|
|
3940
|
+
if (formatPair === void 0) {
|
|
3941
|
+
throw new AdapterError(
|
|
3942
|
+
"INVALID_STRUCTURE",
|
|
3943
|
+
`${filePath}: the stringsdict entry "${pluralKey}" is missing ${FORMAT_KEY}.`
|
|
3944
|
+
);
|
|
3945
|
+
}
|
|
3946
|
+
const formatKey = stringValue(
|
|
3947
|
+
formatPair.valueEl,
|
|
3948
|
+
filePath,
|
|
3949
|
+
`the stringsdict entry "${pluralKey}"'s ${FORMAT_KEY}`
|
|
3950
|
+
);
|
|
3951
|
+
const variableMatch = VARIABLE_REF.exec(formatKey);
|
|
3952
|
+
if (variableMatch === null) {
|
|
3953
|
+
throw new AdapterError(
|
|
3954
|
+
"INVALID_STRUCTURE",
|
|
3955
|
+
`${filePath}: the stringsdict entry "${pluralKey}" is missing a plural rule: its ${FORMAT_KEY} does not reference a %#@variable@ substitution.`
|
|
3956
|
+
);
|
|
3957
|
+
}
|
|
3958
|
+
const variableName = variableMatch[1];
|
|
3959
|
+
const rulePair = pairs.find((pair) => pair.keyText === variableName);
|
|
3960
|
+
if (rulePair === void 0) {
|
|
3961
|
+
throw new AdapterError(
|
|
3962
|
+
"INVALID_STRUCTURE",
|
|
3963
|
+
`${filePath}: the stringsdict entry "${pluralKey}" references variable "${variableName}" in its ${FORMAT_KEY} but has no matching rule dictionary.`
|
|
3964
|
+
);
|
|
3965
|
+
}
|
|
3966
|
+
if (rulePair.valueEl.localName !== "dict") {
|
|
3967
|
+
throw new AdapterError(
|
|
3968
|
+
"INVALID_STRUCTURE",
|
|
3969
|
+
`${filePath}: the stringsdict entry "${pluralKey}"'s "${variableName}" rule must be a <dict>.`
|
|
3970
|
+
);
|
|
3971
|
+
}
|
|
3972
|
+
const rulePairs = dictPairs(
|
|
3973
|
+
rulePair.valueEl,
|
|
3974
|
+
filePath,
|
|
3975
|
+
`the stringsdict entry "${pluralKey}"'s "${variableName}" rule`
|
|
3976
|
+
);
|
|
3977
|
+
const { valueType, categories } = parseRuleDict(rulePairs, filePath, pluralKey, variableName);
|
|
3978
|
+
return { formatKey, variableName, valueType, categories };
|
|
3979
|
+
}
|
|
3980
|
+
function parseStringsDictGroups(content, filePath) {
|
|
3981
|
+
const { root } = parsePlistRoot(content, filePath);
|
|
3982
|
+
const pairs = dictPairs(root, filePath, "the stringsdict document");
|
|
3983
|
+
const out = /* @__PURE__ */ new Map();
|
|
3984
|
+
for (const pair of pairs) {
|
|
3985
|
+
const pluralKey = pair.keyText;
|
|
3986
|
+
if (pair.valueEl.localName !== "dict") {
|
|
3987
|
+
throw new AdapterError(
|
|
3988
|
+
"INVALID_STRUCTURE",
|
|
3989
|
+
`${filePath}: the stringsdict entry "${pluralKey}" must be a <dict>.`
|
|
3990
|
+
);
|
|
3991
|
+
}
|
|
3992
|
+
if (out.has(pluralKey)) {
|
|
3993
|
+
throw new AdapterError(
|
|
3994
|
+
"INVALID_STRUCTURE",
|
|
3995
|
+
`${filePath}: the stringsdict file has two entries with the key "${pluralKey}".`
|
|
3996
|
+
);
|
|
3997
|
+
}
|
|
3998
|
+
out.set(pluralKey, parseEntryDict(pair.valueEl, filePath, pluralKey));
|
|
3999
|
+
}
|
|
4000
|
+
return out;
|
|
4001
|
+
}
|
|
4002
|
+
function parseAppleStringsDictEntries(content, namespace, filePath) {
|
|
4003
|
+
const groups = parseStringsDictGroups(content, filePath);
|
|
4004
|
+
const out = /* @__PURE__ */ new Map();
|
|
4005
|
+
for (const [pluralKey, group] of groups) {
|
|
4006
|
+
for (const category of PLURAL_CATEGORIES) {
|
|
4007
|
+
const text = group.categories.get(category);
|
|
4008
|
+
if (text === void 0) {
|
|
4009
|
+
continue;
|
|
4010
|
+
}
|
|
4011
|
+
const key = makePluralKey(pluralKey, category);
|
|
4012
|
+
out.set(key, {
|
|
4013
|
+
key,
|
|
4014
|
+
namespace,
|
|
4015
|
+
value: text,
|
|
4016
|
+
placeholders: extractAppleStringsPlaceholders(text),
|
|
4017
|
+
isPlural: true
|
|
4018
|
+
});
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
return out;
|
|
4022
|
+
}
|
|
4023
|
+
async function readExistingGroups(filePath, fs, strict) {
|
|
4024
|
+
try {
|
|
4025
|
+
const outcome = await readBoundedFile(fs, filePath);
|
|
4026
|
+
const content = outcomeToContent(
|
|
4027
|
+
outcome,
|
|
4028
|
+
`${filePath}: the destination path is not a regular file.`
|
|
4029
|
+
);
|
|
4030
|
+
return parseStringsDictGroups(content, filePath);
|
|
4031
|
+
} catch (error) {
|
|
4032
|
+
if (isEnoent(error) || !strict) {
|
|
4033
|
+
return /* @__PURE__ */ new Map();
|
|
4034
|
+
}
|
|
4035
|
+
if (error instanceof AdapterError) {
|
|
4036
|
+
throw error;
|
|
4037
|
+
}
|
|
4038
|
+
throw new AdapterError(
|
|
4039
|
+
"INVALID_STRUCTURE",
|
|
4040
|
+
`${filePath}: the destination stringsdict file could not be read.`
|
|
4041
|
+
);
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
function groupTargets(entries) {
|
|
4045
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4046
|
+
for (const [key, entry] of entries) {
|
|
4047
|
+
const baseKey = pluralBaseKey(key);
|
|
4048
|
+
const category = pluralCategoryOf(key);
|
|
4049
|
+
if (baseKey === void 0 || category === void 0) {
|
|
4050
|
+
continue;
|
|
4051
|
+
}
|
|
4052
|
+
const group = groups.get(baseKey) ?? /* @__PURE__ */ new Map();
|
|
4053
|
+
group.set(category, entry.value);
|
|
4054
|
+
groups.set(baseKey, group);
|
|
4055
|
+
}
|
|
4056
|
+
return groups;
|
|
4057
|
+
}
|
|
4058
|
+
var DEFAULT_VALUE_TYPE = "d";
|
|
4059
|
+
function appendTextEl(doc, parent, tag, text) {
|
|
4060
|
+
const el = doc.createElement(tag);
|
|
4061
|
+
el.appendChild(doc.createTextNode(text));
|
|
4062
|
+
parent.appendChild(el);
|
|
4063
|
+
}
|
|
4064
|
+
function buildEntryPair(doc, baseKey, entry, categories) {
|
|
4065
|
+
const keyEl = doc.createElement("key");
|
|
4066
|
+
keyEl.appendChild(doc.createTextNode(baseKey));
|
|
4067
|
+
const entryDict = doc.createElement("dict");
|
|
4068
|
+
appendTextEl(doc, entryDict, "key", FORMAT_KEY);
|
|
4069
|
+
appendTextEl(doc, entryDict, "string", entry.formatKey);
|
|
4070
|
+
appendTextEl(doc, entryDict, "key", entry.variableName);
|
|
4071
|
+
const ruleDict = doc.createElement("dict");
|
|
4072
|
+
appendTextEl(doc, ruleDict, "key", SPEC_TYPE_KEY);
|
|
4073
|
+
appendTextEl(doc, ruleDict, "string", PLURAL_RULE_TYPE);
|
|
4074
|
+
appendTextEl(doc, ruleDict, "key", VALUE_TYPE_KEY);
|
|
4075
|
+
appendTextEl(doc, ruleDict, "string", entry.valueType ?? DEFAULT_VALUE_TYPE);
|
|
4076
|
+
for (const category of PLURAL_CATEGORIES) {
|
|
4077
|
+
const text = categories.get(category);
|
|
4078
|
+
if (text === void 0) {
|
|
4079
|
+
continue;
|
|
4080
|
+
}
|
|
4081
|
+
appendTextEl(doc, ruleDict, "key", category);
|
|
4082
|
+
appendTextEl(doc, ruleDict, "string", text);
|
|
4083
|
+
}
|
|
4084
|
+
entryDict.appendChild(ruleDict);
|
|
4085
|
+
return { keyEl, dictEl: entryDict };
|
|
4086
|
+
}
|
|
4087
|
+
function synthesizeEntry(baseKey) {
|
|
4088
|
+
return {
|
|
4089
|
+
formatKey: `%#@${baseKey}@`,
|
|
4090
|
+
variableName: baseKey,
|
|
4091
|
+
valueType: void 0,
|
|
4092
|
+
categories: /* @__PURE__ */ new Map()
|
|
4093
|
+
};
|
|
4094
|
+
}
|
|
4095
|
+
async function serializeAppleStringsDictEntries(entries, filePath, fs) {
|
|
4096
|
+
const targets = groupTargets(entries);
|
|
4097
|
+
const existing = await readExistingGroups(filePath, fs, targets.size > 0);
|
|
4098
|
+
if (targets.size === 0 && existing.size === 0) {
|
|
4099
|
+
return void 0;
|
|
4100
|
+
}
|
|
4101
|
+
const doc = new DOMParser().parseFromString('<plist version="1.0"><dict/></plist>', "text/xml");
|
|
4102
|
+
const plistEl = doc.documentElement;
|
|
4103
|
+
const dictEl = elementChildren2(plistEl)[0];
|
|
4104
|
+
const orderedKeys = [
|
|
4105
|
+
...[...existing.keys()].filter((key) => targets.has(key)),
|
|
4106
|
+
...[...targets.keys()].filter((key) => !existing.has(key))
|
|
4107
|
+
];
|
|
4108
|
+
for (const baseKey of orderedKeys) {
|
|
4109
|
+
const categories = targets.get(baseKey);
|
|
4110
|
+
if (categories === void 0) {
|
|
4111
|
+
continue;
|
|
4112
|
+
}
|
|
4113
|
+
const base = existing.get(baseKey) ?? synthesizeEntry(baseKey);
|
|
4114
|
+
const { keyEl, dictEl: entryDictEl } = buildEntryPair(doc, baseKey, base, categories);
|
|
4115
|
+
dictEl.appendChild(keyEl);
|
|
4116
|
+
dictEl.appendChild(entryDictEl);
|
|
4117
|
+
}
|
|
4118
|
+
return `${PLIST_HEADER}${new XMLSerializer().serializeToString(doc)}`;
|
|
4119
|
+
}
|
|
4120
|
+
function stringsDictSiblingPath(filePath) {
|
|
4121
|
+
return join(dirname(filePath), `${basename(filePath, extname(filePath))}.stringsdict`);
|
|
4122
|
+
}
|
|
4123
|
+
function partitionEntries(entries) {
|
|
4124
|
+
const singular = /* @__PURE__ */ new Map();
|
|
4125
|
+
const plural = /* @__PURE__ */ new Map();
|
|
4126
|
+
for (const [key, entry] of entries) {
|
|
4127
|
+
(entry.isPlural ? plural : singular).set(key, entry);
|
|
4128
|
+
}
|
|
4129
|
+
return { singular, plural };
|
|
4130
|
+
}
|
|
4131
|
+
async function parseAppleEntries(content, namespace, filePath, fs) {
|
|
4132
|
+
const singular = parseAppleStringsEntries(content, namespace);
|
|
4133
|
+
const siblingPath = stringsDictSiblingPath(filePath);
|
|
4134
|
+
let siblingContent;
|
|
4135
|
+
try {
|
|
4136
|
+
siblingContent = await readFileContent(fs, siblingPath);
|
|
4137
|
+
} catch (error) {
|
|
4138
|
+
if (isEnoent(error)) {
|
|
4139
|
+
return singular;
|
|
4140
|
+
}
|
|
4141
|
+
throw error;
|
|
4142
|
+
}
|
|
4143
|
+
const plural = parseAppleStringsDictEntries(siblingContent, namespace, siblingPath);
|
|
4144
|
+
const merged = new Map(singular);
|
|
4145
|
+
for (const [key, entry] of plural) {
|
|
4146
|
+
if (merged.has(key)) {
|
|
4147
|
+
throw new AdapterError(
|
|
4148
|
+
"INVALID_STRUCTURE",
|
|
4149
|
+
`The key "${key}" is defined both in ${filePath} and as a plural category in ${siblingPath}. Rename one of them to avoid ambiguity.`
|
|
4150
|
+
);
|
|
4151
|
+
}
|
|
4152
|
+
merged.set(key, entry);
|
|
4153
|
+
}
|
|
4154
|
+
return merged;
|
|
4155
|
+
}
|
|
4156
|
+
async function serializeAppleEntries(entries, filePath, fs) {
|
|
4157
|
+
const { singular, plural } = partitionEntries(entries);
|
|
4158
|
+
const siblingPath = stringsDictSiblingPath(filePath);
|
|
4159
|
+
const xml = await serializeAppleStringsDictEntries(plural, siblingPath, fs);
|
|
4160
|
+
if (xml !== void 0) {
|
|
4161
|
+
await fs.writeFileAtomic(siblingPath, xml);
|
|
4162
|
+
}
|
|
4163
|
+
return serializeAppleStringsEntries(singular, filePath, fs);
|
|
4164
|
+
}
|
|
4165
|
+
function createAppleStringsAdapter(fs = nodeAdapterFs) {
|
|
4166
|
+
return createFlatFileAdapter({
|
|
4167
|
+
fs,
|
|
4168
|
+
format: "apple-strings",
|
|
4169
|
+
extensions: [".strings"],
|
|
4170
|
+
parseEntries: parseAppleEntries,
|
|
4171
|
+
serializeEntries: serializeAppleEntries,
|
|
4172
|
+
extractPlaceholders: extractAppleStringsPlaceholders
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4175
|
+
var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
|
|
4176
|
+
var INVALID = { placeholders: [], isPlural: false, valid: false };
|
|
4177
|
+
function tokenOf(element) {
|
|
4178
|
+
switch (element.type) {
|
|
4179
|
+
case TYPE.argument:
|
|
4180
|
+
case TYPE.number:
|
|
4181
|
+
case TYPE.date:
|
|
4182
|
+
case TYPE.time:
|
|
4183
|
+
case TYPE.select:
|
|
4184
|
+
case TYPE.plural:
|
|
4185
|
+
return `{${element.value}}`;
|
|
4186
|
+
case TYPE.tag:
|
|
4187
|
+
return `<${element.value}>`;
|
|
4188
|
+
default:
|
|
4189
|
+
return void 0;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
function childMessages(element) {
|
|
4193
|
+
if (element.type === TYPE.plural || element.type === TYPE.select) {
|
|
4194
|
+
return Object.values(element.options).map((option) => option.value);
|
|
4195
|
+
}
|
|
4196
|
+
if (element.type === TYPE.tag) {
|
|
4197
|
+
return [element.children];
|
|
4198
|
+
}
|
|
4199
|
+
return [];
|
|
4200
|
+
}
|
|
4201
|
+
function addCount(target, token, count) {
|
|
4202
|
+
target.set(token, (target.get(token) ?? 0) + count);
|
|
4203
|
+
}
|
|
4204
|
+
function combineBranches(branchMultisets) {
|
|
2494
4205
|
const combined = /* @__PURE__ */ new Map();
|
|
2495
4206
|
const tokens = /* @__PURE__ */ new Set();
|
|
2496
4207
|
for (const branch of branchMultisets) {
|
|
@@ -2696,16 +4407,6 @@ function compareIcuPlaceholders(sourceValue, targetValue) {
|
|
|
2696
4407
|
return checkPlaceholders(icuPlaceholders(sourceValue), icuPlaceholders(targetValue));
|
|
2697
4408
|
}
|
|
2698
4409
|
}
|
|
2699
|
-
var AdapterError = class extends Error {
|
|
2700
|
-
code;
|
|
2701
|
-
constructor(code, message) {
|
|
2702
|
-
super(message);
|
|
2703
|
-
this.name = "AdapterError";
|
|
2704
|
-
this.code = code;
|
|
2705
|
-
}
|
|
2706
|
-
};
|
|
2707
|
-
var MAX_DEPTH = 100;
|
|
2708
|
-
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
2709
4410
|
var SENTINEL_ESCAPE = "\\u0001";
|
|
2710
4411
|
function isJsonWhitespace(char) {
|
|
2711
4412
|
return char === " " || char === " " || char === "\n" || char === "\r";
|
|
@@ -2878,77 +4579,17 @@ function assertJsonRecord(value) {
|
|
|
2878
4579
|
assertNodesValid(value);
|
|
2879
4580
|
return value;
|
|
2880
4581
|
}
|
|
2881
|
-
function parseJsonObject(content) {
|
|
2882
|
-
return assertJsonRecord(parseOrderedJson(content));
|
|
2883
|
-
}
|
|
2884
|
-
function serializeJsonTree(tree) {
|
|
2885
|
-
return serializeOrderedJson(tree);
|
|
2886
|
-
}
|
|
2887
|
-
function isJsonNode(value) {
|
|
2888
|
-
return value instanceof Map;
|
|
2889
|
-
}
|
|
2890
|
-
function sniffJsonObject(sample) {
|
|
2891
|
-
return sample.trimStart().startsWith("{");
|
|
2892
|
-
}
|
|
2893
|
-
function scanTokens(value, pattern, extract = (match) => match[0]) {
|
|
2894
|
-
const result = [];
|
|
2895
|
-
for (const match of value.matchAll(pattern)) {
|
|
2896
|
-
const token = extract(match);
|
|
2897
|
-
if (token !== void 0) {
|
|
2898
|
-
result.push(token);
|
|
2899
|
-
}
|
|
2900
|
-
}
|
|
2901
|
-
return result;
|
|
2902
|
-
}
|
|
2903
|
-
function namespaceOf(filePath) {
|
|
2904
|
-
return basename(filePath, extname(filePath));
|
|
2905
|
-
}
|
|
2906
|
-
function isEnoent(error) {
|
|
2907
|
-
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2908
|
-
}
|
|
2909
|
-
function rethrowStructured(error, message) {
|
|
2910
|
-
if (error instanceof AdapterError) {
|
|
2911
|
-
throw error;
|
|
2912
|
-
}
|
|
2913
|
-
throw new AdapterError("INVALID_STRUCTURE", message);
|
|
2914
|
-
}
|
|
2915
|
-
function computeIcu(entries, compute) {
|
|
2916
|
-
if (!compute) {
|
|
2917
|
-
return [];
|
|
2918
|
-
}
|
|
2919
|
-
try {
|
|
2920
|
-
return compute(entries);
|
|
2921
|
-
} catch (error) {
|
|
2922
|
-
rethrowStructured(error, "The file could not be analyzed for message validity.");
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
function buildCanHandle(extensions, sniff) {
|
|
2926
|
-
return (filePath, sample) => {
|
|
2927
|
-
if (!extensions.includes(extname(filePath).toLowerCase())) {
|
|
2928
|
-
return false;
|
|
2929
|
-
}
|
|
2930
|
-
return sample === void 0 || sniff === void 0 || sniff(sample);
|
|
2931
|
-
};
|
|
2932
|
-
}
|
|
2933
|
-
function stripLeadingBom(content) {
|
|
2934
|
-
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
4582
|
+
function parseJsonObject(content) {
|
|
4583
|
+
return assertJsonRecord(parseOrderedJson(content));
|
|
2935
4584
|
}
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
return outcome.kind === "ok" ? { kind: "ok", content: stripLeadingBom(outcome.content) } : outcome;
|
|
4585
|
+
function serializeJsonTree(tree) {
|
|
4586
|
+
return serializeOrderedJson(tree);
|
|
2939
4587
|
}
|
|
2940
|
-
function
|
|
2941
|
-
|
|
2942
|
-
throw new AdapterError("INVALID_STRUCTURE", notAFileMessage);
|
|
2943
|
-
}
|
|
2944
|
-
if (outcome.kind === "too-large") {
|
|
2945
|
-
throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
|
|
2946
|
-
}
|
|
2947
|
-
return outcome.content;
|
|
4588
|
+
function isJsonNode(value) {
|
|
4589
|
+
return value instanceof Map;
|
|
2948
4590
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
return outcomeToContent(outcome, "The path is not a regular file.");
|
|
4591
|
+
function sniffJsonObject(sample) {
|
|
4592
|
+
return sample.trimStart().startsWith("{");
|
|
2952
4593
|
}
|
|
2953
4594
|
var BACKSLASH = "\\";
|
|
2954
4595
|
var DOT = ".";
|
|
@@ -3132,7 +4773,7 @@ function mergeDescriptions(entries, content, deriveDescriptions) {
|
|
|
3132
4773
|
}
|
|
3133
4774
|
}
|
|
3134
4775
|
}
|
|
3135
|
-
function
|
|
4776
|
+
function toEntries2(content, namespace, parse3, deriveEntry, keyMode, validateTree, deriveDescriptions) {
|
|
3136
4777
|
try {
|
|
3137
4778
|
const tree = parse3(content);
|
|
3138
4779
|
validateTree?.(tree);
|
|
@@ -3170,7 +4811,7 @@ function createTreeFileAdapter(options) {
|
|
|
3170
4811
|
async read(filePath, locale) {
|
|
3171
4812
|
const content = await readFileContent(fs, filePath);
|
|
3172
4813
|
const namespace = namespaceOf(filePath);
|
|
3173
|
-
const { entries, excludedLeafPaths } =
|
|
4814
|
+
const { entries, excludedLeafPaths } = toEntries2(
|
|
3174
4815
|
content,
|
|
3175
4816
|
namespace,
|
|
3176
4817
|
parse3,
|
|
@@ -3189,114 +4830,781 @@ function createTreeFileAdapter(options) {
|
|
|
3189
4830
|
}
|
|
3190
4831
|
};
|
|
3191
4832
|
}
|
|
3192
|
-
function isMetadataKey(key) {
|
|
3193
|
-
return key.startsWith("@");
|
|
4833
|
+
function isMetadataKey(key) {
|
|
4834
|
+
return key.startsWith("@");
|
|
4835
|
+
}
|
|
4836
|
+
function parseArbObject(content) {
|
|
4837
|
+
const parsed = parseOrderedJson(content);
|
|
4838
|
+
if (!(parsed instanceof Map)) {
|
|
4839
|
+
throw new AdapterError(
|
|
4840
|
+
"INVALID_STRUCTURE",
|
|
4841
|
+
"The file is not a valid object (expected nested objects of string values)."
|
|
4842
|
+
);
|
|
4843
|
+
}
|
|
4844
|
+
return parsed;
|
|
4845
|
+
}
|
|
4846
|
+
function stripArbMetadata(tree) {
|
|
4847
|
+
const out = /* @__PURE__ */ new Map();
|
|
4848
|
+
for (const [key, value] of tree) {
|
|
4849
|
+
if (!isMetadataKey(key)) {
|
|
4850
|
+
out.set(key, value);
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
return out;
|
|
4854
|
+
}
|
|
4855
|
+
function originalKey(encoded) {
|
|
4856
|
+
return decodeKeyToSegments(encoded).join(".");
|
|
4857
|
+
}
|
|
4858
|
+
function messageKeyForMetadata(key) {
|
|
4859
|
+
return isMetadataKey(key) && !key.startsWith("@@") ? key.slice(1) : null;
|
|
4860
|
+
}
|
|
4861
|
+
function descriptionOf(value) {
|
|
4862
|
+
if (!(value instanceof Map)) {
|
|
4863
|
+
return void 0;
|
|
4864
|
+
}
|
|
4865
|
+
const description = value.get("description");
|
|
4866
|
+
return typeof description === "string" ? description : void 0;
|
|
4867
|
+
}
|
|
4868
|
+
function extractArbDescriptions(content) {
|
|
4869
|
+
const tree = parseArbObject(content);
|
|
4870
|
+
const out = /* @__PURE__ */ new Map();
|
|
4871
|
+
for (const [key, value] of tree) {
|
|
4872
|
+
const messageKey = messageKeyForMetadata(key);
|
|
4873
|
+
if (messageKey === null) {
|
|
4874
|
+
continue;
|
|
4875
|
+
}
|
|
4876
|
+
const description = descriptionOf(value);
|
|
4877
|
+
if (description !== void 0) {
|
|
4878
|
+
out.set(encodeSegment(messageKey), description);
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
return out;
|
|
4882
|
+
}
|
|
4883
|
+
function messagesFromEntries(entries) {
|
|
4884
|
+
const out = /* @__PURE__ */ new Map();
|
|
4885
|
+
for (const [key, entry] of entries) {
|
|
4886
|
+
out.set(originalKey(key), entry.value);
|
|
4887
|
+
}
|
|
4888
|
+
return out;
|
|
4889
|
+
}
|
|
4890
|
+
async function readDestinationPairs(filePath, fs) {
|
|
4891
|
+
let content;
|
|
4892
|
+
try {
|
|
4893
|
+
content = await readFileContent(fs, filePath);
|
|
4894
|
+
} catch (error) {
|
|
4895
|
+
if (isEnoent(error)) {
|
|
4896
|
+
return null;
|
|
4897
|
+
}
|
|
4898
|
+
throw error;
|
|
4899
|
+
}
|
|
4900
|
+
return [...parseArbObject(content)];
|
|
4901
|
+
}
|
|
4902
|
+
async function buildArbWriteTree(entries, filePath, fs) {
|
|
4903
|
+
const messages = messagesFromEntries(entries);
|
|
4904
|
+
const pairs = await readDestinationPairs(filePath, fs);
|
|
4905
|
+
const out = /* @__PURE__ */ new Map();
|
|
4906
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
4907
|
+
for (const [key, value] of pairs ?? []) {
|
|
4908
|
+
const translated = isMetadataKey(key) ? void 0 : messages.get(key);
|
|
4909
|
+
if (translated !== void 0) {
|
|
4910
|
+
consumed.add(key);
|
|
4911
|
+
out.set(key, translated);
|
|
4912
|
+
} else if (isMetadataKey(key) || typeof value === "string") {
|
|
4913
|
+
out.set(key, value);
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
for (const [key, value] of messages) {
|
|
4917
|
+
if (!consumed.has(key)) {
|
|
4918
|
+
out.set(key, value);
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
return out;
|
|
4922
|
+
}
|
|
4923
|
+
function parseArb(content) {
|
|
4924
|
+
return assertJsonRecord(stripArbMetadata(parseArbObject(content)));
|
|
4925
|
+
}
|
|
4926
|
+
function createArbAdapter(fs = nodeAdapterFs) {
|
|
4927
|
+
return createTreeFileAdapter({
|
|
4928
|
+
fs,
|
|
4929
|
+
format: "arb",
|
|
4930
|
+
extensions: [".arb"],
|
|
4931
|
+
sniff: sniffJsonObject,
|
|
4932
|
+
parse: parseArb,
|
|
4933
|
+
serialize: serializeJsonTree,
|
|
4934
|
+
extractPlaceholders: icuPlaceholders,
|
|
4935
|
+
deriveEntry: icuDeriveEntry,
|
|
4936
|
+
computeInvalidIcuKeys: icuInvalidKeys,
|
|
4937
|
+
validateMessage: icuIsValid,
|
|
4938
|
+
comparePlaceholders: compareIcuPlaceholders,
|
|
4939
|
+
buildWriteTree: buildArbWriteTree,
|
|
4940
|
+
deriveDescriptions: extractArbDescriptions
|
|
4941
|
+
});
|
|
4942
|
+
}
|
|
4943
|
+
var SIMPLE_ESCAPES = {
|
|
4944
|
+
n: "\n",
|
|
4945
|
+
t: " ",
|
|
4946
|
+
r: "\r",
|
|
4947
|
+
a: "\x07",
|
|
4948
|
+
b: "\b",
|
|
4949
|
+
f: "\f",
|
|
4950
|
+
v: "\v",
|
|
4951
|
+
"0": "\0",
|
|
4952
|
+
'"': '"',
|
|
4953
|
+
"\\": "\\"
|
|
4954
|
+
};
|
|
4955
|
+
var OCTAL_DIGIT = /[0-7]/;
|
|
4956
|
+
function decodeHexEscape(raw, at, line) {
|
|
4957
|
+
let hex = "";
|
|
4958
|
+
let i = at;
|
|
4959
|
+
while (i < raw.length && hex.length < 2 && /[0-9a-fA-F]/.test(raw[i] ?? "")) {
|
|
4960
|
+
hex += raw[i];
|
|
4961
|
+
i += 1;
|
|
4962
|
+
}
|
|
4963
|
+
if (hex === "") {
|
|
4964
|
+
throw new AdapterError(
|
|
4965
|
+
"INVALID_STRUCTURE",
|
|
4966
|
+
`The .po/.pot file has a malformed \\x escape (line ${line}).`
|
|
4967
|
+
);
|
|
4968
|
+
}
|
|
4969
|
+
return { char: String.fromCharCode(Number.parseInt(hex, 16)), next: i };
|
|
4970
|
+
}
|
|
4971
|
+
function decodeOctalEscape(raw, at) {
|
|
4972
|
+
let octal = "";
|
|
4973
|
+
let i = at;
|
|
4974
|
+
while (i < raw.length && octal.length < 3 && OCTAL_DIGIT.test(raw[i] ?? "")) {
|
|
4975
|
+
octal += raw[i];
|
|
4976
|
+
i += 1;
|
|
4977
|
+
}
|
|
4978
|
+
return { char: String.fromCharCode(Number.parseInt(octal, 8)), next: i };
|
|
4979
|
+
}
|
|
4980
|
+
function decodeCString(raw, line) {
|
|
4981
|
+
let out = "";
|
|
4982
|
+
let i = 0;
|
|
4983
|
+
while (i < raw.length) {
|
|
4984
|
+
const char = raw[i];
|
|
4985
|
+
if (char !== "\\") {
|
|
4986
|
+
out += char;
|
|
4987
|
+
i += 1;
|
|
4988
|
+
continue;
|
|
4989
|
+
}
|
|
4990
|
+
const next = raw[i + 1];
|
|
4991
|
+
if (next === void 0) {
|
|
4992
|
+
throw new AdapterError(
|
|
4993
|
+
"INVALID_STRUCTURE",
|
|
4994
|
+
`The .po/.pot file has a trailing, unterminated escape (line ${line}).`
|
|
4995
|
+
);
|
|
4996
|
+
}
|
|
4997
|
+
if (next === "x") {
|
|
4998
|
+
const hex = decodeHexEscape(raw, i + 2, line);
|
|
4999
|
+
out += hex.char;
|
|
5000
|
+
i = hex.next;
|
|
5001
|
+
continue;
|
|
5002
|
+
}
|
|
5003
|
+
if (OCTAL_DIGIT.test(next)) {
|
|
5004
|
+
const octal = decodeOctalEscape(raw, i + 1);
|
|
5005
|
+
out += octal.char;
|
|
5006
|
+
i = octal.next;
|
|
5007
|
+
continue;
|
|
5008
|
+
}
|
|
5009
|
+
const simple = SIMPLE_ESCAPES[next];
|
|
5010
|
+
if (simple === void 0) {
|
|
5011
|
+
throw new AdapterError(
|
|
5012
|
+
"INVALID_STRUCTURE",
|
|
5013
|
+
`The .po/.pot file has an unknown escape sequence "\\${next}" (line ${line}).`
|
|
5014
|
+
);
|
|
5015
|
+
}
|
|
5016
|
+
out += simple;
|
|
5017
|
+
i += 2;
|
|
5018
|
+
}
|
|
5019
|
+
return out;
|
|
5020
|
+
}
|
|
5021
|
+
function escapeChar3(char) {
|
|
5022
|
+
switch (char) {
|
|
5023
|
+
case "\\":
|
|
5024
|
+
return "\\\\";
|
|
5025
|
+
case '"':
|
|
5026
|
+
return '\\"';
|
|
5027
|
+
case "\n":
|
|
5028
|
+
return "\\n";
|
|
5029
|
+
case " ":
|
|
5030
|
+
return "\\t";
|
|
5031
|
+
case "\r":
|
|
5032
|
+
return "\\r";
|
|
5033
|
+
}
|
|
5034
|
+
const code = char.charCodeAt(0);
|
|
5035
|
+
return code < 32 ? `\\x${code.toString(16).padStart(2, "0")}` : char;
|
|
5036
|
+
}
|
|
5037
|
+
function encodeCString(value) {
|
|
5038
|
+
let out = "";
|
|
5039
|
+
for (const char of value) {
|
|
5040
|
+
out += escapeChar3(char);
|
|
5041
|
+
}
|
|
5042
|
+
return out;
|
|
5043
|
+
}
|
|
5044
|
+
var CONTEXT_SEPARATOR = "\uE000";
|
|
5045
|
+
var PLURAL_SUFFIX2 = /^\[(\d+)\]$/;
|
|
5046
|
+
function assertNoReservedChar(raw, field) {
|
|
5047
|
+
if (raw.includes(CONTEXT_SEPARATOR)) {
|
|
5048
|
+
throw new AdapterError(
|
|
5049
|
+
"INVALID_STRUCTURE",
|
|
5050
|
+
`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.`
|
|
5051
|
+
);
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
function escapeBase(raw) {
|
|
5055
|
+
return raw.replace(/\\/g, "\\\\").replace(/\[/g, "\\[");
|
|
5056
|
+
}
|
|
5057
|
+
function unescapeBase(escaped) {
|
|
5058
|
+
let out = "";
|
|
5059
|
+
let i = 0;
|
|
5060
|
+
while (i < escaped.length) {
|
|
5061
|
+
const char = escaped[i];
|
|
5062
|
+
if (char === "\\" && i + 1 < escaped.length) {
|
|
5063
|
+
out += escaped[i + 1];
|
|
5064
|
+
i += 2;
|
|
5065
|
+
continue;
|
|
5066
|
+
}
|
|
5067
|
+
out += char;
|
|
5068
|
+
i += 1;
|
|
5069
|
+
}
|
|
5070
|
+
return out;
|
|
5071
|
+
}
|
|
5072
|
+
function findUnescapedBracket(base) {
|
|
5073
|
+
let i = 0;
|
|
5074
|
+
while (i < base.length) {
|
|
5075
|
+
if (base[i] === "\\") {
|
|
5076
|
+
i += 2;
|
|
5077
|
+
continue;
|
|
5078
|
+
}
|
|
5079
|
+
if (base[i] === "[") {
|
|
5080
|
+
return i;
|
|
5081
|
+
}
|
|
5082
|
+
i += 1;
|
|
5083
|
+
}
|
|
5084
|
+
return -1;
|
|
5085
|
+
}
|
|
5086
|
+
function composeKey(msgctxt, msgid, pluralIndex) {
|
|
5087
|
+
assertNoReservedChar(msgid, "msgid");
|
|
5088
|
+
if (msgctxt !== void 0) {
|
|
5089
|
+
assertNoReservedChar(msgctxt, "msgctxt");
|
|
5090
|
+
}
|
|
5091
|
+
const base = msgctxt === void 0 ? escapeBase(msgid) : `${escapeBase(msgctxt)}${CONTEXT_SEPARATOR}${escapeBase(msgid)}`;
|
|
5092
|
+
return pluralIndex === void 0 ? base : `${base}[${pluralIndex}]`;
|
|
5093
|
+
}
|
|
5094
|
+
function decomposeKey(key) {
|
|
5095
|
+
const bracketAt = findUnescapedBracket(key);
|
|
5096
|
+
let base = key;
|
|
5097
|
+
let pluralIndex;
|
|
5098
|
+
if (bracketAt !== -1) {
|
|
5099
|
+
const suffix = key.slice(bracketAt);
|
|
5100
|
+
const match = PLURAL_SUFFIX2.exec(suffix);
|
|
5101
|
+
if (!match) {
|
|
5102
|
+
throw new AdapterError(
|
|
5103
|
+
"INVALID_STRUCTURE",
|
|
5104
|
+
`The key "${key}" has a malformed plural-index suffix.`
|
|
5105
|
+
);
|
|
5106
|
+
}
|
|
5107
|
+
base = key.slice(0, bracketAt);
|
|
5108
|
+
pluralIndex = Number(match[1]);
|
|
5109
|
+
}
|
|
5110
|
+
const separatorAt = base.indexOf(CONTEXT_SEPARATOR);
|
|
5111
|
+
if (separatorAt === -1) {
|
|
5112
|
+
return { msgctxt: void 0, msgid: unescapeBase(base), pluralIndex };
|
|
5113
|
+
}
|
|
5114
|
+
return {
|
|
5115
|
+
msgctxt: unescapeBase(base.slice(0, separatorAt)),
|
|
5116
|
+
msgid: unescapeBase(base.slice(separatorAt + 1)),
|
|
5117
|
+
pluralIndex
|
|
5118
|
+
};
|
|
5119
|
+
}
|
|
5120
|
+
var GETTEXT_PLACEHOLDER = /%(?:\((\w+)\))?(?:(\d+)\$)?[-+0 #]*(?:[1-9]\d*)?(?:\.\d+)?([disuoxXfFeEgGc])|%%/g;
|
|
5121
|
+
function extractGettextPlaceholders(value) {
|
|
5122
|
+
const out = [];
|
|
5123
|
+
for (const match of value.matchAll(GETTEXT_PLACEHOLDER)) {
|
|
5124
|
+
if (match[0] === "%%") {
|
|
5125
|
+
out.push("%%");
|
|
5126
|
+
continue;
|
|
5127
|
+
}
|
|
5128
|
+
const [, name, position, conversion] = match;
|
|
5129
|
+
if (conversion === void 0) {
|
|
5130
|
+
continue;
|
|
5131
|
+
}
|
|
5132
|
+
if (name !== void 0) {
|
|
5133
|
+
out.push(`%(${name})${conversion}`);
|
|
5134
|
+
} else if (position !== void 0) {
|
|
5135
|
+
out.push(`%${position}$${conversion}`);
|
|
5136
|
+
} else {
|
|
5137
|
+
out.push(`%${conversion}`);
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
return out;
|
|
5141
|
+
}
|
|
5142
|
+
var HEADER_FIELD = /^([\w-]+):\s?(.*)$/;
|
|
5143
|
+
var NPLURALS = /nplurals\s*=\s*(\d+)/;
|
|
5144
|
+
function parseHeaderFields(headerText) {
|
|
5145
|
+
const fields = /* @__PURE__ */ new Map();
|
|
5146
|
+
for (const line of headerText.split("\n")) {
|
|
5147
|
+
const match = HEADER_FIELD.exec(line);
|
|
5148
|
+
if (match?.[1] !== void 0 && match[2] !== void 0) {
|
|
5149
|
+
fields.set(match[1], match[2]);
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
return fields;
|
|
5153
|
+
}
|
|
5154
|
+
function parseNplurals(pluralFormsValue) {
|
|
5155
|
+
const match = NPLURALS.exec(pluralFormsValue);
|
|
5156
|
+
return match?.[1] === void 0 ? void 0 : Number(match[1]);
|
|
5157
|
+
}
|
|
5158
|
+
function defaultPluralFormsExpression(nplurals) {
|
|
5159
|
+
if (nplurals <= 1) {
|
|
5160
|
+
return "nplurals=1; plural=0;";
|
|
5161
|
+
}
|
|
5162
|
+
if (nplurals === 2) {
|
|
5163
|
+
return "nplurals=2; plural=(n != 1);";
|
|
5164
|
+
}
|
|
5165
|
+
return `nplurals=${nplurals}; plural=(n < ${nplurals - 1} ? n : ${nplurals - 1});`;
|
|
5166
|
+
}
|
|
5167
|
+
function joinSelfTerminated(lines, terminator) {
|
|
5168
|
+
return lines.map((line) => `${line}${terminator}`).join("");
|
|
5169
|
+
}
|
|
5170
|
+
function isCommentLine(line) {
|
|
5171
|
+
return line.trimStart().startsWith("#");
|
|
5172
|
+
}
|
|
5173
|
+
function isObsoleteLine(line) {
|
|
5174
|
+
return line.trimStart().startsWith("#~");
|
|
5175
|
+
}
|
|
5176
|
+
function isExtractedCommentLine(line) {
|
|
5177
|
+
return line.trimStart().startsWith("#.");
|
|
5178
|
+
}
|
|
5179
|
+
function extractedCommentText(line) {
|
|
5180
|
+
const rest = line.trimStart().slice(2);
|
|
5181
|
+
return rest.startsWith(" ") ? rest.slice(1) : rest;
|
|
5182
|
+
}
|
|
5183
|
+
function parseQuotedOnLine(line, lineNo) {
|
|
5184
|
+
const trimmed = line.trim();
|
|
5185
|
+
if (trimmed[0] !== '"') {
|
|
5186
|
+
throw new AdapterError(
|
|
5187
|
+
"INVALID_STRUCTURE",
|
|
5188
|
+
`The .po/.pot file expected a quoted string (line ${lineNo}).`
|
|
5189
|
+
);
|
|
5190
|
+
}
|
|
5191
|
+
let i = 1;
|
|
5192
|
+
let raw = "";
|
|
5193
|
+
while (i < trimmed.length && trimmed[i] !== '"') {
|
|
5194
|
+
if (trimmed[i] === "\\") {
|
|
5195
|
+
const next = trimmed[i + 1];
|
|
5196
|
+
if (next === void 0) {
|
|
5197
|
+
throw new AdapterError(
|
|
5198
|
+
"INVALID_STRUCTURE",
|
|
5199
|
+
`The .po/.pot file has an unterminated escape in a quoted string (line ${lineNo}).`
|
|
5200
|
+
);
|
|
5201
|
+
}
|
|
5202
|
+
raw += trimmed[i] + next;
|
|
5203
|
+
i += 2;
|
|
5204
|
+
continue;
|
|
5205
|
+
}
|
|
5206
|
+
raw += trimmed[i];
|
|
5207
|
+
i += 1;
|
|
5208
|
+
}
|
|
5209
|
+
if (trimmed[i] !== '"') {
|
|
5210
|
+
throw new AdapterError(
|
|
5211
|
+
"INVALID_STRUCTURE",
|
|
5212
|
+
`The .po/.pot file has an unterminated quoted string (line ${lineNo}).`
|
|
5213
|
+
);
|
|
5214
|
+
}
|
|
5215
|
+
if (i !== trimmed.length - 1) {
|
|
5216
|
+
throw new AdapterError(
|
|
5217
|
+
"INVALID_STRUCTURE",
|
|
5218
|
+
`The .po/.pot file has unexpected content after a quoted string (line ${lineNo}).`
|
|
5219
|
+
);
|
|
5220
|
+
}
|
|
5221
|
+
return decodeCString(raw, lineNo);
|
|
5222
|
+
}
|
|
5223
|
+
function isBareQuotedLine(line) {
|
|
5224
|
+
return line.trim().startsWith('"');
|
|
5225
|
+
}
|
|
5226
|
+
function gatherStringValue(lines, start, keyword) {
|
|
5227
|
+
const firstLine = (lines[start] ?? "").trim();
|
|
5228
|
+
const rest = firstLine.slice(keyword.length).trim();
|
|
5229
|
+
let decoded = parseQuotedOnLine(rest, start + 1);
|
|
5230
|
+
let i = start + 1;
|
|
5231
|
+
while (i < lines.length && isBareQuotedLine(lines[i] ?? "")) {
|
|
5232
|
+
decoded += parseQuotedOnLine((lines[i] ?? "").trim(), i + 1);
|
|
5233
|
+
i += 1;
|
|
5234
|
+
}
|
|
5235
|
+
return { decoded, nextIndex: i };
|
|
5236
|
+
}
|
|
5237
|
+
var MSGSTR_INDEX = /^msgstr\[(\d+)\]/;
|
|
5238
|
+
function parseLeadingComments(lines, start) {
|
|
5239
|
+
const descriptionParts = [];
|
|
5240
|
+
let i = start;
|
|
5241
|
+
while (i < lines.length && isCommentLine(lines[i] ?? "")) {
|
|
5242
|
+
const line = lines[i] ?? "";
|
|
5243
|
+
if (isExtractedCommentLine(line)) {
|
|
5244
|
+
descriptionParts.push(extractedCommentText(line));
|
|
5245
|
+
}
|
|
5246
|
+
i += 1;
|
|
5247
|
+
}
|
|
5248
|
+
return {
|
|
5249
|
+
description: descriptionParts.length > 0 ? descriptionParts.join("\n") : void 0,
|
|
5250
|
+
next: i
|
|
5251
|
+
};
|
|
5252
|
+
}
|
|
5253
|
+
function parseOptionalMsgctxt(lines, start) {
|
|
5254
|
+
if (!/^msgctxt\s+"/.test((lines[start] ?? "").trim())) {
|
|
5255
|
+
return { msgctxt: void 0, next: start };
|
|
5256
|
+
}
|
|
5257
|
+
const result = gatherStringValue(lines, start, "msgctxt");
|
|
5258
|
+
return { msgctxt: result.decoded, next: result.nextIndex };
|
|
3194
5259
|
}
|
|
3195
|
-
function
|
|
3196
|
-
const
|
|
3197
|
-
|
|
5260
|
+
function parsePluralMsgstrs(lines, start, msgid) {
|
|
5261
|
+
const values = /* @__PURE__ */ new Map();
|
|
5262
|
+
let i = start;
|
|
5263
|
+
while (/^msgstr\[\d+\]\s*"/.test((lines[i] ?? "").trim())) {
|
|
5264
|
+
const trimmed = (lines[i] ?? "").trim();
|
|
5265
|
+
const match = MSGSTR_INDEX.exec(trimmed);
|
|
5266
|
+
const index = Number(match?.[1] ?? "0");
|
|
5267
|
+
if (index !== values.size) {
|
|
5268
|
+
throw new AdapterError(
|
|
5269
|
+
"INVALID_STRUCTURE",
|
|
5270
|
+
`The .po/.pot file has a non-contiguous plural index for msgid "${msgid}": expected msgstr[${values.size}], found msgstr[${index}] (line ${i + 1}).`
|
|
5271
|
+
);
|
|
5272
|
+
}
|
|
5273
|
+
const result = gatherStringValue(lines, i, `msgstr[${index}]`);
|
|
5274
|
+
values.set(index, result.decoded);
|
|
5275
|
+
i = result.nextIndex;
|
|
5276
|
+
}
|
|
5277
|
+
if (values.size === 0) {
|
|
3198
5278
|
throw new AdapterError(
|
|
3199
5279
|
"INVALID_STRUCTURE",
|
|
3200
|
-
|
|
5280
|
+
`The .po/.pot file is missing msgstr[n] forms for the plural msgid "${msgid}" (line ${start + 1}).`
|
|
3201
5281
|
);
|
|
3202
5282
|
}
|
|
3203
|
-
return
|
|
5283
|
+
return { values, next: i };
|
|
3204
5284
|
}
|
|
3205
|
-
function
|
|
3206
|
-
const
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
5285
|
+
function parseRecord(lines, start, terminator) {
|
|
5286
|
+
const comments = parseLeadingComments(lines, start);
|
|
5287
|
+
let i = comments.next;
|
|
5288
|
+
if (i >= lines.length) {
|
|
5289
|
+
throw new AdapterError(
|
|
5290
|
+
"INVALID_STRUCTURE",
|
|
5291
|
+
`The .po/.pot file ends after a comment block with no entry following it (line ${i}).`
|
|
5292
|
+
);
|
|
5293
|
+
}
|
|
5294
|
+
const ctx = parseOptionalMsgctxt(lines, i);
|
|
5295
|
+
i = ctx.next;
|
|
5296
|
+
if (!/^msgid\s+"/.test((lines[i] ?? "").trim())) {
|
|
5297
|
+
throw new AdapterError(
|
|
5298
|
+
"INVALID_STRUCTURE",
|
|
5299
|
+
`The .po/.pot file expected "msgid" (line ${i + 1}).`
|
|
5300
|
+
);
|
|
5301
|
+
}
|
|
5302
|
+
const msgidResult = gatherStringValue(lines, i, "msgid");
|
|
5303
|
+
i = msgidResult.nextIndex;
|
|
5304
|
+
let msgidPlural;
|
|
5305
|
+
if (/^msgid_plural\s+"/.test((lines[i] ?? "").trim())) {
|
|
5306
|
+
const result = gatherStringValue(lines, i, "msgid_plural");
|
|
5307
|
+
msgidPlural = result.decoded;
|
|
5308
|
+
i = result.nextIndex;
|
|
5309
|
+
}
|
|
5310
|
+
const prefixRaw = joinSelfTerminated(lines.slice(start, i), terminator);
|
|
5311
|
+
if (msgidPlural !== void 0) {
|
|
5312
|
+
const plural = parsePluralMsgstrs(lines, i, msgidResult.decoded);
|
|
5313
|
+
return {
|
|
5314
|
+
msgctxt: ctx.msgctxt,
|
|
5315
|
+
msgid: msgidResult.decoded,
|
|
5316
|
+
msgidPlural,
|
|
5317
|
+
description: comments.description,
|
|
5318
|
+
prefixRaw,
|
|
5319
|
+
singularValue: void 0,
|
|
5320
|
+
pluralValues: plural.values,
|
|
5321
|
+
entryEndIndex: plural.next
|
|
5322
|
+
};
|
|
5323
|
+
}
|
|
5324
|
+
if (!/^msgstr\s+"/.test((lines[i] ?? "").trim())) {
|
|
5325
|
+
throw new AdapterError(
|
|
5326
|
+
"INVALID_STRUCTURE",
|
|
5327
|
+
`The .po/.pot file expected "msgstr" for msgid "${msgidResult.decoded}" (line ${i + 1}).`
|
|
5328
|
+
);
|
|
5329
|
+
}
|
|
5330
|
+
const msgstrResult = gatherStringValue(lines, i, "msgstr");
|
|
5331
|
+
return {
|
|
5332
|
+
msgctxt: ctx.msgctxt,
|
|
5333
|
+
msgid: msgidResult.decoded,
|
|
5334
|
+
msgidPlural: void 0,
|
|
5335
|
+
description: comments.description,
|
|
5336
|
+
prefixRaw,
|
|
5337
|
+
singularValue: msgstrResult.decoded,
|
|
5338
|
+
pluralValues: void 0,
|
|
5339
|
+
entryEndIndex: msgstrResult.nextIndex
|
|
5340
|
+
};
|
|
5341
|
+
}
|
|
5342
|
+
function isHeaderRecord(record) {
|
|
5343
|
+
return record.msgctxt === void 0 && record.msgid === "" && record.msgidPlural === void 0;
|
|
5344
|
+
}
|
|
5345
|
+
function scanPo(content) {
|
|
5346
|
+
const terminator = detectLineTerminator(content);
|
|
5347
|
+
const lines = splitPhysicalLines(content);
|
|
5348
|
+
const nodes = [];
|
|
5349
|
+
let raw = [];
|
|
5350
|
+
let headerFields;
|
|
5351
|
+
const flushRaw = () => {
|
|
5352
|
+
if (raw.length > 0) {
|
|
5353
|
+
nodes.push({ kind: "raw", text: joinSelfTerminated(raw, terminator) });
|
|
5354
|
+
raw = [];
|
|
5355
|
+
}
|
|
5356
|
+
};
|
|
5357
|
+
let i = 0;
|
|
5358
|
+
while (i < lines.length) {
|
|
5359
|
+
const line = lines[i] ?? "";
|
|
5360
|
+
if (line.trim() === "") {
|
|
5361
|
+
raw.push(line);
|
|
5362
|
+
i += 1;
|
|
5363
|
+
continue;
|
|
5364
|
+
}
|
|
5365
|
+
if (isObsoleteLine(line)) {
|
|
5366
|
+
while (i < lines.length && isObsoleteLine(lines[i] ?? "")) {
|
|
5367
|
+
raw.push(lines[i] ?? "");
|
|
5368
|
+
i += 1;
|
|
5369
|
+
}
|
|
5370
|
+
continue;
|
|
5371
|
+
}
|
|
5372
|
+
const record = parseRecord(lines, i, terminator);
|
|
5373
|
+
if (isHeaderRecord(record)) {
|
|
5374
|
+
flushRaw();
|
|
5375
|
+
nodes.push({
|
|
5376
|
+
kind: "raw",
|
|
5377
|
+
text: joinSelfTerminated(lines.slice(i, record.entryEndIndex), terminator)
|
|
5378
|
+
});
|
|
5379
|
+
headerFields = parseHeaderFields(record.singularValue ?? "");
|
|
5380
|
+
i = record.entryEndIndex;
|
|
5381
|
+
continue;
|
|
3210
5382
|
}
|
|
5383
|
+
flushRaw();
|
|
5384
|
+
nodes.push({
|
|
5385
|
+
kind: "entry",
|
|
5386
|
+
prefixRaw: record.prefixRaw,
|
|
5387
|
+
msgctxt: record.msgctxt,
|
|
5388
|
+
msgid: record.msgid,
|
|
5389
|
+
msgidPlural: record.msgidPlural,
|
|
5390
|
+
description: record.description,
|
|
5391
|
+
singularValue: record.singularValue,
|
|
5392
|
+
pluralValues: record.pluralValues
|
|
5393
|
+
});
|
|
5394
|
+
i = record.entryEndIndex;
|
|
3211
5395
|
}
|
|
3212
|
-
|
|
5396
|
+
flushRaw();
|
|
5397
|
+
return { nodes, terminator, headerFields };
|
|
3213
5398
|
}
|
|
3214
|
-
function
|
|
3215
|
-
|
|
5399
|
+
function assertPluralIndicesInRange(node, pluralValues, nplurals) {
|
|
5400
|
+
if (nplurals === void 0) {
|
|
5401
|
+
throw new AdapterError(
|
|
5402
|
+
"INVALID_STRUCTURE",
|
|
5403
|
+
`The msgid "${node.msgid}" has plural forms, but the file has no "Plural-Forms" header declaring nplurals.`
|
|
5404
|
+
);
|
|
5405
|
+
}
|
|
5406
|
+
const maxIndex = Math.max(...pluralValues.keys());
|
|
5407
|
+
if (maxIndex >= nplurals) {
|
|
5408
|
+
throw new AdapterError(
|
|
5409
|
+
"INVALID_STRUCTURE",
|
|
5410
|
+
`The msgid "${node.msgid}" has msgstr[${maxIndex}], but the header declares nplurals=${nplurals} (valid indices 0..${nplurals - 1}).`
|
|
5411
|
+
);
|
|
5412
|
+
}
|
|
5413
|
+
return maxIndex;
|
|
3216
5414
|
}
|
|
3217
|
-
function
|
|
3218
|
-
|
|
5415
|
+
function addPluralEntries(entries, node, pluralValues, namespace, nplurals) {
|
|
5416
|
+
assertPluralIndicesInRange(node, pluralValues, nplurals);
|
|
5417
|
+
for (const [index, value] of pluralValues) {
|
|
5418
|
+
const key = composeKey(node.msgctxt, node.msgid, index);
|
|
5419
|
+
entries.set(key, {
|
|
5420
|
+
key,
|
|
5421
|
+
namespace,
|
|
5422
|
+
value,
|
|
5423
|
+
placeholders: extractGettextPlaceholders(value),
|
|
5424
|
+
isPlural: true,
|
|
5425
|
+
...node.msgidPlural !== void 0 ? { meaning: node.msgidPlural } : {},
|
|
5426
|
+
...node.description !== void 0 ? { description: node.description } : {}
|
|
5427
|
+
});
|
|
5428
|
+
}
|
|
3219
5429
|
}
|
|
3220
|
-
function
|
|
3221
|
-
|
|
5430
|
+
function addSingularEntry(entries, node, namespace) {
|
|
5431
|
+
const key = composeKey(node.msgctxt, node.msgid);
|
|
5432
|
+
const value = node.singularValue ?? "";
|
|
5433
|
+
entries.set(key, {
|
|
5434
|
+
key,
|
|
5435
|
+
namespace,
|
|
5436
|
+
value,
|
|
5437
|
+
placeholders: extractGettextPlaceholders(value),
|
|
5438
|
+
isPlural: false,
|
|
5439
|
+
...node.description !== void 0 ? { description: node.description } : {}
|
|
5440
|
+
});
|
|
5441
|
+
}
|
|
5442
|
+
function headerNplurals(doc) {
|
|
5443
|
+
if (doc.headerFields === void 0) {
|
|
3222
5444
|
return void 0;
|
|
3223
5445
|
}
|
|
3224
|
-
|
|
3225
|
-
return typeof description === "string" ? description : void 0;
|
|
5446
|
+
return parseNplurals(doc.headerFields.get("Plural-Forms") ?? "");
|
|
3226
5447
|
}
|
|
3227
|
-
function
|
|
3228
|
-
const
|
|
3229
|
-
const
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
if (
|
|
5448
|
+
function parsePoEntries(content, namespace) {
|
|
5449
|
+
const doc = scanPo(content);
|
|
5450
|
+
const nplurals = headerNplurals(doc);
|
|
5451
|
+
const entries = /* @__PURE__ */ new Map();
|
|
5452
|
+
for (const node of doc.nodes) {
|
|
5453
|
+
if (node.kind !== "entry") {
|
|
3233
5454
|
continue;
|
|
3234
5455
|
}
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
5456
|
+
if (node.pluralValues !== void 0) {
|
|
5457
|
+
addPluralEntries(entries, node, node.pluralValues, namespace, nplurals);
|
|
5458
|
+
continue;
|
|
3238
5459
|
}
|
|
5460
|
+
addSingularEntry(entries, node, namespace);
|
|
3239
5461
|
}
|
|
3240
|
-
return
|
|
5462
|
+
return entries;
|
|
3241
5463
|
}
|
|
3242
|
-
function
|
|
3243
|
-
const
|
|
5464
|
+
function groupEntries2(entries) {
|
|
5465
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3244
5466
|
for (const [key, entry] of entries) {
|
|
3245
|
-
|
|
5467
|
+
const decomposed = decomposeKey(key);
|
|
5468
|
+
const groupKey = composeKey(decomposed.msgctxt, decomposed.msgid);
|
|
5469
|
+
let group = groups.get(groupKey);
|
|
5470
|
+
if (group === void 0) {
|
|
5471
|
+
group = {
|
|
5472
|
+
msgctxt: decomposed.msgctxt,
|
|
5473
|
+
msgid: decomposed.msgid,
|
|
5474
|
+
msgidPlural: void 0,
|
|
5475
|
+
values: /* @__PURE__ */ new Map()
|
|
5476
|
+
};
|
|
5477
|
+
groups.set(groupKey, group);
|
|
5478
|
+
}
|
|
5479
|
+
if (decomposed.pluralIndex !== void 0 && entry.meaning !== void 0) {
|
|
5480
|
+
group.msgidPlural = entry.meaning;
|
|
5481
|
+
}
|
|
5482
|
+
group.values.set(decomposed.pluralIndex, entry.value);
|
|
3246
5483
|
}
|
|
3247
|
-
return
|
|
5484
|
+
return groups;
|
|
3248
5485
|
}
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
|
|
5486
|
+
function renderNewGroup(group, terminator) {
|
|
5487
|
+
const lines = [];
|
|
5488
|
+
if (group.msgctxt !== void 0) {
|
|
5489
|
+
lines.push(`msgctxt "${encodeCString(group.msgctxt)}"`);
|
|
5490
|
+
}
|
|
5491
|
+
lines.push(`msgid "${encodeCString(group.msgid)}"`);
|
|
5492
|
+
const pluralPairs = [...group.values.entries()].filter((pair) => pair[0] !== void 0).sort((a, b) => a[0] - b[0]);
|
|
5493
|
+
if (pluralPairs.length > 0) {
|
|
5494
|
+
lines.push(`msgid_plural "${encodeCString(group.msgidPlural ?? group.msgid)}"`);
|
|
5495
|
+
for (const [index, value] of pluralPairs) {
|
|
5496
|
+
lines.push(`msgstr[${index}] "${encodeCString(value)}"`);
|
|
3256
5497
|
}
|
|
3257
|
-
|
|
5498
|
+
} else {
|
|
5499
|
+
const singular = group.values.get(void 0);
|
|
5500
|
+
lines.push(`msgstr "${encodeCString(singular ?? "")}"`);
|
|
3258
5501
|
}
|
|
3259
|
-
return
|
|
5502
|
+
return `${joinSelfTerminated(lines, terminator)}${terminator}`;
|
|
3260
5503
|
}
|
|
3261
|
-
|
|
3262
|
-
const
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
if (
|
|
3269
|
-
|
|
3270
|
-
out.set(key, translated);
|
|
3271
|
-
} else if (isMetadataKey(key) || typeof value === "string") {
|
|
3272
|
-
out.set(key, value);
|
|
5504
|
+
function renderMsgstrLine(index, value, terminator) {
|
|
5505
|
+
const keyword = index === void 0 ? "msgstr" : `msgstr[${index}]`;
|
|
5506
|
+
return `${keyword} "${encodeCString(value)}"${terminator}`;
|
|
5507
|
+
}
|
|
5508
|
+
function appendNewEntries(entries, emitted, terminator) {
|
|
5509
|
+
const pending = /* @__PURE__ */ new Map();
|
|
5510
|
+
for (const [key, entry] of entries) {
|
|
5511
|
+
if (!emitted.has(key)) {
|
|
5512
|
+
pending.set(key, entry);
|
|
3273
5513
|
}
|
|
3274
5514
|
}
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
5515
|
+
if (pending.size === 0) {
|
|
5516
|
+
return "";
|
|
5517
|
+
}
|
|
5518
|
+
let out = "";
|
|
5519
|
+
for (const group of groupEntries2(pending).values()) {
|
|
5520
|
+
out += renderNewGroup(group, terminator);
|
|
5521
|
+
}
|
|
5522
|
+
return out;
|
|
5523
|
+
}
|
|
5524
|
+
function rewriteDocument(doc, entries) {
|
|
5525
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
5526
|
+
const parts = [];
|
|
5527
|
+
for (const node of doc.nodes) {
|
|
5528
|
+
if (node.kind === "raw") {
|
|
5529
|
+
parts.push(node.text);
|
|
5530
|
+
continue;
|
|
5531
|
+
}
|
|
5532
|
+
if (node.pluralValues !== void 0) {
|
|
5533
|
+
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(
|
|
5534
|
+
(candidate) => candidate.entry !== void 0
|
|
5535
|
+
).sort((a, b) => a.index - b.index);
|
|
5536
|
+
if (survivors.length === 0) {
|
|
5537
|
+
continue;
|
|
5538
|
+
}
|
|
5539
|
+
parts.push(node.prefixRaw);
|
|
5540
|
+
for (const { index, key: key2, entry: entry2 } of survivors) {
|
|
5541
|
+
parts.push(renderMsgstrLine(index, entry2.value, doc.terminator));
|
|
5542
|
+
emitted.add(key2);
|
|
5543
|
+
}
|
|
5544
|
+
continue;
|
|
5545
|
+
}
|
|
5546
|
+
const key = composeKey(node.msgctxt, node.msgid);
|
|
5547
|
+
const entry = entries.get(key);
|
|
5548
|
+
if (entry === void 0) {
|
|
5549
|
+
continue;
|
|
5550
|
+
}
|
|
5551
|
+
parts.push(node.prefixRaw);
|
|
5552
|
+
parts.push(renderMsgstrLine(void 0, entry.value, doc.terminator));
|
|
5553
|
+
emitted.add(key);
|
|
5554
|
+
}
|
|
5555
|
+
return parts.join("") + appendNewEntries(entries, emitted, doc.terminator);
|
|
5556
|
+
}
|
|
5557
|
+
function synthesizeFromScratch(entries) {
|
|
5558
|
+
const terminator = "\n";
|
|
5559
|
+
const groups = groupEntries2(entries);
|
|
5560
|
+
let maxIndex = -1;
|
|
5561
|
+
for (const group of groups.values()) {
|
|
5562
|
+
for (const index of group.values.keys()) {
|
|
5563
|
+
if (index !== void 0 && index > maxIndex) {
|
|
5564
|
+
maxIndex = index;
|
|
5565
|
+
}
|
|
3278
5566
|
}
|
|
3279
5567
|
}
|
|
5568
|
+
const headerLines = [
|
|
5569
|
+
'msgid ""',
|
|
5570
|
+
'msgstr ""',
|
|
5571
|
+
'"Content-Type: text/plain; charset=UTF-8\\n"',
|
|
5572
|
+
'"Content-Transfer-Encoding: 8bit\\n"'
|
|
5573
|
+
];
|
|
5574
|
+
if (maxIndex >= 0) {
|
|
5575
|
+
headerLines.push(`"Plural-Forms: ${defaultPluralFormsExpression(maxIndex + 1)}\\n"`);
|
|
5576
|
+
}
|
|
5577
|
+
let out = `${joinSelfTerminated(headerLines, terminator)}${terminator}`;
|
|
5578
|
+
for (const group of groups.values()) {
|
|
5579
|
+
out += renderNewGroup(group, terminator);
|
|
5580
|
+
}
|
|
3280
5581
|
return out;
|
|
3281
5582
|
}
|
|
3282
|
-
function
|
|
3283
|
-
|
|
5583
|
+
async function readDestination2(filePath, fs) {
|
|
5584
|
+
let outcome;
|
|
5585
|
+
try {
|
|
5586
|
+
outcome = await readBoundedFile(fs, filePath);
|
|
5587
|
+
} catch (error) {
|
|
5588
|
+
if (isEnoent(error)) {
|
|
5589
|
+
return void 0;
|
|
5590
|
+
}
|
|
5591
|
+
throw new AdapterError("INVALID_STRUCTURE", "The destination file could not be read.");
|
|
5592
|
+
}
|
|
5593
|
+
const content = outcomeToContent(outcome, "The destination path is not a regular file.");
|
|
5594
|
+
return scanPo(content);
|
|
3284
5595
|
}
|
|
3285
|
-
function
|
|
3286
|
-
|
|
5596
|
+
async function serializePoEntries(entries, filePath, fs) {
|
|
5597
|
+
const doc = await readDestination2(filePath, fs);
|
|
5598
|
+
return doc === void 0 ? synthesizeFromScratch(entries) : rewriteDocument(doc, entries);
|
|
5599
|
+
}
|
|
5600
|
+
function createGettextAdapter(fs = nodeAdapterFs) {
|
|
5601
|
+
return createFlatFileAdapter({
|
|
3287
5602
|
fs,
|
|
3288
|
-
format: "
|
|
3289
|
-
extensions: [".
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
extractPlaceholders: icuPlaceholders,
|
|
3294
|
-
deriveEntry: icuDeriveEntry,
|
|
3295
|
-
computeInvalidIcuKeys: icuInvalidKeys,
|
|
3296
|
-
validateMessage: icuIsValid,
|
|
3297
|
-
comparePlaceholders: compareIcuPlaceholders,
|
|
3298
|
-
buildWriteTree: buildArbWriteTree,
|
|
3299
|
-
deriveDescriptions: extractArbDescriptions
|
|
5603
|
+
format: "gettext-po",
|
|
5604
|
+
extensions: [".po", ".pot"],
|
|
5605
|
+
parseEntries: parsePoEntries,
|
|
5606
|
+
serializeEntries: serializePoEntries,
|
|
5607
|
+
extractPlaceholders: extractGettextPlaceholders
|
|
3300
5608
|
});
|
|
3301
5609
|
}
|
|
3302
5610
|
function createJsonFileAdapter(options) {
|
|
@@ -3348,23 +5656,6 @@ function extractDoubleBracePlaceholders(value) {
|
|
|
3348
5656
|
function extractI18nextPlaceholders(value) {
|
|
3349
5657
|
return scanTokens(value, I18NEXT_PATTERN);
|
|
3350
5658
|
}
|
|
3351
|
-
var PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/;
|
|
3352
|
-
function isPluralKey(key) {
|
|
3353
|
-
return PLURAL_SUFFIX.test(key);
|
|
3354
|
-
}
|
|
3355
|
-
function pluralCategoryOf(key) {
|
|
3356
|
-
const match = PLURAL_SUFFIX.exec(key);
|
|
3357
|
-
return match?.[1];
|
|
3358
|
-
}
|
|
3359
|
-
function pluralBaseKey(key) {
|
|
3360
|
-
if (!isPluralKey(key)) {
|
|
3361
|
-
return void 0;
|
|
3362
|
-
}
|
|
3363
|
-
return key.replace(PLURAL_SUFFIX, "");
|
|
3364
|
-
}
|
|
3365
|
-
function makePluralKey(baseKey, category) {
|
|
3366
|
-
return `${baseKey}_${category}`;
|
|
3367
|
-
}
|
|
3368
5659
|
function createI18nextJsonAdapter(fs = nodeAdapterFs) {
|
|
3369
5660
|
return createJsonFileAdapter({
|
|
3370
5661
|
fs,
|
|
@@ -3465,45 +5756,7 @@ function createNgxTranslateJsonAdapter(fs = nodeAdapterFs) {
|
|
|
3465
5756
|
validateTree: assertNotMixed,
|
|
3466
5757
|
buildWriteTree: buildNgxWriteTree,
|
|
3467
5758
|
keyMode: "path-notation"
|
|
3468
|
-
});
|
|
3469
|
-
}
|
|
3470
|
-
function toEntries2(content, namespace, parseEntries) {
|
|
3471
|
-
try {
|
|
3472
|
-
return parseEntries(content, namespace);
|
|
3473
|
-
} catch (error) {
|
|
3474
|
-
rethrowStructured(error, "The file could not be parsed.");
|
|
3475
|
-
}
|
|
3476
|
-
}
|
|
3477
|
-
function createFlatFileAdapter(options) {
|
|
3478
|
-
const {
|
|
3479
|
-
format,
|
|
3480
|
-
extensions,
|
|
3481
|
-
sniff,
|
|
3482
|
-
parseEntries,
|
|
3483
|
-
serializeEntries,
|
|
3484
|
-
extractPlaceholders,
|
|
3485
|
-
validateMessage,
|
|
3486
|
-
computeInvalidIcuKeys,
|
|
3487
|
-
fs = nodeAdapterFs
|
|
3488
|
-
} = options;
|
|
3489
|
-
return {
|
|
3490
|
-
format,
|
|
3491
|
-
canHandle: buildCanHandle(extensions, sniff),
|
|
3492
|
-
extractPlaceholders,
|
|
3493
|
-
validateMessage: validateMessage ?? (() => true),
|
|
3494
|
-
async read(filePath, locale) {
|
|
3495
|
-
const content = await readFileContent(fs, filePath);
|
|
3496
|
-
const namespace = namespaceOf(filePath);
|
|
3497
|
-
const entries = toEntries2(content, namespace, parseEntries);
|
|
3498
|
-
const resource = { locale, namespace, format, entries };
|
|
3499
|
-
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
3500
|
-
return { resource, invalidIcuKeys, excludedLeafPaths: [] };
|
|
3501
|
-
},
|
|
3502
|
-
async write(resource, filePath) {
|
|
3503
|
-
const data = await serializeEntries(resource.entries, filePath, fs);
|
|
3504
|
-
await fs.writeFileAtomic(filePath, data);
|
|
3505
|
-
}
|
|
3506
|
-
};
|
|
5759
|
+
});
|
|
3507
5760
|
}
|
|
3508
5761
|
var SUBMESSAGE_TYPES = /* @__PURE__ */ new Set(["plural", "select", "selectordinal", "choice"]);
|
|
3509
5762
|
var ARGUMENT_NAME = /^(?:\d+|[A-Za-z_$][\w$-]*)$/;
|
|
@@ -3603,28 +5856,11 @@ function extractPropertiesPlaceholders(value) {
|
|
|
3603
5856
|
scanRange(value, matchingBraces(value), 0, value.length, true, out);
|
|
3604
5857
|
return out;
|
|
3605
5858
|
}
|
|
3606
|
-
var
|
|
5859
|
+
var UNICODE_ESCAPE3 = /^[0-9a-fA-F]{4}$/;
|
|
3607
5860
|
var LEADING_WHITESPACE = /^[ \t\f]+/;
|
|
3608
|
-
var TRAILING_TERMINATOR = /(?:\r\n|\r|\n)$/;
|
|
3609
|
-
function detectLineTerminator(content) {
|
|
3610
|
-
if (content.includes("\r\n")) {
|
|
3611
|
-
return "\r\n";
|
|
3612
|
-
}
|
|
3613
|
-
return content.includes("\r") ? "\r" : "\n";
|
|
3614
|
-
}
|
|
3615
5861
|
function isPropertiesWhitespace(char) {
|
|
3616
5862
|
return char === " " || char === " " || char === "\f";
|
|
3617
5863
|
}
|
|
3618
|
-
function splitPhysicalLines(content) {
|
|
3619
|
-
if (content === "") {
|
|
3620
|
-
return [];
|
|
3621
|
-
}
|
|
3622
|
-
const lines = content.split(/\r\n|\r|\n/);
|
|
3623
|
-
if (TRAILING_TERMINATOR.test(content)) {
|
|
3624
|
-
lines.pop();
|
|
3625
|
-
}
|
|
3626
|
-
return lines;
|
|
3627
|
-
}
|
|
3628
5864
|
function countTrailingBackslashes(line) {
|
|
3629
5865
|
let count = 0;
|
|
3630
5866
|
for (let i = line.length - 1; i >= 0 && line[i] === "\\"; i -= 1) {
|
|
@@ -3638,7 +5874,7 @@ function isContinued(line) {
|
|
|
3638
5874
|
function isBlankLine(line) {
|
|
3639
5875
|
return line.trim() === "";
|
|
3640
5876
|
}
|
|
3641
|
-
function
|
|
5877
|
+
function isCommentLine2(line) {
|
|
3642
5878
|
const trimmed = line.replace(LEADING_WHITESPACE, "");
|
|
3643
5879
|
return trimmed.startsWith("#") || trimmed.startsWith("!");
|
|
3644
5880
|
}
|
|
@@ -3657,7 +5893,7 @@ function joinContinuation(lines, start) {
|
|
|
3657
5893
|
pieces.push(current);
|
|
3658
5894
|
return { logical: pieces.join(""), nextIndex: index + 1 };
|
|
3659
5895
|
}
|
|
3660
|
-
function
|
|
5896
|
+
function decodeSimpleEscape2(char) {
|
|
3661
5897
|
switch (char) {
|
|
3662
5898
|
case "t":
|
|
3663
5899
|
return " ";
|
|
@@ -3671,9 +5907,9 @@ function decodeSimpleEscape(char) {
|
|
|
3671
5907
|
return char;
|
|
3672
5908
|
}
|
|
3673
5909
|
}
|
|
3674
|
-
function
|
|
5910
|
+
function decodeUnicodeEscape3(raw, at) {
|
|
3675
5911
|
const hex = raw.slice(at, at + 4);
|
|
3676
|
-
if (!
|
|
5912
|
+
if (!UNICODE_ESCAPE3.test(hex)) {
|
|
3677
5913
|
throw new AdapterError("INVALID_STRUCTURE", "The file has a malformed unicode escape.");
|
|
3678
5914
|
}
|
|
3679
5915
|
return String.fromCharCode(Number.parseInt(hex, 16));
|
|
@@ -3693,11 +5929,11 @@ function decodeEscapes(raw) {
|
|
|
3693
5929
|
break;
|
|
3694
5930
|
}
|
|
3695
5931
|
if (next === "u") {
|
|
3696
|
-
out +=
|
|
5932
|
+
out += decodeUnicodeEscape3(raw, i + 2);
|
|
3697
5933
|
i += 6;
|
|
3698
5934
|
continue;
|
|
3699
5935
|
}
|
|
3700
|
-
out +=
|
|
5936
|
+
out += decodeSimpleEscape2(next);
|
|
3701
5937
|
i += 2;
|
|
3702
5938
|
}
|
|
3703
5939
|
return out;
|
|
@@ -3745,7 +5981,7 @@ function parseItems(content) {
|
|
|
3745
5981
|
let i = 0;
|
|
3746
5982
|
while (i < lines.length) {
|
|
3747
5983
|
const line = lines[i] ?? "";
|
|
3748
|
-
if (isBlankLine(line) ||
|
|
5984
|
+
if (isBlankLine(line) || isCommentLine2(line)) {
|
|
3749
5985
|
items.push({ kind: "raw", text: line });
|
|
3750
5986
|
i += 1;
|
|
3751
5987
|
continue;
|
|
@@ -3774,7 +6010,7 @@ function parsePropertiesEntries(content, namespace) {
|
|
|
3774
6010
|
function unicodeEscape(code) {
|
|
3775
6011
|
return `\\u${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
3776
6012
|
}
|
|
3777
|
-
function
|
|
6013
|
+
function escapeChar4(char, isFirst, isKey) {
|
|
3778
6014
|
switch (char) {
|
|
3779
6015
|
case "\\":
|
|
3780
6016
|
return "\\\\";
|
|
@@ -3800,7 +6036,7 @@ function escapeString(input, isKey) {
|
|
|
3800
6036
|
for (let i = 0; i < input.length; i += 1) {
|
|
3801
6037
|
const char = input[i];
|
|
3802
6038
|
if (char !== void 0) {
|
|
3803
|
-
out +=
|
|
6039
|
+
out += escapeChar4(char, i === 0, isKey);
|
|
3804
6040
|
}
|
|
3805
6041
|
}
|
|
3806
6042
|
return out;
|
|
@@ -3922,20 +6158,354 @@ function createVueI18nJsonAdapter(fs = nodeAdapterFs) {
|
|
|
3922
6158
|
})
|
|
3923
6159
|
});
|
|
3924
6160
|
}
|
|
6161
|
+
var PLURAL_CATEGORIES2 = [
|
|
6162
|
+
"zero",
|
|
6163
|
+
"one",
|
|
6164
|
+
"two",
|
|
6165
|
+
"few",
|
|
6166
|
+
"many",
|
|
6167
|
+
"other"
|
|
6168
|
+
];
|
|
6169
|
+
var PLURAL_CATEGORY_SET2 = new Set(PLURAL_CATEGORIES2);
|
|
6170
|
+
function isRecord3(value) {
|
|
6171
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6172
|
+
}
|
|
6173
|
+
function parseXcstringsDocument(content, filePath) {
|
|
6174
|
+
let parsed;
|
|
6175
|
+
try {
|
|
6176
|
+
parsed = JSON.parse(content);
|
|
6177
|
+
} catch {
|
|
6178
|
+
throw new AdapterError("INVALID_JSON", `${filePath} is not valid JSON.`);
|
|
6179
|
+
}
|
|
6180
|
+
if (!isRecord3(parsed)) {
|
|
6181
|
+
throw new AdapterError(
|
|
6182
|
+
"INVALID_STRUCTURE",
|
|
6183
|
+
`${filePath}: the top level of an .xcstrings catalogue must be a JSON object.`
|
|
6184
|
+
);
|
|
6185
|
+
}
|
|
6186
|
+
const sourceLanguage = parsed.sourceLanguage;
|
|
6187
|
+
if (typeof sourceLanguage !== "string" || sourceLanguage.length === 0) {
|
|
6188
|
+
throw new AdapterError(
|
|
6189
|
+
"INVALID_STRUCTURE",
|
|
6190
|
+
`${filePath}: the catalogue has no "sourceLanguage" string field.`
|
|
6191
|
+
);
|
|
6192
|
+
}
|
|
6193
|
+
const strings = parsed.strings;
|
|
6194
|
+
if (!isRecord3(strings)) {
|
|
6195
|
+
throw new AdapterError(
|
|
6196
|
+
"INVALID_STRUCTURE",
|
|
6197
|
+
`${filePath}: the catalogue has no "strings" object field.`
|
|
6198
|
+
);
|
|
6199
|
+
}
|
|
6200
|
+
return { raw: parsed, sourceLanguage, strings };
|
|
6201
|
+
}
|
|
6202
|
+
function entryObjectOf(rawEntry, filePath, key) {
|
|
6203
|
+
if (!isRecord3(rawEntry)) {
|
|
6204
|
+
throw new AdapterError(
|
|
6205
|
+
"INVALID_STRUCTURE",
|
|
6206
|
+
`${filePath}: the entry "${key}" must be a JSON object.`
|
|
6207
|
+
);
|
|
6208
|
+
}
|
|
6209
|
+
return rawEntry;
|
|
6210
|
+
}
|
|
6211
|
+
function localizationsOf(entry, filePath, key) {
|
|
6212
|
+
const localizations = entry.localizations;
|
|
6213
|
+
if (localizations === void 0) {
|
|
6214
|
+
return {};
|
|
6215
|
+
}
|
|
6216
|
+
if (!isRecord3(localizations)) {
|
|
6217
|
+
throw new AdapterError(
|
|
6218
|
+
"INVALID_STRUCTURE",
|
|
6219
|
+
`${filePath}: the entry "${key}" has a "localizations" field that is not a JSON object.`
|
|
6220
|
+
);
|
|
6221
|
+
}
|
|
6222
|
+
return localizations;
|
|
6223
|
+
}
|
|
6224
|
+
function shouldTranslate(entry) {
|
|
6225
|
+
return entry.shouldTranslate !== false;
|
|
6226
|
+
}
|
|
6227
|
+
function stringUnitValueOf(stringUnit, filePath, where) {
|
|
6228
|
+
if (!isRecord3(stringUnit) || typeof stringUnit.value !== "string") {
|
|
6229
|
+
throw new AdapterError(
|
|
6230
|
+
"INVALID_STRUCTURE",
|
|
6231
|
+
`${filePath}: ${where} has a "stringUnit" without a string "value" field.`
|
|
6232
|
+
);
|
|
6233
|
+
}
|
|
6234
|
+
return stringUnit.value;
|
|
6235
|
+
}
|
|
6236
|
+
function pluralVariationsOf(loc) {
|
|
6237
|
+
const variations = loc.variations;
|
|
6238
|
+
if (!isRecord3(variations)) {
|
|
6239
|
+
return void 0;
|
|
6240
|
+
}
|
|
6241
|
+
return variations.plural;
|
|
6242
|
+
}
|
|
6243
|
+
function localizationValueOf(loc, filePath, key, locale) {
|
|
6244
|
+
if (!isRecord3(loc)) {
|
|
6245
|
+
throw new AdapterError(
|
|
6246
|
+
"INVALID_STRUCTURE",
|
|
6247
|
+
`${filePath}: the entry "${key}"'s "${locale}" localization must be a JSON object.`
|
|
6248
|
+
);
|
|
6249
|
+
}
|
|
6250
|
+
const plural = pluralVariationsOf(loc);
|
|
6251
|
+
if (plural !== void 0) {
|
|
6252
|
+
if (!isRecord3(plural)) {
|
|
6253
|
+
throw new AdapterError(
|
|
6254
|
+
"INVALID_STRUCTURE",
|
|
6255
|
+
`${filePath}: the entry "${key}"'s "${locale}" localization has a "variations.plural" field that is not a JSON object.`
|
|
6256
|
+
);
|
|
6257
|
+
}
|
|
6258
|
+
const categories = /* @__PURE__ */ new Map();
|
|
6259
|
+
for (const [category, variation] of Object.entries(plural)) {
|
|
6260
|
+
if (!PLURAL_CATEGORY_SET2.has(category)) {
|
|
6261
|
+
throw new AdapterError(
|
|
6262
|
+
"INVALID_STRUCTURE",
|
|
6263
|
+
`${filePath}: the entry "${key}"'s "${locale}" localization has an unsupported plural category "${category}"; expected one of ${PLURAL_CATEGORIES2.join(", ")}.`
|
|
6264
|
+
);
|
|
6265
|
+
}
|
|
6266
|
+
const value = stringUnitValueOf(
|
|
6267
|
+
isRecord3(variation) ? variation.stringUnit : void 0,
|
|
6268
|
+
filePath,
|
|
6269
|
+
`the entry "${key}"'s "${locale}" "${category}" plural category`
|
|
6270
|
+
);
|
|
6271
|
+
categories.set(category, value);
|
|
6272
|
+
}
|
|
6273
|
+
if (categories.size === 0) {
|
|
6274
|
+
throw new AdapterError(
|
|
6275
|
+
"INVALID_STRUCTURE",
|
|
6276
|
+
`${filePath}: the entry "${key}"'s "${locale}" localization has a "variations.plural" object with no plural categories.`
|
|
6277
|
+
);
|
|
6278
|
+
}
|
|
6279
|
+
return { kind: "plural", categories };
|
|
6280
|
+
}
|
|
6281
|
+
if (loc.stringUnit !== void 0) {
|
|
6282
|
+
return {
|
|
6283
|
+
kind: "plain",
|
|
6284
|
+
value: stringUnitValueOf(
|
|
6285
|
+
loc.stringUnit,
|
|
6286
|
+
filePath,
|
|
6287
|
+
`the entry "${key}"'s "${locale}" localization`
|
|
6288
|
+
)
|
|
6289
|
+
};
|
|
6290
|
+
}
|
|
6291
|
+
throw new AdapterError(
|
|
6292
|
+
"INVALID_STRUCTURE",
|
|
6293
|
+
`${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.`
|
|
6294
|
+
);
|
|
6295
|
+
}
|
|
6296
|
+
function plainEntry(key, namespace, value, extractPlaceholders) {
|
|
6297
|
+
return { key, namespace, value, placeholders: extractPlaceholders(value), isPlural: false };
|
|
6298
|
+
}
|
|
6299
|
+
function pluralEntry(baseKey, category, namespace, value, extractPlaceholders) {
|
|
6300
|
+
const key = makePluralKey(baseKey, category);
|
|
6301
|
+
return [key, { key, namespace, value, placeholders: extractPlaceholders(value), isPlural: true }];
|
|
6302
|
+
}
|
|
6303
|
+
function addPresentEntry(out, loc, key, namespace, locale, filePath, extractPlaceholders) {
|
|
6304
|
+
const content = localizationValueOf(loc, filePath, key, locale);
|
|
6305
|
+
if (content.kind === "plain") {
|
|
6306
|
+
out.set(key, plainEntry(key, namespace, content.value, extractPlaceholders));
|
|
6307
|
+
return;
|
|
6308
|
+
}
|
|
6309
|
+
for (const [category, value] of content.categories) {
|
|
6310
|
+
const [pluralKey, entry] = pluralEntry(key, category, namespace, value, extractPlaceholders);
|
|
6311
|
+
out.set(pluralKey, entry);
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
function readXcstringsLocale(doc, locale, namespace, filePath, extractPlaceholders) {
|
|
6315
|
+
const entries = /* @__PURE__ */ new Map();
|
|
6316
|
+
const excludedLeafPaths = [];
|
|
6317
|
+
for (const [key, rawEntry] of Object.entries(doc.strings)) {
|
|
6318
|
+
const entryObject = entryObjectOf(rawEntry, filePath, key);
|
|
6319
|
+
if (!shouldTranslate(entryObject)) {
|
|
6320
|
+
excludedLeafPaths.push(key);
|
|
6321
|
+
continue;
|
|
6322
|
+
}
|
|
6323
|
+
const localizations = localizationsOf(entryObject, filePath, key);
|
|
6324
|
+
const loc = localizations[locale];
|
|
6325
|
+
if (loc === void 0) {
|
|
6326
|
+
if (locale === doc.sourceLanguage) {
|
|
6327
|
+
entries.set(key, plainEntry(key, namespace, key, extractPlaceholders));
|
|
6328
|
+
}
|
|
6329
|
+
continue;
|
|
6330
|
+
}
|
|
6331
|
+
addPresentEntry(entries, loc, key, namespace, locale, filePath, extractPlaceholders);
|
|
6332
|
+
}
|
|
6333
|
+
return { entries, excludedLeafPaths };
|
|
6334
|
+
}
|
|
6335
|
+
function groupByKey(entries) {
|
|
6336
|
+
const singular = /* @__PURE__ */ new Map();
|
|
6337
|
+
const plural = /* @__PURE__ */ new Map();
|
|
6338
|
+
for (const [key, entry] of entries) {
|
|
6339
|
+
const baseKey = entry.isPlural ? pluralBaseKey(key) : void 0;
|
|
6340
|
+
const category = entry.isPlural ? pluralCategoryOf(key) : void 0;
|
|
6341
|
+
if (baseKey !== void 0 && category !== void 0) {
|
|
6342
|
+
const group = plural.get(baseKey) ?? /* @__PURE__ */ new Map();
|
|
6343
|
+
group.set(category, entry.value);
|
|
6344
|
+
plural.set(baseKey, group);
|
|
6345
|
+
} else {
|
|
6346
|
+
singular.set(key, entry.value);
|
|
6347
|
+
}
|
|
6348
|
+
}
|
|
6349
|
+
return { singular, plural };
|
|
6350
|
+
}
|
|
6351
|
+
function currentPlainValue(loc) {
|
|
6352
|
+
if (typeof loc !== "object" || loc === null) {
|
|
6353
|
+
return void 0;
|
|
6354
|
+
}
|
|
6355
|
+
const stringUnit = loc.stringUnit;
|
|
6356
|
+
if (typeof stringUnit !== "object" || stringUnit === null) {
|
|
6357
|
+
return void 0;
|
|
6358
|
+
}
|
|
6359
|
+
const value = stringUnit.value;
|
|
6360
|
+
return typeof value === "string" ? value : void 0;
|
|
6361
|
+
}
|
|
6362
|
+
function currentPluralValues(loc) {
|
|
6363
|
+
if (typeof loc !== "object" || loc === null) {
|
|
6364
|
+
return void 0;
|
|
6365
|
+
}
|
|
6366
|
+
const variations = loc.variations;
|
|
6367
|
+
if (typeof variations !== "object" || variations === null) {
|
|
6368
|
+
return void 0;
|
|
6369
|
+
}
|
|
6370
|
+
const plural = variations.plural;
|
|
6371
|
+
if (typeof plural !== "object" || plural === null) {
|
|
6372
|
+
return void 0;
|
|
6373
|
+
}
|
|
6374
|
+
const out = /* @__PURE__ */ new Map();
|
|
6375
|
+
for (const [category, variation] of Object.entries(plural)) {
|
|
6376
|
+
const value = currentPlainValue(variation);
|
|
6377
|
+
if (value === void 0) {
|
|
6378
|
+
return void 0;
|
|
6379
|
+
}
|
|
6380
|
+
out.set(category, value);
|
|
6381
|
+
}
|
|
6382
|
+
return out;
|
|
6383
|
+
}
|
|
6384
|
+
function pluralValuesEqual(current, wanted) {
|
|
6385
|
+
if (current === void 0 || current.size !== wanted.size) {
|
|
6386
|
+
return false;
|
|
6387
|
+
}
|
|
6388
|
+
for (const [category, value] of wanted) {
|
|
6389
|
+
if (current.get(category) !== value) {
|
|
6390
|
+
return false;
|
|
6391
|
+
}
|
|
6392
|
+
}
|
|
6393
|
+
return true;
|
|
6394
|
+
}
|
|
6395
|
+
function buildPluralLocalization(categories) {
|
|
6396
|
+
const plural = {};
|
|
6397
|
+
for (const [category, value] of categories) {
|
|
6398
|
+
plural[category] = { stringUnit: { state: "translated", value } };
|
|
6399
|
+
}
|
|
6400
|
+
return { variations: { plural } };
|
|
6401
|
+
}
|
|
6402
|
+
function buildPlainLocalization(value) {
|
|
6403
|
+
return { stringUnit: { state: "translated", value } };
|
|
6404
|
+
}
|
|
6405
|
+
function deleteLocalization(entry, locale) {
|
|
6406
|
+
const localizations = entry.localizations;
|
|
6407
|
+
if (typeof localizations !== "object" || localizations === null) {
|
|
6408
|
+
return;
|
|
6409
|
+
}
|
|
6410
|
+
delete localizations[locale];
|
|
6411
|
+
}
|
|
6412
|
+
function writeLocalization(entry, locale, filePath, key, content) {
|
|
6413
|
+
const localizations = localizationsOf(entry, filePath, key);
|
|
6414
|
+
if (entry.localizations === void 0) {
|
|
6415
|
+
entry.localizations = localizations;
|
|
6416
|
+
}
|
|
6417
|
+
localizations[locale] = content;
|
|
6418
|
+
}
|
|
6419
|
+
function patchKey(entry, key, locale, filePath, touched) {
|
|
6420
|
+
const loc = localizationsOf(entry, filePath, key)[locale];
|
|
6421
|
+
const singularValue = touched.singular.get(key);
|
|
6422
|
+
if (singularValue !== void 0) {
|
|
6423
|
+
const isPlural = currentPluralValues(loc) !== void 0;
|
|
6424
|
+
const currentValue = isPlural ? void 0 : currentPlainValue(loc) ?? "";
|
|
6425
|
+
if (currentValue === singularValue) {
|
|
6426
|
+
return;
|
|
6427
|
+
}
|
|
6428
|
+
writeLocalization(entry, locale, filePath, key, buildPlainLocalization(singularValue));
|
|
6429
|
+
return;
|
|
6430
|
+
}
|
|
6431
|
+
const pluralCategories = touched.plural.get(key);
|
|
6432
|
+
if (pluralCategories !== void 0) {
|
|
6433
|
+
if (pluralValuesEqual(currentPluralValues(loc), pluralCategories)) {
|
|
6434
|
+
return;
|
|
6435
|
+
}
|
|
6436
|
+
writeLocalization(entry, locale, filePath, key, buildPluralLocalization(pluralCategories));
|
|
6437
|
+
return;
|
|
6438
|
+
}
|
|
6439
|
+
deleteLocalization(entry, locale);
|
|
6440
|
+
}
|
|
6441
|
+
function patchXcstringsDocument(doc, locale, resource, filePath) {
|
|
6442
|
+
const touched = groupByKey(resource.entries);
|
|
6443
|
+
for (const [key, rawEntry] of Object.entries(doc.strings)) {
|
|
6444
|
+
const entry = entryObjectOf(rawEntry, filePath, key);
|
|
6445
|
+
if (!shouldTranslate(entry)) {
|
|
6446
|
+
continue;
|
|
6447
|
+
}
|
|
6448
|
+
patchKey(entry, key, locale, filePath, touched);
|
|
6449
|
+
}
|
|
6450
|
+
}
|
|
6451
|
+
var EXTENSIONS = [".xcstrings"];
|
|
6452
|
+
function createAppleXcstringsAdapter(fs = nodeAdapterFs) {
|
|
6453
|
+
return {
|
|
6454
|
+
format: "apple-xcstrings",
|
|
6455
|
+
canHandle: buildCanHandle(EXTENSIONS),
|
|
6456
|
+
extractPlaceholders: extractAppleStringsPlaceholders,
|
|
6457
|
+
validateMessage: () => true,
|
|
6458
|
+
async read(filePath, locale) {
|
|
6459
|
+
const content = await readFileContent(fs, filePath);
|
|
6460
|
+
const doc = parseXcstringsDocument(content, filePath);
|
|
6461
|
+
const namespace = namespaceOf(filePath);
|
|
6462
|
+
const { entries, excludedLeafPaths } = readXcstringsLocale(
|
|
6463
|
+
doc,
|
|
6464
|
+
locale,
|
|
6465
|
+
namespace,
|
|
6466
|
+
filePath,
|
|
6467
|
+
extractAppleStringsPlaceholders
|
|
6468
|
+
);
|
|
6469
|
+
return {
|
|
6470
|
+
resource: { locale, namespace, format: "apple-xcstrings", entries },
|
|
6471
|
+
invalidIcuKeys: [],
|
|
6472
|
+
excludedLeafPaths
|
|
6473
|
+
};
|
|
6474
|
+
},
|
|
6475
|
+
async write(resource, filePath) {
|
|
6476
|
+
let content;
|
|
6477
|
+
try {
|
|
6478
|
+
content = await readFileContent(fs, filePath);
|
|
6479
|
+
} catch (error) {
|
|
6480
|
+
if (isEnoent(error)) {
|
|
6481
|
+
throw new AdapterError(
|
|
6482
|
+
"INVALID_STRUCTURE",
|
|
6483
|
+
`${filePath}: the catalogue does not exist. verbatra does not create a new .xcstrings catalogue; create it in Xcode first.`
|
|
6484
|
+
);
|
|
6485
|
+
}
|
|
6486
|
+
throw error;
|
|
6487
|
+
}
|
|
6488
|
+
const doc = parseXcstringsDocument(content, filePath);
|
|
6489
|
+
patchXcstringsDocument(doc, resource.locale, resource, filePath);
|
|
6490
|
+
await fs.writeFileAtomic(filePath, `${JSON.stringify(doc.raw, null, 2)}
|
|
6491
|
+
`);
|
|
6492
|
+
}
|
|
6493
|
+
};
|
|
6494
|
+
}
|
|
3925
6495
|
var XLIFF_PATTERN = /<(?:x|g|bx|ex|ph|it|mrk)\b[^>]*>|\{[^{}]+\}/g;
|
|
3926
6496
|
function extractXliffPlaceholders(value) {
|
|
3927
6497
|
return scanTokens(value, XLIFF_PATTERN);
|
|
3928
6498
|
}
|
|
3929
|
-
var
|
|
3930
|
-
var
|
|
3931
|
-
function
|
|
3932
|
-
return node.nodeType ===
|
|
6499
|
+
var ELEMENT_NODE3 = 1;
|
|
6500
|
+
var TEXT_NODE2 = 3;
|
|
6501
|
+
function isElement3(node) {
|
|
6502
|
+
return node.nodeType === ELEMENT_NODE3;
|
|
3933
6503
|
}
|
|
3934
|
-
function
|
|
3935
|
-
return Array.from(parent.childNodes).filter(
|
|
6504
|
+
function elementChildren3(parent) {
|
|
6505
|
+
return Array.from(parent.childNodes).filter(isElement3);
|
|
3936
6506
|
}
|
|
3937
6507
|
function childByName(parent, name) {
|
|
3938
|
-
return
|
|
6508
|
+
return elementChildren3(parent).find((el) => el.localName === name) ?? null;
|
|
3939
6509
|
}
|
|
3940
6510
|
function collectByTag(root, name) {
|
|
3941
6511
|
return Array.from(root.getElementsByTagName(name));
|
|
@@ -3943,21 +6513,21 @@ function collectByTag(root, name) {
|
|
|
3943
6513
|
function unitKey(element, index) {
|
|
3944
6514
|
return element.getAttribute("id") ?? element.getAttribute("resname") ?? `unit-${index}`;
|
|
3945
6515
|
}
|
|
3946
|
-
function
|
|
6516
|
+
function onFatal3(level) {
|
|
3947
6517
|
if (level === "fatalError") {
|
|
3948
6518
|
throw new Error("malformed XML");
|
|
3949
6519
|
}
|
|
3950
6520
|
}
|
|
3951
|
-
function
|
|
6521
|
+
function assertNoDoctype2(content) {
|
|
3952
6522
|
if (/<!DOCTYPE/i.test(content) || /<!ENTITY/i.test(content)) {
|
|
3953
6523
|
throw new AdapterError("INVALID_XML", "XLIFF with a DTD or entity declaration is rejected.");
|
|
3954
6524
|
}
|
|
3955
6525
|
}
|
|
3956
6526
|
function parseXml(content) {
|
|
3957
|
-
|
|
6527
|
+
assertNoDoctype2(content);
|
|
3958
6528
|
let doc;
|
|
3959
6529
|
try {
|
|
3960
|
-
doc = new DOMParser({ onError:
|
|
6530
|
+
doc = new DOMParser({ onError: onFatal3 }).parseFromString(content, "text/xml");
|
|
3961
6531
|
} catch {
|
|
3962
6532
|
throw new AdapterError("INVALID_XML", "The file is not valid XML.");
|
|
3963
6533
|
}
|
|
@@ -4004,7 +6574,7 @@ function walkXliff20(root) {
|
|
|
4004
6574
|
collectByTag(root, "unit").forEach((unit, index) => {
|
|
4005
6575
|
const baseKey = unitKey(unit, index);
|
|
4006
6576
|
const description = unitDescription(unit);
|
|
4007
|
-
const segments =
|
|
6577
|
+
const segments = elementChildren3(unit).filter((el) => el.localName === "segment");
|
|
4008
6578
|
segments.forEach((segment, segIndex) => {
|
|
4009
6579
|
const source = childByName(segment, "source");
|
|
4010
6580
|
if (source !== null) {
|
|
@@ -4060,12 +6630,19 @@ function parseXliffEntries(content, namespace) {
|
|
|
4060
6630
|
}
|
|
4061
6631
|
return out;
|
|
4062
6632
|
}
|
|
4063
|
-
|
|
6633
|
+
function destinationReadErrorMessage(error) {
|
|
6634
|
+
if (isEnoent(error)) {
|
|
6635
|
+
return "The destination XLIFF file does not exist.";
|
|
6636
|
+
}
|
|
6637
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
6638
|
+
return `The destination XLIFF file could not be read: ${reason}`;
|
|
6639
|
+
}
|
|
6640
|
+
async function readDestination3(filePath, fs) {
|
|
4064
6641
|
let outcome;
|
|
4065
6642
|
try {
|
|
4066
6643
|
outcome = await readBoundedFile(fs, filePath);
|
|
4067
|
-
} catch {
|
|
4068
|
-
throw new AdapterError("INVALID_STRUCTURE",
|
|
6644
|
+
} catch (error) {
|
|
6645
|
+
throw new AdapterError("INVALID_STRUCTURE", destinationReadErrorMessage(error));
|
|
4069
6646
|
}
|
|
4070
6647
|
return outcomeToContent(outcome, "The destination path is not a regular file.");
|
|
4071
6648
|
}
|
|
@@ -4089,14 +6666,31 @@ function isAllowedInlineElement(element) {
|
|
|
4089
6666
|
return hasAllowedNamespace && XLIFF_INLINE_ELEMENTS.has(element.localName ?? "");
|
|
4090
6667
|
}
|
|
4091
6668
|
function isAllowedFragmentNode(node) {
|
|
4092
|
-
if (node.nodeType ===
|
|
6669
|
+
if (node.nodeType === TEXT_NODE2) {
|
|
4093
6670
|
return true;
|
|
4094
6671
|
}
|
|
4095
|
-
return
|
|
6672
|
+
return isElement3(node) && isAllowedInlineElement(node);
|
|
4096
6673
|
}
|
|
4097
6674
|
function allDescendantNodes(node) {
|
|
4098
|
-
const
|
|
4099
|
-
|
|
6675
|
+
const result = [];
|
|
6676
|
+
const stack = [{ node, depth: 1 }];
|
|
6677
|
+
while (stack.length > 0) {
|
|
6678
|
+
const top = stack.pop();
|
|
6679
|
+
if (top === void 0) {
|
|
6680
|
+
break;
|
|
6681
|
+
}
|
|
6682
|
+
if (top.depth > MAX_DEPTH) {
|
|
6683
|
+
throw new AdapterError(
|
|
6684
|
+
"MAX_DEPTH_EXCEEDED",
|
|
6685
|
+
"The translated value nests inline elements too deeply."
|
|
6686
|
+
);
|
|
6687
|
+
}
|
|
6688
|
+
for (const child of Array.from(top.node.childNodes)) {
|
|
6689
|
+
result.push(child);
|
|
6690
|
+
stack.push({ node: child, depth: top.depth + 1 });
|
|
6691
|
+
}
|
|
6692
|
+
}
|
|
6693
|
+
return result;
|
|
4100
6694
|
}
|
|
4101
6695
|
function hasDisallowedNode(root) {
|
|
4102
6696
|
return allDescendantNodes(root).some((node) => !isAllowedFragmentNode(node));
|
|
@@ -4122,7 +6716,7 @@ function sanitizeInlineAttributes(root) {
|
|
|
4122
6716
|
}
|
|
4123
6717
|
}
|
|
4124
6718
|
function fragmentNodes(parser, value) {
|
|
4125
|
-
|
|
6719
|
+
assertNoDoctype2(value);
|
|
4126
6720
|
try {
|
|
4127
6721
|
const root = parser.parseFromString(`<wrapper>${value}</wrapper>`, "text/xml").documentElement;
|
|
4128
6722
|
if (root === null || hasDisallowedNode(root)) {
|
|
@@ -4130,7 +6724,10 @@ function fragmentNodes(parser, value) {
|
|
|
4130
6724
|
}
|
|
4131
6725
|
sanitizeInlineAttributes(root);
|
|
4132
6726
|
return Array.from(root.childNodes);
|
|
4133
|
-
} catch {
|
|
6727
|
+
} catch (error) {
|
|
6728
|
+
if (error instanceof AdapterError) {
|
|
6729
|
+
throw error;
|
|
6730
|
+
}
|
|
4134
6731
|
return null;
|
|
4135
6732
|
}
|
|
4136
6733
|
}
|
|
@@ -4148,8 +6745,8 @@ function setTargetValue(doc, parser, element, value) {
|
|
|
4148
6745
|
}
|
|
4149
6746
|
}
|
|
4150
6747
|
async function serializeXliffEntries(entries, filePath, fs) {
|
|
4151
|
-
const { doc, root } = parseXml(await
|
|
4152
|
-
const parser = new DOMParser({ onError:
|
|
6748
|
+
const { doc, root } = parseXml(await readDestination3(filePath, fs));
|
|
6749
|
+
const parser = new DOMParser({ onError: onFatal3 });
|
|
4153
6750
|
for (const unit of walkUnits(root)) {
|
|
4154
6751
|
const entry = entries.get(unit.key);
|
|
4155
6752
|
if (entry !== void 0) {
|
|
@@ -4222,7 +6819,7 @@ function createYamlAdapter(fs = nodeAdapterFs) {
|
|
|
4222
6819
|
});
|
|
4223
6820
|
}
|
|
4224
6821
|
function createDefaultRegistry(fs = nodeAdapterFs) {
|
|
4225
|
-
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));
|
|
6822
|
+
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));
|
|
4226
6823
|
}
|
|
4227
6824
|
function enoent(path) {
|
|
4228
6825
|
return Object.assign(new Error(`ENOENT: no such file or directory, open '${path}'`), {
|
|
@@ -4609,7 +7206,7 @@ async function editEntry(input, deps = {}) {
|
|
|
4609
7206
|
`The key "${input.key}" was not found in the source resource.`
|
|
4610
7207
|
);
|
|
4611
7208
|
}
|
|
4612
|
-
return withLocaleWriteLock(cwd, locale, fs, async () => {
|
|
7209
|
+
return withLocaleWriteLock(cwd, writeLockKeyFor(config.format, locale), fs, async () => {
|
|
4613
7210
|
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
4614
7211
|
const gate = gateCandidateValue(sourceEntry, input.value, adapter);
|
|
4615
7212
|
if (!gate.accepted) {
|
|
@@ -4754,6 +7351,41 @@ function diffLocaleSnapshots(previous, current) {
|
|
|
4754
7351
|
return { added, changed, removed };
|
|
4755
7352
|
}
|
|
4756
7353
|
|
|
7354
|
+
// src/flow/locale-values.ts
|
|
7355
|
+
function mergeValues(source, target) {
|
|
7356
|
+
const keys = /* @__PURE__ */ new Set([...source.entries.keys(), ...target.entries.keys()]);
|
|
7357
|
+
const values = {};
|
|
7358
|
+
for (const key of keys) {
|
|
7359
|
+
const sourceEntry = source.entries.get(key);
|
|
7360
|
+
const targetEntry = target.entries.get(key);
|
|
7361
|
+
values[key] = {
|
|
7362
|
+
...sourceEntry !== void 0 ? { source: sourceEntry.value } : {},
|
|
7363
|
+
...targetEntry !== void 0 ? { target: targetEntry.value } : {}
|
|
7364
|
+
};
|
|
7365
|
+
}
|
|
7366
|
+
return values;
|
|
7367
|
+
}
|
|
7368
|
+
async function localeValues(input, deps = {}) {
|
|
7369
|
+
const config = input.config;
|
|
7370
|
+
const cwd = input.cwd ?? process.cwd();
|
|
7371
|
+
const fs = deps.fs ?? defaultFs;
|
|
7372
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry, deps.fs);
|
|
7373
|
+
const resolver = createLocalePathResolver(cwd, config);
|
|
7374
|
+
const source = await readSourceResource(config, resolver, fs, adapter);
|
|
7375
|
+
return Promise.all(
|
|
7376
|
+
selectLocales(config, input.locales).map(async (locale) => {
|
|
7377
|
+
const target = await readTargetResource({
|
|
7378
|
+
resolver,
|
|
7379
|
+
format: config.format,
|
|
7380
|
+
locale,
|
|
7381
|
+
adapter,
|
|
7382
|
+
fs
|
|
7383
|
+
});
|
|
7384
|
+
return { locale, values: mergeValues(source.resource, target) };
|
|
7385
|
+
})
|
|
7386
|
+
);
|
|
7387
|
+
}
|
|
7388
|
+
|
|
4757
7389
|
// src/flow/lock-state.ts
|
|
4758
7390
|
function toLockLocaleState(locale, keyCount, diff2) {
|
|
4759
7391
|
return {
|
|
@@ -4833,7 +7465,7 @@ async function retranslateEntry(input, deps = {}) {
|
|
|
4833
7465
|
);
|
|
4834
7466
|
}
|
|
4835
7467
|
const provider = selectProvider(config.provider, deps.createProvider);
|
|
4836
|
-
return withLocaleWriteLock(cwd, locale, fs, async () => {
|
|
7468
|
+
return withLocaleWriteLock(cwd, writeLockKeyFor(config.format, locale), fs, async () => {
|
|
4837
7469
|
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
4838
7470
|
const result = await provider.translateBatch(
|
|
4839
7471
|
buildTranslateRequest(
|
|
@@ -5885,7 +8517,7 @@ async function runLiveLocale(context, targetLocale) {
|
|
|
5885
8517
|
};
|
|
5886
8518
|
return withLocaleWriteLock(
|
|
5887
8519
|
context.cwd,
|
|
5888
|
-
targetLocale,
|
|
8520
|
+
writeLockKeyFor(context.config.format, targetLocale),
|
|
5889
8521
|
context.fs,
|
|
5890
8522
|
async () => {
|
|
5891
8523
|
const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
|
|
@@ -6000,7 +8632,7 @@ function resolveRunConcurrency(value, dryRun, config) {
|
|
|
6000
8632
|
}
|
|
6001
8633
|
return concurrency;
|
|
6002
8634
|
}
|
|
6003
|
-
async function
|
|
8635
|
+
async function translate3(input, deps = {}) {
|
|
6004
8636
|
const config = input.config;
|
|
6005
8637
|
const cwd = input.cwd ?? process.cwd();
|
|
6006
8638
|
const dryRun = input.dryRun ?? false;
|
|
@@ -6291,14 +8923,15 @@ function applyColumnGeometry(sheet) {
|
|
|
6291
8923
|
function writeRow(sheet, sheetRow) {
|
|
6292
8924
|
const row = sheet.addRow([]);
|
|
6293
8925
|
row.getCell(COLUMN.key).value = sheetRow.key;
|
|
6294
|
-
row.getCell(COLUMN.source).value = sheetRow.source;
|
|
6295
|
-
row.getCell(COLUMN.current).value = sheetRow.currentTarget;
|
|
8926
|
+
row.getCell(COLUMN.source).value = escapeFormulaLead(sheetRow.source);
|
|
8927
|
+
row.getCell(COLUMN.current).value = escapeFormulaLead(sheetRow.currentTarget);
|
|
6296
8928
|
row.getCell(COLUMN.status).value = sheetRow.status;
|
|
6297
8929
|
const translationCell = row.getCell(COLUMN.translation);
|
|
6298
8930
|
translationCell.numFmt = TEXT_NUMBER_FORMAT;
|
|
6299
|
-
|
|
8931
|
+
const translation = escapeFormulaLead(sheetRow.translation);
|
|
8932
|
+
translationCell.value = translation === "" ? null : translation;
|
|
6300
8933
|
row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
|
|
6301
|
-
row.getCell(COLUMN.context).value = sheetRow.context;
|
|
8934
|
+
row.getCell(COLUMN.context).value = escapeFormulaLead(sheetRow.context);
|
|
6302
8935
|
row.getCell(COLUMN.reviewStatus).value = sheetRow.reviewStatus;
|
|
6303
8936
|
row.getCell(COLUMN.reviewReasons).value = sheetRow.reviewReasons;
|
|
6304
8937
|
for (let column = COLUMN.key; column <= COLUMN.reviewReasons; column += 1) {
|
|
@@ -6609,7 +9242,7 @@ function readDelimited(input, options = {}) {
|
|
|
6609
9242
|
duplicateKeys: into.duplicates
|
|
6610
9243
|
};
|
|
6611
9244
|
}
|
|
6612
|
-
function
|
|
9245
|
+
function assertNoDoctype3(name, xml) {
|
|
6613
9246
|
if (/<!DOCTYPE/i.test(xml) || /<!ENTITY/i.test(xml)) {
|
|
6614
9247
|
throw new ExchangeError(
|
|
6615
9248
|
"WORKBOOK_INVALID",
|
|
@@ -6682,7 +9315,7 @@ async function guardWorkbookBytes(bytes, limits) {
|
|
|
6682
9315
|
const remaining = limits.maxDecompressedBytes - actualTotal;
|
|
6683
9316
|
const { raw, content } = await streamEntryBounded(file, remaining);
|
|
6684
9317
|
actualTotal += raw;
|
|
6685
|
-
|
|
9318
|
+
assertNoDoctype3(file.name, content);
|
|
6686
9319
|
}
|
|
6687
9320
|
}
|
|
6688
9321
|
function cellString(cell) {
|
|
@@ -6698,19 +9331,31 @@ function cellString(cell) {
|
|
|
6698
9331
|
}
|
|
6699
9332
|
return typeof cell.text === "string" ? cell.text : "";
|
|
6700
9333
|
}
|
|
9334
|
+
var FORMULA_GUARDED_COLUMNS = /* @__PURE__ */ new Set([
|
|
9335
|
+
COLUMN.source,
|
|
9336
|
+
COLUMN.current,
|
|
9337
|
+
COLUMN.translation,
|
|
9338
|
+
COLUMN.context
|
|
9339
|
+
]);
|
|
9340
|
+
var LEGACY_HEADER_COLUMN_COUNT = COLUMN.sourceHash;
|
|
6701
9341
|
function assertHeader(sheet) {
|
|
6702
9342
|
const header = sheet.getRow(HEADER_ROW);
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
9343
|
+
for (let column = 1; column <= LEGACY_HEADER_COLUMN_COUNT; column += 1) {
|
|
9344
|
+
const label = cellString(header.getCell(column));
|
|
9345
|
+
if (label !== HEADERS[column - 1]) {
|
|
9346
|
+
throw new ExchangeError(
|
|
9347
|
+
"WORKBOOK_INVALID",
|
|
9348
|
+
`The sheet "${sheet.name}" has an unexpected header label in column ${column} (expected "${HEADERS[column - 1]}").`
|
|
9349
|
+
);
|
|
9350
|
+
}
|
|
6710
9351
|
}
|
|
6711
9352
|
}
|
|
6712
9353
|
function sheetRowCells(row) {
|
|
6713
|
-
return HEADERS.map((_, index) =>
|
|
9354
|
+
return HEADERS.map((_, index) => {
|
|
9355
|
+
const column = index + 1;
|
|
9356
|
+
const value = cellString(row.getCell(column));
|
|
9357
|
+
return FORMULA_GUARDED_COLUMNS.has(column) ? unescapeFormulaLead(value) : value;
|
|
9358
|
+
});
|
|
6714
9359
|
}
|
|
6715
9360
|
function assertRowCellCap(row, sheetName, limits) {
|
|
6716
9361
|
if (row.cellCount > limits.maxCellsPerRow) {
|
|
@@ -6937,6 +9582,7 @@ async function exportWorkbook(input, deps = {}) {
|
|
|
6937
9582
|
await writeDelimitedFiles(fs, path, format, sheets);
|
|
6938
9583
|
} else {
|
|
6939
9584
|
const model = { sheets };
|
|
9585
|
+
await fs.mkdir?.(dirname(path));
|
|
6940
9586
|
await fs.writeBytes(path, await buildWorkbook(model));
|
|
6941
9587
|
}
|
|
6942
9588
|
return {
|
|
@@ -7097,7 +9743,12 @@ async function readDelimitedText(path, fs) {
|
|
|
7097
9743
|
async function collectDelimitedSources(path, config, fs, format) {
|
|
7098
9744
|
const single = await readDelimitedText(path, fs);
|
|
7099
9745
|
if (single !== void 0) {
|
|
7100
|
-
|
|
9746
|
+
const locale = basename(path, `.${format}`);
|
|
9747
|
+
return {
|
|
9748
|
+
sources: [{ locale, text: single }],
|
|
9749
|
+
staleLocales: [],
|
|
9750
|
+
expectedLocales: [locale]
|
|
9751
|
+
};
|
|
7101
9752
|
}
|
|
7102
9753
|
const exported = await readExportedLocales(fs, path, format);
|
|
7103
9754
|
const sources = [];
|
|
@@ -7119,7 +9770,7 @@ async function collectDelimitedSources(path, config, fs, format) {
|
|
|
7119
9770
|
`No ${format} file was found at ${path}, and it holds no <locale>.${format} file for any configured target locale.`
|
|
7120
9771
|
);
|
|
7121
9772
|
}
|
|
7122
|
-
return { sources, staleLocales };
|
|
9773
|
+
return { sources, staleLocales, expectedLocales: config.targetLocales };
|
|
7123
9774
|
}
|
|
7124
9775
|
function parseDelimitedSources(sources, format) {
|
|
7125
9776
|
const sheets = [];
|
|
@@ -7139,10 +9790,15 @@ async function readImportData(path, config, fs, format) {
|
|
|
7139
9790
|
const handoff = await collectDelimitedSources(path, config, fs, format);
|
|
7140
9791
|
return {
|
|
7141
9792
|
data: parseDelimitedSources(handoff.sources, format),
|
|
7142
|
-
staleLocales: handoff.staleLocales
|
|
9793
|
+
staleLocales: handoff.staleLocales,
|
|
9794
|
+
expectedLocales: handoff.expectedLocales
|
|
7143
9795
|
};
|
|
7144
9796
|
}
|
|
7145
|
-
return {
|
|
9797
|
+
return {
|
|
9798
|
+
data: await readWorkbook(await readWorkbookBytes(path, fs)),
|
|
9799
|
+
staleLocales: [],
|
|
9800
|
+
expectedLocales: config.targetLocales
|
|
9801
|
+
};
|
|
7146
9802
|
} catch (error) {
|
|
7147
9803
|
if (error instanceof SdkError) {
|
|
7148
9804
|
throw error;
|
|
@@ -7207,11 +9863,11 @@ var StaleHandoffFileError = class extends Error {
|
|
|
7207
9863
|
this.name = "StaleHandoffFileError";
|
|
7208
9864
|
}
|
|
7209
9865
|
};
|
|
7210
|
-
function absentLocaleFailures(
|
|
9866
|
+
function absentLocaleFailures(expectedLocales, sheets, format, staleLocales) {
|
|
7211
9867
|
const present = new Set(sheets.map((sheet) => sheet.locale));
|
|
7212
9868
|
const stale = new Set(staleLocales);
|
|
7213
9869
|
const failures = [];
|
|
7214
|
-
for (const locale of
|
|
9870
|
+
for (const locale of expectedLocales) {
|
|
7215
9871
|
if (present.has(locale)) {
|
|
7216
9872
|
continue;
|
|
7217
9873
|
}
|
|
@@ -7220,7 +9876,7 @@ function absentLocaleFailures(config, sheets, format, staleLocales) {
|
|
|
7220
9876
|
}
|
|
7221
9877
|
return failures;
|
|
7222
9878
|
}
|
|
7223
|
-
function
|
|
9879
|
+
function lineOf2(reported) {
|
|
7224
9880
|
return reported.line === void 0 ? {} : { line: reported.line };
|
|
7225
9881
|
}
|
|
7226
9882
|
async function runSheet(ctx, sheet, lock) {
|
|
@@ -7245,8 +9901,8 @@ async function runSheet(ctx, sheet, lock) {
|
|
|
7245
9901
|
baseline,
|
|
7246
9902
|
adapter: ctx.adapter,
|
|
7247
9903
|
sourceInvalidIcuKeys: ctx.sourceInvalidIcuKeys,
|
|
7248
|
-
malformedRows: ctx.malformedRows.filter((problem) => problem.locale === sheet.locale).map((problem) => ({ row: problem.row, column: problem.column, ...
|
|
7249
|
-
duplicateKeys: ctx.duplicateKeys.filter((duplicate) => duplicate.locale === sheet.locale).map((duplicate) => ({ key: duplicate.key, row: duplicate.row, ...
|
|
9904
|
+
malformedRows: ctx.malformedRows.filter((problem) => problem.locale === sheet.locale).map((problem) => ({ row: problem.row, column: problem.column, ...lineOf2(problem) })),
|
|
9905
|
+
duplicateKeys: ctx.duplicateKeys.filter((duplicate) => duplicate.locale === sheet.locale).map((duplicate) => ({ key: duplicate.key, row: duplicate.row, ...lineOf2(duplicate) }))
|
|
7250
9906
|
});
|
|
7251
9907
|
if (ctx.dryRun) {
|
|
7252
9908
|
return { summary, lockEntries: {}, cacheAdditions: {} };
|
|
@@ -7281,7 +9937,7 @@ async function importWorkbook(input, deps = {}) {
|
|
|
7281
9937
|
const resolver = createLocalePathResolver(cwd, config);
|
|
7282
9938
|
const source = await readSourceResource(config, resolver, fs, adapter);
|
|
7283
9939
|
const format = input.format ?? DEFAULT_EXCHANGE_FORMAT;
|
|
7284
|
-
const { data, staleLocales } = await readImportData(
|
|
9940
|
+
const { data, staleLocales, expectedLocales } = await readImportData(
|
|
7285
9941
|
resolve(cwd, input.workbook),
|
|
7286
9942
|
config,
|
|
7287
9943
|
fs,
|
|
@@ -7309,15 +9965,20 @@ async function importWorkbook(input, deps = {}) {
|
|
|
7309
9965
|
if (dryRun) {
|
|
7310
9966
|
summary = (await runSheet(ctx, sheet, lock)).summary;
|
|
7311
9967
|
} else {
|
|
7312
|
-
summary = await withLocaleWriteLock(
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
9968
|
+
summary = await withLocaleWriteLock(
|
|
9969
|
+
cwd,
|
|
9970
|
+
writeLockKeyFor(config.format, sheet.locale),
|
|
9971
|
+
fs,
|
|
9972
|
+
async () => {
|
|
9973
|
+
const result = await runSheet(ctx, sheet, lock);
|
|
9974
|
+
await updateLockFileLocale(cwd, fs, sheet.locale, {
|
|
9975
|
+
mode: "replace",
|
|
9976
|
+
entries: result.lockEntries
|
|
9977
|
+
});
|
|
9978
|
+
collectSheetAdditions(cacheAdditions, sheet.locale, result.cacheAdditions);
|
|
9979
|
+
return result.summary;
|
|
9980
|
+
}
|
|
9981
|
+
);
|
|
7321
9982
|
}
|
|
7322
9983
|
summaries.push(summary);
|
|
7323
9984
|
} catch (error) {
|
|
@@ -7327,7 +9988,7 @@ async function importWorkbook(input, deps = {}) {
|
|
|
7327
9988
|
summaries.push(failureSummary(sheet.locale, error));
|
|
7328
9989
|
}
|
|
7329
9990
|
}
|
|
7330
|
-
summaries.push(...absentLocaleFailures(
|
|
9991
|
+
summaries.push(...absentLocaleFailures(expectedLocales, data.sheets, format, staleLocales));
|
|
7331
9992
|
if (!dryRun) {
|
|
7332
9993
|
await feedTranslationMemory(cwd, fs, computeFingerprint(config), cacheAdditions);
|
|
7333
9994
|
}
|
|
@@ -7342,18 +10003,57 @@ async function loadLockFile(input = {}, deps = {}) {
|
|
|
7342
10003
|
return readLockFile(lockFilePath(cwd), fs);
|
|
7343
10004
|
}
|
|
7344
10005
|
|
|
10006
|
+
// src/redact.ts
|
|
10007
|
+
var REDACTED2 = "[REDACTED]";
|
|
10008
|
+
var KEY_PATTERNS2 = [
|
|
10009
|
+
/\bsk-[A-Za-z0-9_-]{8,}/g,
|
|
10010
|
+
/AIza[0-9A-Za-z_-]{35}/g,
|
|
10011
|
+
/[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
|
|
10012
|
+
];
|
|
10013
|
+
var PROVIDER_ENV_VAR_NAMES = [
|
|
10014
|
+
"ANTHROPIC_API_KEY",
|
|
10015
|
+
"OPENAI_API_KEY",
|
|
10016
|
+
"GEMINI_API_KEY",
|
|
10017
|
+
"DEEPL_API_KEY",
|
|
10018
|
+
"GOOGLE_TRANSLATE_API_KEY",
|
|
10019
|
+
"OPENAI_COMPATIBLE_API_KEY"
|
|
10020
|
+
];
|
|
10021
|
+
function escapeForRegExp2(value) {
|
|
10022
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10023
|
+
}
|
|
10024
|
+
function scrubConfiguredEnvValues(text) {
|
|
10025
|
+
let out = text;
|
|
10026
|
+
for (const name of PROVIDER_ENV_VAR_NAMES) {
|
|
10027
|
+
const value = process.env[name];
|
|
10028
|
+
if (value !== void 0 && value.length > 0) {
|
|
10029
|
+
out = out.replace(new RegExp(escapeForRegExp2(value), "g"), REDACTED2);
|
|
10030
|
+
}
|
|
10031
|
+
}
|
|
10032
|
+
return out;
|
|
10033
|
+
}
|
|
10034
|
+
function redact2(text) {
|
|
10035
|
+
let out = text;
|
|
10036
|
+
for (const pattern of KEY_PATTERNS2) {
|
|
10037
|
+
out = out.replace(pattern, REDACTED2);
|
|
10038
|
+
}
|
|
10039
|
+
return scrubConfiguredEnvValues(out);
|
|
10040
|
+
}
|
|
10041
|
+
|
|
7345
10042
|
// src/scaffolding.ts
|
|
7346
10043
|
var scaffoldingMetadata = {
|
|
7347
10044
|
/** The environment variable each scaffoldable provider reads its API key from. */
|
|
7348
10045
|
providerEnv: PROVIDER_ENV,
|
|
7349
|
-
/**
|
|
10046
|
+
/**
|
|
10047
|
+
* A reasonable default model to prefill per language-model provider. DeepL and Google Cloud
|
|
10048
|
+
* Translation have none, since neither takes a model.
|
|
10049
|
+
*/
|
|
7350
10050
|
scaffoldModels: SCAFFOLD_MODELS,
|
|
7351
10051
|
/**
|
|
7352
10052
|
* The option key each language-model provider takes its output token limit under, since they do
|
|
7353
10053
|
* not agree: Anthropic calls it `maxTokens` and the others `maxOutputTokens`. A generator that
|
|
7354
10054
|
* prefills a token limit must read the key from here rather than assume one, because
|
|
7355
10055
|
* {@link verbatraConfigSchema} validates each provider's options strictly and rejects the wrong
|
|
7356
|
-
* one. DeepL
|
|
10056
|
+
* one. DeepL and Google Cloud Translation have no entry, since neither takes a token limit.
|
|
7357
10057
|
*/
|
|
7358
10058
|
providerTokenLimitKeys: SCAFFOLD_TOKEN_LIMIT_KEYS,
|
|
7359
10059
|
/** Every i18n file format the SDK can read and write. */
|
|
@@ -7370,7 +10070,7 @@ var defaultCreateWatcher = (paths) => {
|
|
|
7370
10070
|
};
|
|
7371
10071
|
};
|
|
7372
10072
|
function defaultRunTranslate(deps) {
|
|
7373
|
-
return (input) =>
|
|
10073
|
+
return (input) => translate3(input, {
|
|
7374
10074
|
...deps.adapterRegistry !== void 0 ? { adapterRegistry: deps.adapterRegistry } : {},
|
|
7375
10075
|
...deps.createProvider !== void 0 ? { createProvider: deps.createProvider } : {},
|
|
7376
10076
|
...deps.fs !== void 0 ? { fs: deps.fs } : {}
|
|
@@ -7470,6 +10170,6 @@ async function watch(input, deps = {}) {
|
|
|
7470
10170
|
return { stop };
|
|
7471
10171
|
}
|
|
7472
10172
|
|
|
7473
|
-
export { CACHE_FILE_NAME, DEFAULT_DELIMITED_PATH, DEFAULT_EXCHANGE_FORMAT, DEFAULT_WORKBOOK_PATH, EXCHANGE_FORMATS, LOCK_FILE_NAME, SdkError, check, createLocalePathResolver, defineConfig, diff, diffLocaleSnapshots, doctor, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, lockState, readGlossaryFile, readLocaleFileSnapshot, retranslateEntry, runStatus, scaffoldingMetadata,
|
|
10173
|
+
export { CACHE_FILE_NAME, DEFAULT_DELIMITED_PATH, DEFAULT_EXCHANGE_FORMAT, DEFAULT_WORKBOOK_PATH, EXCHANGE_FORMATS, LOCK_FILE_NAME, SdkError, check, createLocalePathResolver, defineConfig, diff, diffLocaleSnapshots, doctor, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, localeValues, lockState, readGlossaryFile, readLocaleFileSnapshot, redact2 as redact, retranslateEntry, runStatus, scaffoldingMetadata, translate3 as translate, updateGlossaryTerm, verbatraConfigSchema, watch };
|
|
7474
10174
|
//# sourceMappingURL=index.js.map
|
|
7475
10175
|
//# sourceMappingURL=index.js.map
|