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