@braze/sanity-plugin-ai-translation 0.2.64 → 0.2.66

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
@@ -6,6 +6,7 @@ import defaults from "lodash.defaults";
6
6
  import { Card, Stack, Flex, Text, Box, Heading, Button, Spinner, Inline, Checkbox, Grid, Radio, TextInput, Badge, Select, Label, Code, Tooltip } from "@sanity/ui";
7
7
  import { toPlainText } from "@portabletext/react";
8
8
  import { uuid } from "@sanity/uuid";
9
+ import { XMLParser } from "fast-xml-parser";
9
10
  import cloneDeep from "lodash.clonedeep";
10
11
  import get from "lodash.get";
11
12
  import set from "lodash.set";
@@ -1539,7 +1540,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1539
1540
  cleanedLength: cleanedText.length,
1540
1541
  preview: cleanedText.substring(0, 500)
1541
1542
  });
1542
- const parsed = parseReviewFormat(cleanedText, originalDoc);
1543
+ const parsed = parseResponseToNodes(cleanedText, originalDoc);
1543
1544
  devLog.debug("Parsed response structure", {
1544
1545
  rootNodeCount: parsed.length,
1545
1546
  rootNodeNames: parsed.map((n) => n.type === "field" && n.name ? n.name : "unknown")
@@ -1561,7 +1562,118 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1561
1562
  codeBlocksRemoved,
1562
1563
  preview: cleaned.trim().substring(0, 200)
1563
1564
  }), cleaned.trim();
1564
- }, parseReviewFormat = (text, originalDoc) => {
1565
+ }, TAG_NAMES = ["field", "block", "span"], looksLikeXmlResponse = (text) => TAG_NAMES.some((tag) => text.includes(`<${tag}`));
1566
+ function parseCompactFormatSections(text, originalDoc) {
1567
+ const nodes = [], lines = text.split(`
1568
+ `), compactStartRegex = /^---\s+(\w+)\s+---$/;
1569
+ for (let i = 0; i < lines.length; i++) {
1570
+ const match = lines[i].trim().match(compactStartRegex);
1571
+ if (!match) continue;
1572
+ const fieldName = match[1], contentLines = [];
1573
+ for (i++; i < lines.length; ) {
1574
+ const currentLine = lines[i];
1575
+ if (currentLine.trim().match(new RegExp(`^---\\s+/${fieldName}\\s+---$`)))
1576
+ break;
1577
+ contentLines.push(currentLine), i++;
1578
+ }
1579
+ const content = contentLines.join(`
1580
+ `);
1581
+ let contentType;
1582
+ try {
1583
+ const fieldValue = get(originalDoc, [fieldName]);
1584
+ if (fieldValue && typeof fieldValue == "object" && fieldValue !== null) {
1585
+ const fieldType = fieldValue._type;
1586
+ typeof fieldType == "string" && fieldType.startsWith("translatable.") && (contentType = fieldType);
1587
+ }
1588
+ } catch {
1589
+ }
1590
+ contentType || (content.includes("<block") || content.includes("<span") ? contentType = "translatable.richtext" : contentType = "translatable.vtt.captions"), nodes.push({
1591
+ type: "field",
1592
+ name: fieldName,
1593
+ text: content,
1594
+ attributes: { type: contentType },
1595
+ children: []
1596
+ });
1597
+ }
1598
+ return nodes;
1599
+ }
1600
+ const parseResponseToNodes = (text, originalDoc) => {
1601
+ if (looksLikeXmlResponse(text))
1602
+ try {
1603
+ const wrapped = `<root>${text}</root>`, parsed = new XMLParser({
1604
+ ignoreAttributes: !1,
1605
+ attributeNamePrefix: "@_",
1606
+ textNodeName: "#text",
1607
+ parseTagValue: !1,
1608
+ trimValues: !1,
1609
+ // preserve exact whitespace in text (e.g. trailing space in spans)
1610
+ isArray: (tagName) => TAG_NAMES.includes(tagName)
1611
+ }).parse(wrapped);
1612
+ if (parsed && typeof parsed == "object" && parsed.root) {
1613
+ const xmlNodes = convertXmlObjectToParsedNodes(parsed.root), compactNodes = parseCompactFormatSections(text, originalDoc);
1614
+ if (xmlNodes.length > 0 || compactNodes.length > 0) {
1615
+ const merged = [...xmlNodes, ...compactNodes];
1616
+ return devLog.debug("Parsed response with XML parser + compact sections", {
1617
+ xmlNodeCount: xmlNodes.length,
1618
+ compactNodeCount: compactNodes.length,
1619
+ total: merged.length
1620
+ }), merged;
1621
+ }
1622
+ }
1623
+ } catch (err) {
1624
+ devLog.warn("XML parse failed, falling back to regex parser", {
1625
+ error: err instanceof Error ? err.message : String(err)
1626
+ });
1627
+ }
1628
+ return parseReviewFormat(text, originalDoc);
1629
+ };
1630
+ function normalizeToArray(value) {
1631
+ return Array.isArray(value) ? value : value != null && typeof value == "object" ? [value] : [];
1632
+ }
1633
+ function convertXmlObjectToParsedNodes(root) {
1634
+ const result = [];
1635
+ for (const tagName of TAG_NAMES) {
1636
+ const raw = root[tagName];
1637
+ if (raw !== void 0) {
1638
+ for (const item of normalizeToArray(raw))
1639
+ if (item != null && typeof item == "object") {
1640
+ const node = convertXmlElementToParsedNode(item, tagName);
1641
+ node && result.push(node);
1642
+ }
1643
+ }
1644
+ }
1645
+ return result;
1646
+ }
1647
+ function convertXmlElementToParsedNode(obj, tagName) {
1648
+ const attributes = {};
1649
+ for (const [key, value] of Object.entries(obj))
1650
+ if (key.startsWith("@_")) {
1651
+ const attrName = key.slice(2);
1652
+ attributes[attrName] = value != null ? String(value) : "";
1653
+ }
1654
+ let text;
1655
+ const textVal = obj["#text"];
1656
+ textVal != null && (text = typeof textVal == "string" ? textVal : String(textVal));
1657
+ const children = [];
1658
+ for (const childTag of TAG_NAMES) {
1659
+ const raw = obj[childTag];
1660
+ if (raw !== void 0) {
1661
+ for (const item of normalizeToArray(raw))
1662
+ if (item != null && typeof item == "object") {
1663
+ const child = convertXmlElementToParsedNode(item, childTag);
1664
+ child && children.push(child);
1665
+ }
1666
+ }
1667
+ }
1668
+ return {
1669
+ type: tagName,
1670
+ name: attributes.name,
1671
+ text: text !== void 0 && text !== "" ? unescapeXml(text) : void 0,
1672
+ attributes,
1673
+ children
1674
+ };
1675
+ }
1676
+ const parseReviewFormat = (text, originalDoc) => {
1565
1677
  const root = [], stack = [], lines = text.split(`
1566
1678
  `);
1567
1679
  devLog.debug("Parsing response text", {
@@ -1897,10 +2009,22 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1897
2009
  });
1898
2010
  return;
1899
2011
  }
1900
- node.text !== void 0 && !node.attributes.readonly && (get(doc, path) !== void 0 ? set(doc, path, node.text) : devLog.debug("Skipping field not present in original document", {
1901
- path: path.join("."),
1902
- attemptedValue: node.text
1903
- }));
2012
+ if (node.text !== void 0 && !node.attributes.readonly) {
2013
+ const existingValue = get(doc, path);
2014
+ if (existingValue === void 0)
2015
+ devLog.debug("Skipping field not present in original document", {
2016
+ path: path.join("."),
2017
+ attemptedValue: node.text
2018
+ });
2019
+ else if (typeof existingValue == "string") {
2020
+ const value = path.length === 1 ? node.text.trim() : node.text;
2021
+ set(doc, path, value);
2022
+ } else
2023
+ devLog.warn("Skipping text apply at path where value is not a string (prevents structure corruption)", {
2024
+ path: path.join("."),
2025
+ actualType: typeof existingValue
2026
+ });
2027
+ }
1904
2028
  }, applyBlockChanges = (block, blockNode) => {
1905
2029
  if (blockNode.attributes._key && block._key !== blockNode.attributes._key) {
1906
2030
  devLog.warn("Block key mismatch, skipping", {