lingo.dev 0.113.5 → 0.113.7

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/build/cli.mjs CHANGED
@@ -2151,7 +2151,13 @@ function _isMetadataKey(key) {
2151
2151
  }
2152
2152
 
2153
2153
  // src/cli/loaders/android.ts
2154
- import { parseStringPromise, Builder } from "xml2js";
2154
+ import { createRequire } from "node:module";
2155
+ import { parseStringPromise } from "xml2js";
2156
+ var require2 = createRequire(import.meta.url);
2157
+ var sax = require2("sax");
2158
+ var defaultAndroidResourcesXml = `<?xml version="1.0" encoding="utf-8"?>
2159
+ <resources>
2160
+ </resources>`;
2155
2161
  function createAndroidLoader() {
2156
2162
  return createLoader({
2157
2163
  async pull(locale, input2) {
@@ -2159,173 +2165,1053 @@ function createAndroidLoader() {
2159
2165
  if (!input2) {
2160
2166
  return {};
2161
2167
  }
2162
- const result = {};
2163
- const stringRegex = /<string\s+name="([^"]+)"(?:\s+translatable="([^"]+)")?[^>]*>([\s\S]*?)<\/string>/gi;
2164
- let stringMatch;
2165
- while ((stringMatch = stringRegex.exec(input2)) !== null) {
2166
- const name = stringMatch[1];
2167
- const translatable = stringMatch[2];
2168
- let value = stringMatch[3];
2169
- if (translatable === "false") {
2168
+ const document = await parseAndroidDocument(input2);
2169
+ return buildPullResult(document);
2170
+ } catch (error) {
2171
+ console.error("Error parsing Android resource file:", error);
2172
+ throw new CLIError({
2173
+ message: "Failed to parse Android resource file",
2174
+ docUrl: "androidResouceError"
2175
+ });
2176
+ }
2177
+ },
2178
+ async push(locale, payload, originalInput, originalLocale, pullInput, pullOutput) {
2179
+ try {
2180
+ const selectedBase = selectBaseXml(
2181
+ locale,
2182
+ originalLocale,
2183
+ pullInput,
2184
+ originalInput
2185
+ );
2186
+ const existingDocument = await parseAndroidDocument(selectedBase);
2187
+ const sourceDocument = await parseAndroidDocument(originalInput);
2188
+ const translatedDocument = buildTranslatedDocument(
2189
+ payload,
2190
+ existingDocument,
2191
+ sourceDocument
2192
+ );
2193
+ const referenceXml = selectedBase || originalInput || defaultAndroidResourcesXml;
2194
+ const declaration = resolveXmlDeclaration(referenceXml);
2195
+ return buildAndroidXml(translatedDocument, declaration);
2196
+ } catch (error) {
2197
+ console.error("Error generating Android resource file:", error);
2198
+ throw new CLIError({
2199
+ message: "Failed to generate Android resource file",
2200
+ docUrl: "androidResouceError"
2201
+ });
2202
+ }
2203
+ }
2204
+ });
2205
+ }
2206
+ function resolveXmlDeclaration(xml) {
2207
+ if (!xml) {
2208
+ const xmldec = {
2209
+ version: "1.0",
2210
+ encoding: "utf-8"
2211
+ };
2212
+ return {
2213
+ xmldec,
2214
+ headless: false
2215
+ };
2216
+ }
2217
+ const match2 = xml.match(
2218
+ /<\?xml\s+version="([^"]+)"(?:\s+encoding="([^"]+)")?\s*\?>/
2219
+ );
2220
+ if (match2) {
2221
+ const version = match2[1] && match2[1].trim().length > 0 ? match2[1] : "1.0";
2222
+ const encoding = match2[2] && match2[2].trim().length > 0 ? match2[2] : void 0;
2223
+ const xmldec = encoding ? { version, encoding } : { version };
2224
+ return {
2225
+ xmldec,
2226
+ headless: false
2227
+ };
2228
+ }
2229
+ return { headless: true };
2230
+ }
2231
+ async function parseAndroidDocument(input2) {
2232
+ const xmlToParse = input2 && input2.trim().length > 0 ? input2 : defaultAndroidResourcesXml;
2233
+ const parsed = await parseStringPromise(xmlToParse, {
2234
+ explicitArray: true,
2235
+ explicitChildren: true,
2236
+ preserveChildrenOrder: true,
2237
+ charsAsChildren: true,
2238
+ includeWhiteChars: true,
2239
+ mergeAttrs: false,
2240
+ normalize: false,
2241
+ normalizeTags: false,
2242
+ trim: false,
2243
+ attrkey: "$",
2244
+ charkey: "_",
2245
+ childkey: "$$"
2246
+ });
2247
+ if (!parsed || !parsed.resources) {
2248
+ return {
2249
+ resources: { $$: [] },
2250
+ resourceNodes: []
2251
+ };
2252
+ }
2253
+ const resourcesNode = parsed.resources;
2254
+ resourcesNode["#name"] = resourcesNode["#name"] ?? "resources";
2255
+ resourcesNode.$$ = resourcesNode.$$ ?? [];
2256
+ const metadata = extractResourceMetadata(xmlToParse);
2257
+ const resourceNodes = [];
2258
+ let metaIndex = 0;
2259
+ for (const child of resourcesNode.$$) {
2260
+ const elementName = child?.["#name"];
2261
+ if (!isResourceElementName(elementName)) {
2262
+ continue;
2263
+ }
2264
+ const meta = metadata[metaIndex++];
2265
+ if (!meta || meta.type !== elementName) {
2266
+ continue;
2267
+ }
2268
+ const name = child?.$?.name ?? meta.name;
2269
+ if (!name) {
2270
+ continue;
2271
+ }
2272
+ const translatable = (child?.$?.translatable ?? "").toLowerCase() !== "false";
2273
+ switch (meta.type) {
2274
+ case "string": {
2275
+ resourceNodes.push({
2276
+ type: "string",
2277
+ name,
2278
+ translatable,
2279
+ node: child,
2280
+ meta: cloneTextMeta(meta.meta)
2281
+ });
2282
+ break;
2283
+ }
2284
+ case "string-array": {
2285
+ const itemNodes = child?.item ?? [];
2286
+ const items = [];
2287
+ const templateItems = meta.items;
2288
+ for (let i = 0; i < Math.max(itemNodes.length, templateItems.length); i++) {
2289
+ const nodeItem = itemNodes[i];
2290
+ const templateItem = templateItems[i] ?? templateItems[templateItems.length - 1];
2291
+ if (!nodeItem) {
2170
2292
  continue;
2171
2293
  }
2172
- const cdataRegex = /<!\[CDATA\[([\s\S]*?)\]\]>/g;
2173
- value = value.replace(cdataRegex, (match2, content) => content);
2174
- value = value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/\\'/g, "'");
2175
- result[name] = value;
2294
+ items.push({
2295
+ node: nodeItem,
2296
+ meta: cloneTextMeta(templateItem.meta)
2297
+ });
2176
2298
  }
2177
- const parsed = await parseStringPromise(input2, {
2178
- explicitArray: true,
2179
- mergeAttrs: false,
2180
- normalize: true,
2181
- normalizeTags: false,
2182
- trim: true,
2183
- attrkey: "$",
2184
- charkey: "_"
2299
+ resourceNodes.push({
2300
+ type: "string-array",
2301
+ name,
2302
+ translatable,
2303
+ node: child,
2304
+ items
2185
2305
  });
2186
- if (!parsed || !parsed.resources) {
2187
- return result;
2188
- }
2189
- if (parsed.resources["string-array"]) {
2190
- parsed.resources["string-array"].forEach((arrayItem) => {
2191
- if (arrayItem.$ && arrayItem.$.translatable === "false") {
2192
- return;
2193
- }
2194
- const name = arrayItem.$.name;
2195
- const items = [];
2196
- if (arrayItem.item) {
2197
- arrayItem.item.forEach((item) => {
2198
- let itemValue = "";
2199
- if (typeof item === "string") {
2200
- itemValue = item;
2201
- } else if (item._) {
2202
- itemValue = item._;
2203
- } else if (item._ === "") {
2204
- itemValue = "";
2205
- }
2206
- items.push(
2207
- itemValue === "" || /^\s+$/.test(itemValue) ? itemValue : itemValue.trim()
2208
- );
2209
- });
2210
- }
2211
- result[name] = items;
2306
+ break;
2307
+ }
2308
+ case "plurals": {
2309
+ const itemNodes = child?.item ?? [];
2310
+ const templateItems = meta.items;
2311
+ const items = [];
2312
+ for (const templateItem of templateItems) {
2313
+ const quantity = templateItem.quantity;
2314
+ if (!quantity) {
2315
+ continue;
2316
+ }
2317
+ const nodeItem = itemNodes.find(
2318
+ (item) => item?.$?.quantity === quantity
2319
+ );
2320
+ if (!nodeItem) {
2321
+ continue;
2322
+ }
2323
+ items.push({
2324
+ node: nodeItem,
2325
+ quantity,
2326
+ meta: cloneTextMeta(templateItem.meta)
2212
2327
  });
2213
2328
  }
2214
- if (parsed.resources.plurals) {
2215
- parsed.resources.plurals.forEach((pluralItem) => {
2216
- if (pluralItem.$ && pluralItem.$.translatable === "false") {
2217
- return;
2218
- }
2219
- const name = pluralItem.$.name;
2220
- const pluralObj = {};
2221
- if (pluralItem.item) {
2222
- pluralItem.item.forEach((item) => {
2223
- if (item.$ && item.$.quantity) {
2224
- let value = "";
2225
- if (item._) {
2226
- value = item._;
2227
- } else if (item._ === "") {
2228
- value = "";
2229
- }
2230
- pluralObj[item.$.quantity] = value === "" || /^\s+$/.test(value) ? value : value.trim();
2231
- }
2232
- });
2233
- }
2234
- result[name] = pluralObj;
2235
- });
2329
+ resourceNodes.push({
2330
+ type: "plurals",
2331
+ name,
2332
+ translatable,
2333
+ node: child,
2334
+ items
2335
+ });
2336
+ break;
2337
+ }
2338
+ case "bool": {
2339
+ resourceNodes.push({
2340
+ type: "bool",
2341
+ name,
2342
+ translatable,
2343
+ node: child,
2344
+ meta: cloneTextMeta(meta.meta)
2345
+ });
2346
+ break;
2347
+ }
2348
+ case "integer": {
2349
+ resourceNodes.push({
2350
+ type: "integer",
2351
+ name,
2352
+ translatable,
2353
+ node: child,
2354
+ meta: cloneTextMeta(meta.meta)
2355
+ });
2356
+ break;
2357
+ }
2358
+ }
2359
+ }
2360
+ return { resources: resourcesNode, resourceNodes };
2361
+ }
2362
+ function buildPullResult(document) {
2363
+ const result = {};
2364
+ for (const resource of document.resourceNodes) {
2365
+ if (!resource.translatable) {
2366
+ continue;
2367
+ }
2368
+ switch (resource.type) {
2369
+ case "string": {
2370
+ result[resource.name] = decodeAndroidText(
2371
+ segmentsToString(resource.meta.segments)
2372
+ );
2373
+ break;
2374
+ }
2375
+ case "string-array": {
2376
+ result[resource.name] = resource.items.map(
2377
+ (item) => decodeAndroidText(segmentsToString(item.meta.segments))
2378
+ );
2379
+ break;
2380
+ }
2381
+ case "plurals": {
2382
+ const pluralMap = {};
2383
+ for (const item of resource.items) {
2384
+ pluralMap[item.quantity] = decodeAndroidText(
2385
+ segmentsToString(item.meta.segments)
2386
+ );
2236
2387
  }
2237
- if (parsed.resources.bool) {
2238
- parsed.resources.bool.forEach((boolItem) => {
2239
- if (boolItem.$ && boolItem.$.translatable === "false") {
2240
- return;
2241
- }
2242
- const name = boolItem.$.name;
2243
- result[name] = boolItem._ === "true";
2244
- });
2388
+ result[resource.name] = pluralMap;
2389
+ break;
2390
+ }
2391
+ case "bool": {
2392
+ const value = segmentsToString(resource.meta.segments).trim();
2393
+ result[resource.name] = value === "true";
2394
+ break;
2395
+ }
2396
+ case "integer": {
2397
+ const value = parseInt(
2398
+ segmentsToString(resource.meta.segments).trim(),
2399
+ 10
2400
+ );
2401
+ result[resource.name] = Number.isNaN(value) ? 0 : value;
2402
+ break;
2403
+ }
2404
+ }
2405
+ }
2406
+ return result;
2407
+ }
2408
+ function buildTranslatedDocument(payload, existingDocument, sourceDocument) {
2409
+ const templateDocument = sourceDocument;
2410
+ const finalDocument = cloneDocumentStructure(templateDocument);
2411
+ const templateMap = createResourceMap(templateDocument);
2412
+ const existingMap = createResourceMap(existingDocument);
2413
+ const payloadEntries = payload ?? {};
2414
+ const finalMap = createResourceMap(finalDocument);
2415
+ for (const resource of finalDocument.resourceNodes) {
2416
+ if (!resource.translatable) {
2417
+ continue;
2418
+ }
2419
+ const templateResource = templateMap.get(resource.name);
2420
+ let translationValue;
2421
+ if (Object.prototype.hasOwnProperty.call(payloadEntries, resource.name) && payloadEntries[resource.name] !== void 0 && payloadEntries[resource.name] !== null) {
2422
+ translationValue = payloadEntries[resource.name];
2423
+ } else if (existingMap.has(resource.name)) {
2424
+ translationValue = extractValueFromResource(
2425
+ existingMap.get(resource.name)
2426
+ );
2427
+ } else {
2428
+ translationValue = extractValueFromResource(templateResource ?? resource);
2429
+ }
2430
+ updateResourceNode(resource, translationValue, templateResource);
2431
+ }
2432
+ for (const resource of existingDocument.resourceNodes) {
2433
+ if (finalMap.has(resource.name)) {
2434
+ continue;
2435
+ }
2436
+ const cloned = cloneResourceNode(resource);
2437
+ appendResourceNode(finalDocument, cloned);
2438
+ finalMap.set(cloned.name, cloned);
2439
+ }
2440
+ for (const [name, value] of Object.entries(payloadEntries)) {
2441
+ if (finalMap.has(name)) {
2442
+ continue;
2443
+ }
2444
+ try {
2445
+ const inferred = createResourceNodeFromValue(name, value);
2446
+ appendResourceNode(finalDocument, inferred);
2447
+ finalMap.set(name, inferred);
2448
+ } catch (error) {
2449
+ if (error instanceof CLIError) {
2450
+ throw error;
2451
+ }
2452
+ }
2453
+ }
2454
+ return finalDocument;
2455
+ }
2456
+ function buildAndroidXml(document, declaration) {
2457
+ const xmlBody = serializeElement(document.resources);
2458
+ if (declaration.headless) {
2459
+ return xmlBody;
2460
+ }
2461
+ if (declaration.xmldec) {
2462
+ const { version, encoding } = declaration.xmldec;
2463
+ const encodingPart = encoding ? ` encoding="${encoding}"` : "";
2464
+ return `<?xml version="${version}"${encodingPart}?>
2465
+ ${xmlBody}`;
2466
+ }
2467
+ return `<?xml version="1.0" encoding="utf-8"?>
2468
+ ${xmlBody}`;
2469
+ }
2470
+ function selectBaseXml(locale, originalLocale, pullInput, originalInput) {
2471
+ if (locale === originalLocale) {
2472
+ return pullInput ?? originalInput;
2473
+ }
2474
+ return pullInput ?? originalInput;
2475
+ }
2476
+ function updateResourceNode(target, rawValue, template) {
2477
+ switch (target.type) {
2478
+ case "string": {
2479
+ const value = asString(rawValue, target.name);
2480
+ const templateMeta = template && template.type === "string" ? template.meta : target.meta;
2481
+ const useCdata = templateMeta.hasCdata;
2482
+ setTextualNodeContent(target.node, value, useCdata);
2483
+ target.meta = makeTextMeta([
2484
+ { kind: useCdata ? "cdata" : "text", value }
2485
+ ]);
2486
+ break;
2487
+ }
2488
+ case "string-array": {
2489
+ const values = asStringArray(rawValue, target.name);
2490
+ const templateItems = template && template.type === "string-array" ? template.items : target.items;
2491
+ const maxLength = Math.max(target.items.length, templateItems.length);
2492
+ for (let index = 0; index < maxLength; index++) {
2493
+ const targetItem = target.items[index];
2494
+ const templateItem = templateItems[index] ?? templateItems[templateItems.length - 1] ?? target.items[index];
2495
+ if (!targetItem || !templateItem) {
2496
+ continue;
2245
2497
  }
2246
- if (parsed.resources.integer) {
2247
- parsed.resources.integer.forEach((intItem) => {
2248
- if (intItem.$ && intItem.$.translatable === "false") {
2249
- return;
2250
- }
2251
- const name = intItem.$.name;
2252
- result[name] = parseInt(intItem._ || "0", 10);
2253
- });
2498
+ const translation = index < values.length ? values[index] : segmentsToString(templateItem.meta.segments);
2499
+ const useCdata = templateItem.meta.hasCdata;
2500
+ setTextualNodeContent(targetItem.node, translation, useCdata);
2501
+ targetItem.meta = makeTextMeta([
2502
+ { kind: useCdata ? "cdata" : "text", value: translation }
2503
+ ]);
2504
+ }
2505
+ break;
2506
+ }
2507
+ case "plurals": {
2508
+ const pluralValues = asPluralMap(rawValue, target.name);
2509
+ const templateItems = template && template.type === "plurals" ? template.items : target.items;
2510
+ const templateMap = new Map(
2511
+ templateItems.map((item) => [item.quantity, item])
2512
+ );
2513
+ for (const item of target.items) {
2514
+ const templateItem = templateMap.get(item.quantity) ?? templateMap.values().next().value;
2515
+ const fallback = templateItem ? segmentsToString(templateItem.meta.segments) : segmentsToString(item.meta.segments);
2516
+ const translation = typeof pluralValues[item.quantity] === "string" ? pluralValues[item.quantity] : fallback;
2517
+ const useCdata = templateItem ? templateItem.meta.hasCdata : item.meta.hasCdata;
2518
+ setTextualNodeContent(item.node, translation, useCdata);
2519
+ item.meta = makeTextMeta([
2520
+ { kind: useCdata ? "cdata" : "text", value: translation }
2521
+ ]);
2522
+ }
2523
+ break;
2524
+ }
2525
+ case "bool": {
2526
+ const boolValue = asBoolean(rawValue, target.name);
2527
+ const strValue = boolValue ? "true" : "false";
2528
+ setTextualNodeContent(target.node, strValue, false);
2529
+ target.meta = makeTextMeta([{ kind: "text", value: strValue }]);
2530
+ break;
2531
+ }
2532
+ case "integer": {
2533
+ const intValue = asInteger(rawValue, target.name);
2534
+ const strValue = intValue.toString();
2535
+ setTextualNodeContent(target.node, strValue, false);
2536
+ target.meta = makeTextMeta([{ kind: "text", value: strValue }]);
2537
+ break;
2538
+ }
2539
+ }
2540
+ }
2541
+ function appendResourceNode(document, resourceNode) {
2542
+ document.resources.$$ = document.resources.$$ ?? [];
2543
+ const children = document.resources.$$;
2544
+ if (children.length === 0 || children[children.length - 1]["#name"] !== "__text__" && children[children.length - 1]["#name"] !== "__comment__") {
2545
+ children.push({ "#name": "__text__", _: "\n " });
2546
+ }
2547
+ children.push(resourceNode.node);
2548
+ children.push({ "#name": "__text__", _: "\n" });
2549
+ document.resourceNodes.push(resourceNode);
2550
+ }
2551
+ function setTextualNodeContent(node, value, useCdata) {
2552
+ const escapedValue = useCdata ? value : escapeAndroidString(value);
2553
+ node._ = escapedValue;
2554
+ node.$$ = node.$$ ?? [];
2555
+ let textNode = node.$$.find(
2556
+ (child) => child["#name"] === "__text__" || child["#name"] === "__cdata"
2557
+ );
2558
+ if (!textNode) {
2559
+ textNode = {};
2560
+ node.$$.push(textNode);
2561
+ }
2562
+ textNode["#name"] = useCdata ? "__cdata" : "__text__";
2563
+ textNode._ = useCdata ? value : escapedValue;
2564
+ }
2565
+ function buildResourceNameMap(document) {
2566
+ const map = /* @__PURE__ */ new Map();
2567
+ for (const node of document.resourceNodes) {
2568
+ if (!map.has(node.name)) {
2569
+ map.set(node.name, node);
2570
+ }
2571
+ }
2572
+ return map;
2573
+ }
2574
+ function createResourceMap(document) {
2575
+ return buildResourceNameMap(document);
2576
+ }
2577
+ function cloneResourceNode(resource) {
2578
+ switch (resource.type) {
2579
+ case "string": {
2580
+ const nodeClone = deepClone(resource.node);
2581
+ return {
2582
+ type: "string",
2583
+ name: resource.name,
2584
+ translatable: resource.translatable,
2585
+ node: nodeClone,
2586
+ meta: cloneTextMeta(resource.meta)
2587
+ };
2588
+ }
2589
+ case "string-array": {
2590
+ const nodeClone = deepClone(resource.node);
2591
+ const itemNodes = nodeClone.item ?? [];
2592
+ const items = itemNodes.map((itemNode, index) => {
2593
+ const templateMeta = resource.items[index]?.meta ?? resource.items[resource.items.length - 1]?.meta ?? makeTextMeta([]);
2594
+ return {
2595
+ node: itemNode,
2596
+ meta: cloneTextMeta(templateMeta)
2597
+ };
2598
+ });
2599
+ return {
2600
+ type: "string-array",
2601
+ name: resource.name,
2602
+ translatable: resource.translatable,
2603
+ node: nodeClone,
2604
+ items
2605
+ };
2606
+ }
2607
+ case "plurals": {
2608
+ const nodeClone = deepClone(resource.node);
2609
+ const itemNodes = nodeClone.item ?? [];
2610
+ const items = [];
2611
+ for (const templateItem of resource.items) {
2612
+ const cloneNode = itemNodes.find(
2613
+ (item) => item?.$?.quantity === templateItem.quantity
2614
+ );
2615
+ if (!cloneNode) {
2616
+ continue;
2254
2617
  }
2255
- return result;
2256
- } catch (error) {
2257
- console.error("Error parsing Android resource file:", error);
2618
+ items.push({
2619
+ node: cloneNode,
2620
+ quantity: templateItem.quantity,
2621
+ meta: cloneTextMeta(templateItem.meta)
2622
+ });
2623
+ }
2624
+ return {
2625
+ type: "plurals",
2626
+ name: resource.name,
2627
+ translatable: resource.translatable,
2628
+ node: nodeClone,
2629
+ items
2630
+ };
2631
+ }
2632
+ case "bool": {
2633
+ const nodeClone = deepClone(resource.node);
2634
+ return {
2635
+ type: "bool",
2636
+ name: resource.name,
2637
+ translatable: resource.translatable,
2638
+ node: nodeClone,
2639
+ meta: cloneTextMeta(resource.meta)
2640
+ };
2641
+ }
2642
+ case "integer": {
2643
+ const nodeClone = deepClone(resource.node);
2644
+ return {
2645
+ type: "integer",
2646
+ name: resource.name,
2647
+ translatable: resource.translatable,
2648
+ node: nodeClone,
2649
+ meta: cloneTextMeta(resource.meta)
2650
+ };
2651
+ }
2652
+ }
2653
+ }
2654
+ function cloneTextMeta(meta) {
2655
+ return {
2656
+ hasCdata: meta.hasCdata,
2657
+ segments: meta.segments.map((segment) => ({ ...segment }))
2658
+ };
2659
+ }
2660
+ function asString(value, name) {
2661
+ if (typeof value === "string") {
2662
+ return value;
2663
+ }
2664
+ throw new CLIError({
2665
+ message: `Expected string value for resource "${name}"`,
2666
+ docUrl: "androidResouceError"
2667
+ });
2668
+ }
2669
+ function asStringArray(value, name) {
2670
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
2671
+ return value;
2672
+ }
2673
+ throw new CLIError({
2674
+ message: `Expected array of strings for resource "${name}"`,
2675
+ docUrl: "androidResouceError"
2676
+ });
2677
+ }
2678
+ function asPluralMap(value, name) {
2679
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2680
+ const result = {};
2681
+ for (const [quantity, pluralValue] of Object.entries(value)) {
2682
+ if (typeof pluralValue !== "string") {
2258
2683
  throw new CLIError({
2259
- message: "Failed to parse Android resource file",
2684
+ message: `Expected plural item "${quantity}" of "${name}" to be a string`,
2260
2685
  docUrl: "androidResouceError"
2261
2686
  });
2262
2687
  }
2263
- },
2264
- async push(locale, payload) {
2265
- try {
2266
- const xmlObj = {
2267
- resources: {
2268
- string: [],
2269
- "string-array": [],
2270
- plurals: [],
2271
- bool: [],
2272
- integer: []
2273
- }
2688
+ result[quantity] = pluralValue;
2689
+ }
2690
+ return result;
2691
+ }
2692
+ throw new CLIError({
2693
+ message: `Expected object value for plurals resource "${name}"`,
2694
+ docUrl: "androidResouceError"
2695
+ });
2696
+ }
2697
+ function asBoolean(value, name) {
2698
+ if (typeof value === "boolean") {
2699
+ return value;
2700
+ }
2701
+ if (typeof value === "string") {
2702
+ if (value === "true" || value === "false") {
2703
+ return value === "true";
2704
+ }
2705
+ }
2706
+ throw new CLIError({
2707
+ message: `Expected boolean value for resource "${name}"`,
2708
+ docUrl: "androidResouceError"
2709
+ });
2710
+ }
2711
+ function asInteger(value, name) {
2712
+ if (typeof value === "number" && Number.isInteger(value)) {
2713
+ return value;
2714
+ }
2715
+ throw new CLIError({
2716
+ message: `Expected number value for resource "${name}"`,
2717
+ docUrl: "androidResouceError"
2718
+ });
2719
+ }
2720
+ function escapeAndroidString(value) {
2721
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/(?<!\\)'/g, "\\'");
2722
+ }
2723
+ function segmentsToString(segments) {
2724
+ return segments.map((segment) => segment.value).join("");
2725
+ }
2726
+ function makeTextMeta(segments) {
2727
+ return {
2728
+ segments,
2729
+ hasCdata: segments.some((segment) => segment.kind === "cdata")
2730
+ };
2731
+ }
2732
+ function createResourceNodeFromValue(name, value) {
2733
+ const inferredType = inferTypeFromValue(value);
2734
+ switch (inferredType) {
2735
+ case "string": {
2736
+ const stringValue = asString(value, name);
2737
+ const escaped = escapeAndroidString(stringValue);
2738
+ const node = {
2739
+ "#name": "string",
2740
+ $: { name },
2741
+ _: escaped,
2742
+ $$: [{ "#name": "__text__", _: escaped }]
2743
+ };
2744
+ return {
2745
+ type: "string",
2746
+ name,
2747
+ translatable: true,
2748
+ node,
2749
+ meta: makeTextMeta([{ kind: "text", value: stringValue }])
2750
+ };
2751
+ }
2752
+ case "string-array": {
2753
+ const items = asStringArray(value, name);
2754
+ const node = {
2755
+ "#name": "string-array",
2756
+ $: { name },
2757
+ $$: [],
2758
+ item: []
2759
+ };
2760
+ const itemNodes = [];
2761
+ for (const itemValue of items) {
2762
+ const escaped = escapeAndroidString(itemValue);
2763
+ const itemNode = {
2764
+ "#name": "item",
2765
+ _: escaped,
2766
+ $$: [{ "#name": "__text__", _: escaped }]
2274
2767
  };
2275
- const processHtmlContent = (str) => {
2276
- if (typeof str !== "string") return { _: String(str) };
2277
- const processedStr = str.replace(/(?<!\\)'/g, "\\'");
2278
- return { _: processedStr };
2768
+ node.$$.push(itemNode);
2769
+ node.item.push(itemNode);
2770
+ itemNodes.push({
2771
+ node: itemNode,
2772
+ meta: makeTextMeta([{ kind: "text", value: itemValue }])
2773
+ });
2774
+ }
2775
+ return {
2776
+ type: "string-array",
2777
+ name,
2778
+ translatable: true,
2779
+ node,
2780
+ items: itemNodes
2781
+ };
2782
+ }
2783
+ case "plurals": {
2784
+ const pluralMap = asPluralMap(value, name);
2785
+ const node = {
2786
+ "#name": "plurals",
2787
+ $: { name },
2788
+ $$: [],
2789
+ item: []
2790
+ };
2791
+ const items = [];
2792
+ for (const [quantity, pluralValue] of Object.entries(pluralMap)) {
2793
+ const escaped = escapeAndroidString(pluralValue);
2794
+ const itemNode = {
2795
+ "#name": "item",
2796
+ $: { quantity },
2797
+ _: escaped,
2798
+ $$: [{ "#name": "__text__", _: escaped }]
2279
2799
  };
2280
- for (const [key, value] of Object.entries(payload)) {
2281
- if (typeof value === "string") {
2282
- xmlObj.resources.string.push({
2283
- $: { name: key },
2284
- ...processHtmlContent(value)
2285
- });
2286
- } else if (Array.isArray(value)) {
2287
- xmlObj.resources["string-array"].push({
2288
- $: { name: key },
2289
- item: value.map((item) => processHtmlContent(item))
2290
- });
2291
- } else if (typeof value === "object") {
2292
- xmlObj.resources.plurals.push({
2293
- $: { name: key },
2294
- item: Object.entries(value).map(([quantity, text]) => ({
2295
- $: { quantity },
2296
- ...processHtmlContent(text)
2297
- }))
2298
- });
2299
- } else if (typeof value === "boolean") {
2300
- xmlObj.resources.bool.push({
2301
- $: { name: key },
2302
- _: value.toString()
2303
- });
2304
- } else if (typeof value === "number") {
2305
- xmlObj.resources.integer.push({
2306
- $: { name: key },
2307
- _: value.toString()
2308
- });
2309
- }
2800
+ node.$$.push(itemNode);
2801
+ node.item.push(itemNode);
2802
+ items.push({
2803
+ node: itemNode,
2804
+ quantity,
2805
+ meta: makeTextMeta([{ kind: "text", value: pluralValue }])
2806
+ });
2807
+ }
2808
+ return {
2809
+ type: "plurals",
2810
+ name,
2811
+ translatable: true,
2812
+ node,
2813
+ items
2814
+ };
2815
+ }
2816
+ case "bool": {
2817
+ const boolValue = asBoolean(value, name);
2818
+ const textValue = boolValue ? "true" : "false";
2819
+ const node = {
2820
+ "#name": "bool",
2821
+ $: { name },
2822
+ _: textValue,
2823
+ $$: [{ "#name": "__text__", _: textValue }]
2824
+ };
2825
+ return {
2826
+ type: "bool",
2827
+ name,
2828
+ translatable: true,
2829
+ node,
2830
+ meta: makeTextMeta([{ kind: "text", value: textValue }])
2831
+ };
2832
+ }
2833
+ case "integer": {
2834
+ const intValue = asInteger(value, name);
2835
+ const textValue = intValue.toString();
2836
+ const node = {
2837
+ "#name": "integer",
2838
+ $: { name },
2839
+ _: textValue,
2840
+ $$: [{ "#name": "__text__", _: textValue }]
2841
+ };
2842
+ return {
2843
+ type: "integer",
2844
+ name,
2845
+ translatable: true,
2846
+ node,
2847
+ meta: makeTextMeta([{ kind: "text", value: textValue }])
2848
+ };
2849
+ }
2850
+ }
2851
+ }
2852
+ function cloneDocumentStructure(document) {
2853
+ const resourcesClone = deepClone(document.resources);
2854
+ const lookup = buildResourceLookup(resourcesClone);
2855
+ const resourceNodes = [];
2856
+ for (const resource of document.resourceNodes) {
2857
+ const cloned = cloneResourceNodeFromLookup(resource, lookup);
2858
+ resourceNodes.push(cloned);
2859
+ }
2860
+ return {
2861
+ resources: resourcesClone,
2862
+ resourceNodes
2863
+ };
2864
+ }
2865
+ function buildResourceLookup(resources) {
2866
+ const lookup = /* @__PURE__ */ new Map();
2867
+ const children = Array.isArray(resources.$$) ? resources.$$ : [];
2868
+ for (const child of children) {
2869
+ const type = child?.["#name"];
2870
+ const name = child?.$?.name;
2871
+ if (!type || !name || !isResourceElementName(type)) {
2872
+ continue;
2873
+ }
2874
+ const key = resourceLookupKey(type, name);
2875
+ if (!lookup.has(key)) {
2876
+ lookup.set(key, []);
2877
+ }
2878
+ lookup.get(key).push(child);
2879
+ }
2880
+ return lookup;
2881
+ }
2882
+ function cloneResourceNodeFromLookup(resource, lookup) {
2883
+ const node = takeResourceNode(lookup, resource.type, resource.name);
2884
+ if (!node) {
2885
+ return cloneResourceNode(resource);
2886
+ }
2887
+ switch (resource.type) {
2888
+ case "string": {
2889
+ return {
2890
+ type: "string",
2891
+ name: resource.name,
2892
+ translatable: resource.translatable,
2893
+ node,
2894
+ meta: cloneTextMeta(resource.meta)
2895
+ };
2896
+ }
2897
+ case "string-array": {
2898
+ const childItems = (Array.isArray(node.$$) ? node.$$ : []).filter(
2899
+ (child) => child?.["#name"] === "item"
2900
+ );
2901
+ node.item = childItems;
2902
+ if (childItems.length < resource.items.length) {
2903
+ return cloneResourceNode(resource);
2904
+ }
2905
+ const items = resource.items.map((item, index) => {
2906
+ const nodeItem = childItems[index];
2907
+ if (!nodeItem) {
2908
+ return {
2909
+ node: deepClone(item.node),
2910
+ meta: cloneTextMeta(item.meta)
2911
+ };
2310
2912
  }
2311
- const builder = new Builder({
2312
- headless: true,
2313
- renderOpts: {
2314
- pretty: true,
2315
- indent: " ",
2316
- newline: "\n"
2913
+ return {
2914
+ node: nodeItem,
2915
+ meta: cloneTextMeta(item.meta)
2916
+ };
2917
+ });
2918
+ return {
2919
+ type: "string-array",
2920
+ name: resource.name,
2921
+ translatable: resource.translatable,
2922
+ node,
2923
+ items
2924
+ };
2925
+ }
2926
+ case "plurals": {
2927
+ const childItems = (Array.isArray(node.$$) ? node.$$ : []).filter(
2928
+ (child) => child?.["#name"] === "item"
2929
+ );
2930
+ node.item = childItems;
2931
+ const itemMap = /* @__PURE__ */ new Map();
2932
+ for (const item of childItems) {
2933
+ if (item?.$?.quantity) {
2934
+ itemMap.set(item.$.quantity, item);
2935
+ }
2936
+ }
2937
+ const items = [];
2938
+ for (const templateItem of resource.items) {
2939
+ const nodeItem = itemMap.get(templateItem.quantity);
2940
+ if (!nodeItem) {
2941
+ return cloneResourceNode(resource);
2942
+ }
2943
+ items.push({
2944
+ node: nodeItem,
2945
+ quantity: templateItem.quantity,
2946
+ meta: cloneTextMeta(templateItem.meta)
2947
+ });
2948
+ }
2949
+ return {
2950
+ type: "plurals",
2951
+ name: resource.name,
2952
+ translatable: resource.translatable,
2953
+ node,
2954
+ items
2955
+ };
2956
+ }
2957
+ case "bool": {
2958
+ return {
2959
+ type: "bool",
2960
+ name: resource.name,
2961
+ translatable: resource.translatable,
2962
+ node,
2963
+ meta: cloneTextMeta(resource.meta)
2964
+ };
2965
+ }
2966
+ case "integer": {
2967
+ return {
2968
+ type: "integer",
2969
+ name: resource.name,
2970
+ translatable: resource.translatable,
2971
+ node,
2972
+ meta: cloneTextMeta(resource.meta)
2973
+ };
2974
+ }
2975
+ }
2976
+ }
2977
+ function takeResourceNode(lookup, type, name) {
2978
+ const key = resourceLookupKey(type, name);
2979
+ const list = lookup.get(key);
2980
+ if (!list || list.length === 0) {
2981
+ return void 0;
2982
+ }
2983
+ return list.shift();
2984
+ }
2985
+ function resourceLookupKey(type, name) {
2986
+ return `${type}:${name}`;
2987
+ }
2988
+ function extractValueFromResource(resource) {
2989
+ switch (resource.type) {
2990
+ case "string":
2991
+ return decodeAndroidText(segmentsToString(resource.meta.segments));
2992
+ case "string-array":
2993
+ return resource.items.map(
2994
+ (item) => decodeAndroidText(segmentsToString(item.meta.segments))
2995
+ );
2996
+ case "plurals": {
2997
+ const result = {};
2998
+ for (const item of resource.items) {
2999
+ result[item.quantity] = decodeAndroidText(
3000
+ segmentsToString(item.meta.segments)
3001
+ );
3002
+ }
3003
+ return result;
3004
+ }
3005
+ case "bool": {
3006
+ const value = segmentsToString(resource.meta.segments).trim();
3007
+ return value === "true";
3008
+ }
3009
+ case "integer": {
3010
+ const value = parseInt(
3011
+ segmentsToString(resource.meta.segments).trim(),
3012
+ 10
3013
+ );
3014
+ return Number.isNaN(value) ? 0 : value;
3015
+ }
3016
+ }
3017
+ }
3018
+ function inferTypeFromValue(value) {
3019
+ if (typeof value === "string") {
3020
+ return "string";
3021
+ }
3022
+ if (Array.isArray(value)) {
3023
+ return "string-array";
3024
+ }
3025
+ if (value && typeof value === "object") {
3026
+ return "plurals";
3027
+ }
3028
+ if (typeof value === "boolean") {
3029
+ return "bool";
3030
+ }
3031
+ if (typeof value === "number" && Number.isInteger(value)) {
3032
+ return "integer";
3033
+ }
3034
+ throw new CLIError({
3035
+ message: "Unable to infer Android resource type from payload",
3036
+ docUrl: "androidResouceError"
3037
+ });
3038
+ }
3039
+ function extractResourceMetadata(xml) {
3040
+ const parser = sax.parser(true, {
3041
+ trim: false,
3042
+ normalize: false,
3043
+ lowercase: false
3044
+ });
3045
+ const stack = [];
3046
+ const result = [];
3047
+ parser.onopentag = (node) => {
3048
+ const lowerName = node.name.toLowerCase();
3049
+ const attributes = {};
3050
+ for (const [key, value] of Object.entries(node.attributes ?? {})) {
3051
+ attributes[key.toLowerCase()] = String(value);
3052
+ }
3053
+ stack.push({
3054
+ name: lowerName,
3055
+ rawName: node.name,
3056
+ attributes,
3057
+ segments: [],
3058
+ items: []
3059
+ });
3060
+ if (lowerName !== "resources" && lowerName !== "item" && !isResourceElementName(lowerName)) {
3061
+ const attrString = Object.entries(node.attributes ?? {}).map(
3062
+ ([key, value]) => ` ${key}="${escapeAttributeValue(String(value))}"`
3063
+ ).join("");
3064
+ appendSegmentToNearestResource(stack, {
3065
+ kind: "text",
3066
+ value: `<${node.name}${attrString}>`
3067
+ });
3068
+ }
3069
+ };
3070
+ parser.ontext = (text) => {
3071
+ if (!text) {
3072
+ return;
3073
+ }
3074
+ appendSegmentToNearestResource(stack, { kind: "text", value: text });
3075
+ };
3076
+ parser.oncdata = (cdata) => {
3077
+ appendSegmentToNearestResource(stack, { kind: "cdata", value: cdata });
3078
+ };
3079
+ parser.onclosetag = () => {
3080
+ const entry = stack.pop();
3081
+ if (!entry) {
3082
+ return;
3083
+ }
3084
+ const parent = stack[stack.length - 1];
3085
+ if (entry.name === "item" && parent) {
3086
+ const meta = makeTextMeta(entry.segments);
3087
+ parent.items.push({
3088
+ quantity: entry.attributes.quantity,
3089
+ meta
3090
+ });
3091
+ return;
3092
+ }
3093
+ if (entry.name !== "resources" && entry.name !== "item" && !isResourceElementName(entry.name)) {
3094
+ appendSegmentToNearestResource(stack, {
3095
+ kind: "text",
3096
+ value: `</${entry.rawName}>`
3097
+ });
3098
+ return;
3099
+ }
3100
+ if (!isResourceElementName(entry.name)) {
3101
+ return;
3102
+ }
3103
+ const name = entry.attributes.name;
3104
+ if (!name) {
3105
+ return;
3106
+ }
3107
+ const translatable = (entry.attributes.translatable ?? "").toLowerCase() !== "false";
3108
+ switch (entry.name) {
3109
+ case "string": {
3110
+ result.push({
3111
+ type: "string",
3112
+ name,
3113
+ translatable,
3114
+ meta: makeTextMeta(entry.segments)
3115
+ });
3116
+ break;
3117
+ }
3118
+ case "string-array": {
3119
+ result.push({
3120
+ type: "string-array",
3121
+ name,
3122
+ translatable,
3123
+ items: entry.items.map((item) => ({
3124
+ meta: cloneTextMeta(item.meta)
3125
+ }))
3126
+ });
3127
+ break;
3128
+ }
3129
+ case "plurals": {
3130
+ const items = [];
3131
+ for (const item of entry.items) {
3132
+ if (!item.quantity) {
3133
+ continue;
2317
3134
  }
3135
+ items.push({
3136
+ quantity: item.quantity,
3137
+ meta: cloneTextMeta(item.meta)
3138
+ });
3139
+ }
3140
+ result.push({
3141
+ type: "plurals",
3142
+ name,
3143
+ translatable,
3144
+ items
2318
3145
  });
2319
- return builder.buildObject(xmlObj);
2320
- } catch (error) {
2321
- console.error("Error generating Android resource file:", error);
2322
- throw new CLIError({
2323
- message: "Failed to generate Android resource file",
2324
- docUrl: "androidResouceError"
3146
+ break;
3147
+ }
3148
+ case "bool": {
3149
+ result.push({
3150
+ type: "bool",
3151
+ name,
3152
+ translatable,
3153
+ meta: makeTextMeta(entry.segments)
3154
+ });
3155
+ break;
3156
+ }
3157
+ case "integer": {
3158
+ result.push({
3159
+ type: "integer",
3160
+ name,
3161
+ translatable,
3162
+ meta: makeTextMeta(entry.segments)
2325
3163
  });
3164
+ break;
2326
3165
  }
2327
3166
  }
2328
- });
3167
+ };
3168
+ parser.write(xml).close();
3169
+ return result;
3170
+ }
3171
+ function appendSegmentToNearestResource(stack, segment) {
3172
+ for (let index = stack.length - 1; index >= 0; index--) {
3173
+ const entry = stack[index];
3174
+ if (entry.name === "string" || entry.name === "item" || entry.name === "bool" || entry.name === "integer") {
3175
+ entry.segments.push(segment);
3176
+ return;
3177
+ }
3178
+ }
3179
+ }
3180
+ function isResourceElementName(value) {
3181
+ return value === "string" || value === "string-array" || value === "plurals" || value === "bool" || value === "integer";
3182
+ }
3183
+ function deepClone(value) {
3184
+ return value === void 0 ? value : JSON.parse(JSON.stringify(value));
3185
+ }
3186
+ function serializeElement(node) {
3187
+ if (!node) {
3188
+ return "";
3189
+ }
3190
+ const name = node["#name"] ?? "resources";
3191
+ if (name === "__text__") {
3192
+ return node._ ?? "";
3193
+ }
3194
+ if (name === "__cdata") {
3195
+ return `<![CDATA[${node._ ?? ""}]]>`;
3196
+ }
3197
+ if (name === "__comment__") {
3198
+ return `<!--${node._ ?? ""}-->`;
3199
+ }
3200
+ const attributes = node.$ ?? {};
3201
+ const attrString = Object.entries(attributes).map(([key, value]) => ` ${key}="${escapeAttributeValue(String(value))}"`).join("");
3202
+ const children = Array.isArray(node.$$) ? node.$$ : [];
3203
+ if (children.length === 0) {
3204
+ const textContent = node._ ?? "";
3205
+ return `<${name}${attrString}>${textContent}</${name}>`;
3206
+ }
3207
+ const childContent = children.map(serializeElement).join("");
3208
+ return `<${name}${attrString}>${childContent}</${name}>`;
3209
+ }
3210
+ function escapeAttributeValue(value) {
3211
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&apos;");
3212
+ }
3213
+ function decodeAndroidText(value) {
3214
+ return value.replace(/\\'/g, "'");
2329
3215
  }
2330
3216
 
2331
3217
  // src/cli/loaders/csv.ts
@@ -2751,39 +3637,320 @@ function parsePropertyLine(line) {
2751
3637
  };
2752
3638
  }
2753
3639
 
3640
+ // src/cli/loaders/xcode-strings/tokenizer.ts
3641
+ var Tokenizer = class {
3642
+ input;
3643
+ pos;
3644
+ line;
3645
+ column;
3646
+ constructor(input2) {
3647
+ this.input = input2;
3648
+ this.pos = 0;
3649
+ this.line = 1;
3650
+ this.column = 1;
3651
+ }
3652
+ tokenize() {
3653
+ const tokens = [];
3654
+ while (this.pos < this.input.length) {
3655
+ const char = this.current();
3656
+ if (this.isWhitespace(char)) {
3657
+ this.advance();
3658
+ continue;
3659
+ }
3660
+ if (char === "/" && this.peek() === "/") {
3661
+ tokens.push(this.scanSingleLineComment());
3662
+ continue;
3663
+ }
3664
+ if (char === "/" && this.peek() === "*") {
3665
+ tokens.push(this.scanMultiLineComment());
3666
+ continue;
3667
+ }
3668
+ if (char === '"') {
3669
+ tokens.push(this.scanString());
3670
+ continue;
3671
+ }
3672
+ if (char === "=") {
3673
+ tokens.push(this.makeToken("EQUALS" /* EQUALS */, "="));
3674
+ this.advance();
3675
+ continue;
3676
+ }
3677
+ if (char === ";") {
3678
+ tokens.push(this.makeToken("SEMICOLON" /* SEMICOLON */, ";"));
3679
+ this.advance();
3680
+ continue;
3681
+ }
3682
+ this.advance();
3683
+ }
3684
+ tokens.push(this.makeToken("EOF" /* EOF */, ""));
3685
+ return tokens;
3686
+ }
3687
+ scanString() {
3688
+ const start = this.getPosition();
3689
+ let value = "";
3690
+ this.advance();
3691
+ while (this.pos < this.input.length) {
3692
+ const char = this.current();
3693
+ if (char === "\\") {
3694
+ this.advance();
3695
+ if (this.pos < this.input.length) {
3696
+ const nextChar = this.current();
3697
+ value += "\\" + nextChar;
3698
+ this.advance();
3699
+ }
3700
+ continue;
3701
+ }
3702
+ if (char === '"') {
3703
+ this.advance();
3704
+ return {
3705
+ type: "STRING" /* STRING */,
3706
+ value,
3707
+ ...start
3708
+ };
3709
+ }
3710
+ value += char;
3711
+ this.advance();
3712
+ }
3713
+ return {
3714
+ type: "STRING" /* STRING */,
3715
+ value,
3716
+ ...start
3717
+ };
3718
+ }
3719
+ scanSingleLineComment() {
3720
+ const start = this.getPosition();
3721
+ let value = "";
3722
+ this.advance();
3723
+ this.advance();
3724
+ while (this.pos < this.input.length && this.current() !== "\n") {
3725
+ value += this.current();
3726
+ this.advance();
3727
+ }
3728
+ return {
3729
+ type: "COMMENT_SINGLE" /* COMMENT_SINGLE */,
3730
+ value,
3731
+ ...start
3732
+ };
3733
+ }
3734
+ scanMultiLineComment() {
3735
+ const start = this.getPosition();
3736
+ let value = "";
3737
+ this.advance();
3738
+ this.advance();
3739
+ while (this.pos < this.input.length) {
3740
+ if (this.current() === "*" && this.peek() === "/") {
3741
+ this.advance();
3742
+ this.advance();
3743
+ return {
3744
+ type: "COMMENT_MULTI" /* COMMENT_MULTI */,
3745
+ value,
3746
+ ...start
3747
+ };
3748
+ }
3749
+ value += this.current();
3750
+ this.advance();
3751
+ }
3752
+ return {
3753
+ type: "COMMENT_MULTI" /* COMMENT_MULTI */,
3754
+ value,
3755
+ ...start
3756
+ };
3757
+ }
3758
+ current() {
3759
+ return this.input[this.pos];
3760
+ }
3761
+ peek() {
3762
+ if (this.pos + 1 < this.input.length) {
3763
+ return this.input[this.pos + 1];
3764
+ }
3765
+ return null;
3766
+ }
3767
+ advance() {
3768
+ if (this.pos < this.input.length) {
3769
+ if (this.current() === "\n") {
3770
+ this.line++;
3771
+ this.column = 1;
3772
+ } else {
3773
+ this.column++;
3774
+ }
3775
+ this.pos++;
3776
+ }
3777
+ }
3778
+ isWhitespace(char) {
3779
+ return char === " " || char === " " || char === "\n" || char === "\r";
3780
+ }
3781
+ getPosition() {
3782
+ return {
3783
+ line: this.line,
3784
+ column: this.column
3785
+ };
3786
+ }
3787
+ makeToken(type, value) {
3788
+ return {
3789
+ type,
3790
+ value,
3791
+ ...this.getPosition()
3792
+ };
3793
+ }
3794
+ };
3795
+
3796
+ // src/cli/loaders/xcode-strings/escape.ts
3797
+ function unescapeString(raw) {
3798
+ let result = "";
3799
+ let i = 0;
3800
+ while (i < raw.length) {
3801
+ if (raw[i] === "\\" && i + 1 < raw.length) {
3802
+ const nextChar = raw[i + 1];
3803
+ switch (nextChar) {
3804
+ case '"':
3805
+ result += '"';
3806
+ i += 2;
3807
+ break;
3808
+ case "\\":
3809
+ result += "\\";
3810
+ i += 2;
3811
+ break;
3812
+ case "n":
3813
+ result += "\n";
3814
+ i += 2;
3815
+ break;
3816
+ case "t":
3817
+ result += " ";
3818
+ i += 2;
3819
+ break;
3820
+ case "r":
3821
+ result += "\r";
3822
+ i += 2;
3823
+ break;
3824
+ default:
3825
+ result += raw[i];
3826
+ i++;
3827
+ break;
3828
+ }
3829
+ } else {
3830
+ result += raw[i];
3831
+ i++;
3832
+ }
3833
+ }
3834
+ return result;
3835
+ }
3836
+ function escapeString(str) {
3837
+ if (str == null) {
3838
+ return "";
3839
+ }
3840
+ let result = "";
3841
+ for (let i = 0; i < str.length; i++) {
3842
+ const char = str[i];
3843
+ switch (char) {
3844
+ case "\\":
3845
+ result += "\\\\";
3846
+ break;
3847
+ case '"':
3848
+ result += '\\"';
3849
+ break;
3850
+ case "\n":
3851
+ result += "\\n";
3852
+ break;
3853
+ case "\r":
3854
+ result += "\\r";
3855
+ break;
3856
+ case " ":
3857
+ result += "\\t";
3858
+ break;
3859
+ default:
3860
+ result += char;
3861
+ break;
3862
+ }
3863
+ }
3864
+ return result;
3865
+ }
3866
+
3867
+ // src/cli/loaders/xcode-strings/parser.ts
3868
+ var Parser = class {
3869
+ tokens;
3870
+ pos;
3871
+ constructor(tokens) {
3872
+ this.tokens = tokens;
3873
+ this.pos = 0;
3874
+ }
3875
+ parse() {
3876
+ const result = {};
3877
+ while (this.pos < this.tokens.length) {
3878
+ const token = this.current();
3879
+ if (token.type === "COMMENT_SINGLE" /* COMMENT_SINGLE */ || token.type === "COMMENT_MULTI" /* COMMENT_MULTI */) {
3880
+ this.advance();
3881
+ continue;
3882
+ }
3883
+ if (token.type === "EOF" /* EOF */) {
3884
+ break;
3885
+ }
3886
+ if (token.type === "STRING" /* STRING */) {
3887
+ const entry = this.parseEntry();
3888
+ if (entry) {
3889
+ result[entry.key] = entry.value;
3890
+ }
3891
+ continue;
3892
+ }
3893
+ this.advance();
3894
+ }
3895
+ return result;
3896
+ }
3897
+ parseEntry() {
3898
+ const keyToken = this.current();
3899
+ if (keyToken.type !== "STRING" /* STRING */) {
3900
+ return null;
3901
+ }
3902
+ const key = keyToken.value;
3903
+ this.advance();
3904
+ if (!this.expect("EQUALS" /* EQUALS */)) {
3905
+ return null;
3906
+ }
3907
+ const valueToken = this.current();
3908
+ if (valueToken.type !== "STRING" /* STRING */) {
3909
+ return null;
3910
+ }
3911
+ const rawValue = valueToken.value;
3912
+ this.advance();
3913
+ if (!this.expect("SEMICOLON" /* SEMICOLON */)) {
3914
+ }
3915
+ const value = unescapeString(rawValue);
3916
+ return { key, value };
3917
+ }
3918
+ current() {
3919
+ return this.tokens[this.pos];
3920
+ }
3921
+ advance() {
3922
+ if (this.pos < this.tokens.length) {
3923
+ this.pos++;
3924
+ }
3925
+ }
3926
+ expect(type) {
3927
+ if (this.current()?.type === type) {
3928
+ this.advance();
3929
+ return true;
3930
+ }
3931
+ return false;
3932
+ }
3933
+ };
3934
+
2754
3935
  // src/cli/loaders/xcode-strings.ts
2755
3936
  function createXcodeStringsLoader() {
2756
3937
  return createLoader({
2757
3938
  async pull(locale, input2) {
2758
- const lines = input2.split("\n");
2759
- const result = {};
2760
- for (const line of lines) {
2761
- const trimmedLine = line.trim();
2762
- if (trimmedLine && !trimmedLine.startsWith("//")) {
2763
- const match2 = trimmedLine.match(/^"(.+)"\s*=\s*"(.+)";$/);
2764
- if (match2) {
2765
- const [, key, value] = match2;
2766
- result[key] = unescapeXcodeString(value);
2767
- }
2768
- }
2769
- }
3939
+ const tokenizer = new Tokenizer(input2);
3940
+ const tokens = tokenizer.tokenize();
3941
+ const parser = new Parser(tokens);
3942
+ const result = parser.parse();
2770
3943
  return result;
2771
3944
  },
2772
3945
  async push(locale, payload) {
2773
- const lines = Object.entries(payload).map(([key, value]) => {
2774
- const escapedValue = escapeXcodeString(value);
3946
+ const lines = Object.entries(payload).filter(([_36, value]) => value != null).map(([key, value]) => {
3947
+ const escapedValue = escapeString(value);
2775
3948
  return `"${key}" = "${escapedValue}";`;
2776
3949
  });
2777
3950
  return lines.join("\n");
2778
3951
  }
2779
3952
  });
2780
3953
  }
2781
- function unescapeXcodeString(str) {
2782
- return str.replace(/\\"/g, '"').replace(/\\n/g, "\n").replace(/\\\\/g, "\\");
2783
- }
2784
- function escapeXcodeString(str) {
2785
- return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
2786
- }
2787
3954
 
2788
3955
  // src/cli/loaders/xcode-stringsdict.ts
2789
3956
  import plist from "plist";
@@ -4095,7 +5262,7 @@ function pushNewFile(locale, translations, originalLocale) {
4095
5262
  }
4096
5263
 
4097
5264
  // src/cli/loaders/xml.ts
4098
- import { parseStringPromise as parseStringPromise2, Builder as Builder2 } from "xml2js";
5265
+ import { parseStringPromise as parseStringPromise2, Builder } from "xml2js";
4099
5266
  function normalizeXMLString(xmlString) {
4100
5267
  return xmlString.replace(/\s+/g, " ").replace(/>\s+</g, "><").replace("\n", "").trim();
4101
5268
  }
@@ -4122,7 +5289,7 @@ function createXmlLoader() {
4122
5289
  },
4123
5290
  async push(locale, data) {
4124
5291
  try {
4125
- const builder = new Builder2({ headless: true });
5292
+ const builder = new Builder({ headless: true });
4126
5293
  const xmlOutput = builder.buildObject(data);
4127
5294
  const expectedOutput = normalizeXMLString(xmlOutput);
4128
5295
  return expectedOutput;
@@ -12212,7 +13379,7 @@ async function renderHero2() {
12212
13379
  // package.json
12213
13380
  var package_default = {
12214
13381
  name: "lingo.dev",
12215
- version: "0.113.5",
13382
+ version: "0.113.7",
12216
13383
  description: "Lingo.dev CLI",
12217
13384
  private: false,
12218
13385
  publishConfig: {
@@ -12419,6 +13586,7 @@ var package_default = {
12419
13586
  "remark-parse": "^11.0.0",
12420
13587
  "remark-rehype": "^11.1.2",
12421
13588
  "remark-stringify": "^11.0.0",
13589
+ sax: "^1.4.1",
12422
13590
  "srt-parser-2": "^1.2.3",
12423
13591
  unified: "^11.0.5",
12424
13592
  "unist-util-visit": "^5.0.0",