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