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

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,7 +6,6 @@ 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";
10
9
  import cloneDeep from "lodash.clonedeep";
11
10
  import get from "lodash.get";
12
11
  import set from "lodash.set";
@@ -1540,7 +1539,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1540
1539
  cleanedLength: cleanedText.length,
1541
1540
  preview: cleanedText.substring(0, 500)
1542
1541
  });
1543
- const parsed = parseResponseToNodes(cleanedText, originalDoc);
1542
+ const parsed = parseReviewFormat(cleanedText, originalDoc);
1544
1543
  devLog.debug("Parsed response structure", {
1545
1544
  rootNodeCount: parsed.length,
1546
1545
  rootNodeNames: parsed.map((n) => n.type === "field" && n.name ? n.name : "unknown")
@@ -1562,118 +1561,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1562
1561
  codeBlocksRemoved,
1563
1562
  preview: cleaned.trim().substring(0, 200)
1564
1563
  }), cleaned.trim();
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) => {
1564
+ }, parseReviewFormat = (text, originalDoc) => {
1677
1565
  const root = [], stack = [], lines = text.split(`
1678
1566
  `);
1679
1567
  devLog.debug("Parsing response text", {
@@ -1761,25 +1649,34 @@ const parseReviewFormat = (text, originalDoc) => {
1761
1649
  attributes,
1762
1650
  children: []
1763
1651
  }, closingTag = `</${tagName}>`;
1764
- if (content.endsWith(closingTag.replace(/[<>]/g, ""))) {
1765
- const textContent = content.substring(0, content.lastIndexOf("<"));
1766
- if (node.text = unescapeXml(textContent), stack.length > 0) {
1652
+ if (content.includes(closingTag)) {
1653
+ const closeIdx = content.indexOf(closingTag);
1654
+ node.text = unescapeXml(content.substring(0, closeIdx));
1655
+ const remainder = content.substring(closeIdx + closingTag.length).trim();
1656
+ if (stack.length > 0) {
1767
1657
  const parent = stack[stack.length - 1];
1768
1658
  parent && parent.children.push(node);
1769
1659
  } else
1770
1660
  root.push(node);
1771
- } else if (content.includes(closingTag))
1772
- if (node.text = unescapeXml(content.replace(closingTag, "")), stack.length > 0) {
1661
+ remainder && (lines[i] = remainder, i--);
1662
+ } else if (content.endsWith(closingTag)) {
1663
+ const textContent = content.substring(0, content.lastIndexOf("<"));
1664
+ if (node.text = unescapeXml(textContent), stack.length > 0) {
1773
1665
  const parent = stack[stack.length - 1];
1774
1666
  parent && parent.children.push(node);
1775
1667
  } else
1776
1668
  root.push(node);
1777
- else if (attributes.type && typeof attributes.type == "string" && attributes.type.startsWith("translatable.")) {
1669
+ } else if (attributes.type && typeof attributes.type == "string" && attributes.type.startsWith("translatable.")) {
1778
1670
  const contentLines = [];
1779
1671
  for (i++; i < lines.length; ) {
1780
1672
  const currentLine = lines[i];
1781
- if (currentLine.trim().match(/^<\/field>$/))
1673
+ if (currentLine.includes("</field>")) {
1674
+ const idx = currentLine.indexOf("</field>"), before = currentLine.substring(0, idx).trimEnd();
1675
+ before.trim() && contentLines.push(before);
1676
+ const after = currentLine.substring(idx + 8).trim();
1677
+ after && (lines[i] = after, i--);
1782
1678
  break;
1679
+ }
1783
1680
  contentLines.push(currentLine), i++;
1784
1681
  }
1785
1682
  if (node.text = contentLines.join(`
@@ -1788,19 +1685,15 @@ const parseReviewFormat = (text, originalDoc) => {
1788
1685
  parent && parent.children.push(node);
1789
1686
  } else
1790
1687
  root.push(node);
1791
- } else if (attributes.type === "string" && content && content.includes(closingTag))
1792
- if (node.text = unescapeXml(content.replace(closingTag, "")), stack.length > 0) {
1793
- const parent = stack[stack.length - 1];
1794
- parent && parent.children.push(node);
1795
- } else
1796
- root.push(node);
1797
- else if (attributes.type === "string" && content && !content.includes(closingTag)) {
1688
+ } else if (attributes.type === "string" && content && !content.includes(closingTag)) {
1798
1689
  const contentLines = [content];
1799
1690
  for (i++; i < lines.length; ) {
1800
1691
  const currentLine = lines[i];
1801
1692
  if (currentLine.includes(closingTag)) {
1802
- const closingIndex = currentLine.indexOf(closingTag), remainingContent = currentLine.substring(0, closingIndex);
1693
+ const closingIndex = currentLine.indexOf(closingTag), remainingContent = currentLine.substring(0, closingIndex).trimEnd();
1803
1694
  remainingContent && contentLines.push(remainingContent);
1695
+ const after = currentLine.substring(closingIndex + closingTag.length).trim();
1696
+ after && (lines[i] = after, i--);
1804
1697
  break;
1805
1698
  }
1806
1699
  contentLines.push(currentLine), i++;
@@ -1815,8 +1708,13 @@ const parseReviewFormat = (text, originalDoc) => {
1815
1708
  const contentLines = [];
1816
1709
  for (i++; i < lines.length; ) {
1817
1710
  const currentLine = lines[i];
1818
- if (currentLine.trim() === "</field>")
1711
+ if (currentLine.includes("</field>")) {
1712
+ const idx = currentLine.indexOf("</field>"), before = currentLine.substring(0, idx).trimEnd();
1713
+ before.trim() && contentLines.push(before);
1714
+ const after = currentLine.substring(idx + 8).trim();
1715
+ after && (lines[i] = after, i--);
1819
1716
  break;
1717
+ }
1820
1718
  contentLines.push(currentLine), i++;
1821
1719
  }
1822
1720
  if (node.text = contentLines.join(`
@@ -1829,32 +1727,36 @@ const parseReviewFormat = (text, originalDoc) => {
1829
1727
  stack.push(node);
1830
1728
  continue;
1831
1729
  }
1832
- const closeMatch = trimmed.match(/^<\/(field|block|span)>/);
1833
- if (closeMatch) {
1834
- if (stack.length === 0) {
1835
- devLog.warn("Closing tag found but stack is empty", {
1730
+ if (trimmed.match(/^<\/(field|block|span)>/)) {
1731
+ let afterClosingTag = trimmed;
1732
+ for (; ; ) {
1733
+ const match = afterClosingTag.match(/^<\/(field|block|span)>/);
1734
+ if (!match) break;
1735
+ if (stack.length === 0) {
1736
+ devLog.warn("Closing tag found but stack is empty", {
1737
+ line: i,
1738
+ tagName: match[1],
1739
+ trimmed: afterClosingTag
1740
+ }), afterClosingTag = afterClosingTag.substring(match[0].length).trim(), afterClosingTag = afterClosingTag.replace(/<!--[\s\S]*?-->/g, "").trim();
1741
+ continue;
1742
+ }
1743
+ matchedPatternCount++, devLog.debug("Matched closing tag", {
1836
1744
  line: i,
1837
- tagName: closeMatch[1],
1838
- trimmed
1745
+ tagName: match[1],
1746
+ trimmed: afterClosingTag,
1747
+ stackLength: stack.length,
1748
+ stackTopName: stack[stack.length - 1]?.name
1839
1749
  });
1840
- continue;
1750
+ const node = stack.pop();
1751
+ if (node)
1752
+ if (stack.length > 0) {
1753
+ const parent = stack[stack.length - 1];
1754
+ parent && parent.children.push(node);
1755
+ } else
1756
+ root.push(node);
1757
+ afterClosingTag = afterClosingTag.substring(match[0].length).trim(), afterClosingTag = afterClosingTag.replace(/<!--[\s\S]*?-->/g, "").trim();
1841
1758
  }
1842
- matchedPatternCount++, devLog.debug("Matched closing tag", {
1843
- line: i,
1844
- tagName: closeMatch[1],
1845
- trimmed,
1846
- stackLength: stack.length,
1847
- stackTopName: stack[stack.length - 1]?.name
1848
- });
1849
- const node = stack.pop();
1850
- if (node)
1851
- if (stack.length > 0) {
1852
- const parent = stack[stack.length - 1];
1853
- parent && parent.children.push(node);
1854
- } else
1855
- root.push(node);
1856
- let afterClosingTag = trimmed.substring(closeMatch[0].length).trim();
1857
- if (afterClosingTag = afterClosingTag.replace(/<!--[\s\S]*?-->/g, "").trim(), afterClosingTag) {
1759
+ if (afterClosingTag) {
1858
1760
  const remainingOpenMatch = afterClosingTag.match(/^<(field|block|span)([^>]*)>(.*)$/);
1859
1761
  if (remainingOpenMatch) {
1860
1762
  const [, tagName, attrsStr, content] = remainingOpenMatch, attributes = parseAttributes(attrsStr), remainingNode = {
@@ -1864,13 +1766,17 @@ const parseReviewFormat = (text, originalDoc) => {
1864
1766
  attributes,
1865
1767
  children: []
1866
1768
  }, remainingClosingTag = `</${tagName}>`;
1867
- if (content.includes(remainingClosingTag))
1868
- if (remainingNode.text = unescapeXml(content.replace(remainingClosingTag, "")), stack.length > 0) {
1769
+ if (content.includes(remainingClosingTag)) {
1770
+ const closeIdx = content.indexOf(remainingClosingTag);
1771
+ remainingNode.text = unescapeXml(content.substring(0, closeIdx));
1772
+ const remainder = content.substring(closeIdx + remainingClosingTag.length).trim();
1773
+ if (stack.length > 0) {
1869
1774
  const parent = stack[stack.length - 1];
1870
1775
  parent && parent.children.push(remainingNode);
1871
1776
  } else
1872
1777
  root.push(remainingNode);
1873
- else if (attributes.type === "string" && content)
1778
+ remainder && (lines[i] = remainder, i--);
1779
+ } else if (attributes.type === "string" && content)
1874
1780
  if (remainingNode.text = unescapeXml(content), stack.length > 0) {
1875
1781
  const parent = stack[stack.length - 1];
1876
1782
  parent && parent.children.push(remainingNode);
@@ -1880,8 +1786,13 @@ const parseReviewFormat = (text, originalDoc) => {
1880
1786
  const contentLines = [];
1881
1787
  for (i++; i < lines.length; ) {
1882
1788
  const currentLine = lines[i];
1883
- if (currentLine.trim() === `</${tagName}>` || currentLine.trim().startsWith(`</${tagName}>`))
1789
+ if (currentLine.includes(`</${tagName}>`)) {
1790
+ const idx = currentLine.indexOf(`</${tagName}>`), before = currentLine.substring(0, idx).trimEnd();
1791
+ before.trim() && contentLines.push(before);
1792
+ const after = currentLine.substring(idx + `</${tagName}>`.length).trim();
1793
+ after && (lines[i] = after, i--);
1884
1794
  break;
1795
+ }
1885
1796
  contentLines.push(currentLine), i++;
1886
1797
  }
1887
1798
  if (remainingNode.text = contentLines.join(`
@@ -1892,7 +1803,8 @@ const parseReviewFormat = (text, originalDoc) => {
1892
1803
  root.push(remainingNode);
1893
1804
  } else
1894
1805
  stack.push(remainingNode);
1895
- }
1806
+ } else
1807
+ lines[i] = afterClosingTag, i--;
1896
1808
  }
1897
1809
  continue;
1898
1810
  }
@@ -2011,19 +1923,14 @@ const parseReviewFormat = (text, originalDoc) => {
2011
1923
  }
2012
1924
  if (node.text !== void 0 && !node.attributes.readonly) {
2013
1925
  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
- });
1926
+ existingValue !== void 0 && typeof existingValue == "string" ? set(doc, path, node.text) : existingValue !== void 0 ? devLog.debug("Skipping field - existing value is not a string (would corrupt structure)", {
1927
+ path: path.join("."),
1928
+ existingType: typeof existingValue,
1929
+ attemptedValue: node.text?.substring(0, 50)
1930
+ }) : devLog.debug("Skipping field not present in original document", {
1931
+ path: path.join("."),
1932
+ attemptedValue: node.text
1933
+ });
2027
1934
  }
2028
1935
  }, applyBlockChanges = (block, blockNode) => {
2029
1936
  if (blockNode.attributes._key && block._key !== blockNode.attributes._key) {
@@ -2055,7 +1962,11 @@ const parseReviewFormat = (text, originalDoc) => {
2055
1962
  });
2056
1963
  return;
2057
1964
  }
2058
- fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (fieldName in block ? block[fieldName] = fieldNode.text : devLog.debug("Skipping block field not present in original", {
1965
+ fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (fieldName in block && typeof block[fieldName] == "string" ? block[fieldName] = fieldNode.text : fieldName in block ? devLog.debug("Skipping block field - existing value is not a string (would corrupt structure)", {
1966
+ fieldName,
1967
+ existingType: typeof block[fieldName],
1968
+ attemptedValue: fieldNode.text?.substring(0, 50)
1969
+ }) : devLog.debug("Skipping block field not present in original", {
2059
1970
  fieldName,
2060
1971
  attemptedValue: fieldNode.text
2061
1972
  }));