@braze/sanity-plugin-ai-translation 0.2.6 → 0.2.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/dist/index.d.ts +204 -0
- package/dist/index.js +777 -106
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getPublishedId, pathToString, useClient, defineType, defineField, definePlugin } from "sanity";
|
|
2
2
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { WarningOutlineIcon, SearchIcon, CheckmarkIcon, ErrorOutlineIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
|
|
3
|
+
import { WarningOutlineIcon, SyncIcon, SearchIcon, CheckmarkIcon, ErrorOutlineIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
|
|
4
4
|
import { createContext, useReducer, useRef, useEffect, useContext, useMemo, useState, useCallback } from "react";
|
|
5
5
|
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";
|
|
@@ -122,6 +122,8 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
122
122
|
mode: "new",
|
|
123
123
|
newReleaseTitle: ""
|
|
124
124
|
},
|
|
125
|
+
reconcilingStatus: "idle",
|
|
126
|
+
reconciliationProgress: void 0,
|
|
125
127
|
dispatch: () => {
|
|
126
128
|
}
|
|
127
129
|
}), translationReducer = (context, action) => {
|
|
@@ -207,8 +209,24 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
207
209
|
releaseConfig: {
|
|
208
210
|
...context.releaseConfig,
|
|
209
211
|
conflicts: void 0,
|
|
210
|
-
replaceExisting: void 0
|
|
211
|
-
|
|
212
|
+
replaceExisting: void 0,
|
|
213
|
+
skipConflicts: void 0
|
|
214
|
+
},
|
|
215
|
+
// Reset reconciliation state
|
|
216
|
+
reconcilingStatus: "idle",
|
|
217
|
+
reconciliationProgress: void 0
|
|
218
|
+
};
|
|
219
|
+
case "SET_RECONCILING_STATUS":
|
|
220
|
+
return {
|
|
221
|
+
...context,
|
|
222
|
+
reconcilingStatus: action.payload,
|
|
223
|
+
// Clear progress when returning to idle or on error
|
|
224
|
+
reconciliationProgress: action.payload === "idle" || action.payload === "error" ? void 0 : context.reconciliationProgress
|
|
225
|
+
};
|
|
226
|
+
case "SET_RECONCILIATION_PROGRESS":
|
|
227
|
+
return {
|
|
228
|
+
...context,
|
|
229
|
+
reconciliationProgress: action.payload
|
|
212
230
|
};
|
|
213
231
|
default:
|
|
214
232
|
return context;
|
|
@@ -238,7 +256,10 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
238
256
|
releaseConfig: {
|
|
239
257
|
mode: "new",
|
|
240
258
|
newReleaseTitle: defaultReleaseTitle
|
|
241
|
-
}
|
|
259
|
+
},
|
|
260
|
+
// Reconciliation state for existing releases
|
|
261
|
+
reconcilingStatus: "idle",
|
|
262
|
+
reconciliationProgress: void 0
|
|
242
263
|
};
|
|
243
264
|
activeTranslation && devLog.info("Reconnecting to active translation", {
|
|
244
265
|
documentId: document._id,
|
|
@@ -505,7 +526,7 @@ const DEFAULT_RETRY_OPTIONS = {
|
|
|
505
526
|
logRetries: !0
|
|
506
527
|
};
|
|
507
528
|
function calculateDelay(attempt, options) {
|
|
508
|
-
const delay = options.initialDelayMs *
|
|
529
|
+
const delay = options.initialDelayMs * options.backoffMultiplier ** attempt;
|
|
509
530
|
return Math.min(delay, options.maxDelayMs);
|
|
510
531
|
}
|
|
511
532
|
function sleep(ms) {
|
|
@@ -924,6 +945,85 @@ const fetchTranslationPrompts = async ({
|
|
|
924
945
|
translatedCount: translationMap.size,
|
|
925
946
|
component: "updateReferences"
|
|
926
947
|
}));
|
|
948
|
+
}, updateBackwardReferences = async ({
|
|
949
|
+
client,
|
|
950
|
+
releaseId,
|
|
951
|
+
sourceDocumentIds,
|
|
952
|
+
targetLanguages,
|
|
953
|
+
sourceLanguage,
|
|
954
|
+
languageFieldName,
|
|
955
|
+
excludeDocumentIds
|
|
956
|
+
}) => {
|
|
957
|
+
if (!sourceDocumentIds.length || !targetLanguages.length)
|
|
958
|
+
return;
|
|
959
|
+
const normalizedSourceIds = sourceDocumentIds.map((id) => getPublishedId(id));
|
|
960
|
+
devLog.debug("Starting backward reference update", {
|
|
961
|
+
releaseId,
|
|
962
|
+
sourceDocumentCount: normalizedSourceIds.length,
|
|
963
|
+
targetLanguages,
|
|
964
|
+
excludeCount: excludeDocumentIds.length,
|
|
965
|
+
component: "updateBackwardReferences"
|
|
966
|
+
});
|
|
967
|
+
try {
|
|
968
|
+
for (const targetLanguage of targetLanguages) {
|
|
969
|
+
const existingDocsWithStaleRefs = await client.fetch(
|
|
970
|
+
`*[
|
|
971
|
+
_id match "versions." + $releaseId + ".*" &&
|
|
972
|
+
@[$languageFieldName] == $targetLanguage &&
|
|
973
|
+
!(_id in $excludeIds)
|
|
974
|
+
] {
|
|
975
|
+
_id,
|
|
976
|
+
"hasStaleRefs": count(*[
|
|
977
|
+
_id in $sourceDocumentIds &&
|
|
978
|
+
^references(@._id)
|
|
979
|
+
]) > 0
|
|
980
|
+
}[hasStaleRefs == true]`,
|
|
981
|
+
{
|
|
982
|
+
releaseId,
|
|
983
|
+
targetLanguage,
|
|
984
|
+
languageFieldName,
|
|
985
|
+
excludeIds: excludeDocumentIds,
|
|
986
|
+
sourceDocumentIds: normalizedSourceIds
|
|
987
|
+
}
|
|
988
|
+
);
|
|
989
|
+
if (existingDocsWithStaleRefs.length === 0) {
|
|
990
|
+
devLog.debug("No existing documents with stale references found", {
|
|
991
|
+
releaseId,
|
|
992
|
+
targetLanguage,
|
|
993
|
+
component: "updateBackwardReferences"
|
|
994
|
+
});
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
devLog.info("Found existing documents with stale references", {
|
|
998
|
+
releaseId,
|
|
999
|
+
targetLanguage,
|
|
1000
|
+
count: existingDocsWithStaleRefs.length,
|
|
1001
|
+
documentIds: existingDocsWithStaleRefs.map((d) => d._id),
|
|
1002
|
+
component: "updateBackwardReferences"
|
|
1003
|
+
}), await Promise.all(
|
|
1004
|
+
existingDocsWithStaleRefs.map(async ({ _id }) => {
|
|
1005
|
+
await localizeReferences({
|
|
1006
|
+
client,
|
|
1007
|
+
documentId: _id,
|
|
1008
|
+
sourceLanguage,
|
|
1009
|
+
languageFieldName
|
|
1010
|
+
});
|
|
1011
|
+
})
|
|
1012
|
+
), devLog.info("Updated backward references", {
|
|
1013
|
+
releaseId,
|
|
1014
|
+
targetLanguage,
|
|
1015
|
+
updatedCount: existingDocsWithStaleRefs.length,
|
|
1016
|
+
component: "updateBackwardReferences"
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
devLog.error("Error updating backward references", error instanceof Error ? error : new Error(String(error)), {
|
|
1021
|
+
releaseId,
|
|
1022
|
+
sourceDocumentIds: normalizedSourceIds,
|
|
1023
|
+
targetLanguages,
|
|
1024
|
+
component: "updateBackwardReferences"
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
927
1027
|
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages, mainSourceDocumentId) => {
|
|
928
1028
|
for (const sourceDocument of sourceDocuments)
|
|
929
1029
|
for (const lang of targetLanguages) {
|
|
@@ -939,6 +1039,57 @@ const fetchTranslationPrompts = async ({
|
|
|
939
1039
|
payload
|
|
940
1040
|
}), mainSourceDocumentId && updateActiveTranslationJob(mainSourceDocumentId, payload);
|
|
941
1041
|
}
|
|
1042
|
+
}, emitTelemetryEvent = (config, event) => {
|
|
1043
|
+
if (config?.onEvent)
|
|
1044
|
+
try {
|
|
1045
|
+
config.onEvent(event), devLog.debug("Telemetry event emitted", { eventName: event.name });
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
devLog.warn("Failed to emit telemetry event", {
|
|
1048
|
+
eventName: event.name,
|
|
1049
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
}, emitTelemetryError = (config, bundle) => {
|
|
1053
|
+
if (config?.onError)
|
|
1054
|
+
try {
|
|
1055
|
+
config.onError(bundle), devLog.debug("Telemetry error emitted", {
|
|
1056
|
+
documentId: bundle.documentId,
|
|
1057
|
+
errorType: bundle.error.type
|
|
1058
|
+
});
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
devLog.warn("Failed to emit telemetry error", {
|
|
1061
|
+
documentId: bundle.documentId,
|
|
1062
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}, buildErrorBundle = (params) => {
|
|
1066
|
+
const durationMs = Date.now() - params.startTime;
|
|
1067
|
+
return {
|
|
1068
|
+
documentId: params.documentId,
|
|
1069
|
+
documentType: params.documentType,
|
|
1070
|
+
targetLanguage: params.targetLanguage,
|
|
1071
|
+
sourceLanguage: params.sourceLanguage,
|
|
1072
|
+
translationMode: params.translationMode,
|
|
1073
|
+
error: {
|
|
1074
|
+
message: params.error.message,
|
|
1075
|
+
stack: params.error.stack,
|
|
1076
|
+
type: params.errorType
|
|
1077
|
+
},
|
|
1078
|
+
debug: {
|
|
1079
|
+
originalDocument: params.originalDocument,
|
|
1080
|
+
serializedDocument: params.serializedDocument,
|
|
1081
|
+
openaiResponse: params.openaiResponse,
|
|
1082
|
+
translationPrompts: params.translationPrompts,
|
|
1083
|
+
validationErrors: params.validationErrors
|
|
1084
|
+
},
|
|
1085
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1086
|
+
durationMs
|
|
1087
|
+
};
|
|
1088
|
+
}, createTimestamp = () => (/* @__PURE__ */ new Date()).toISOString(), classifyError = (error, context) => {
|
|
1089
|
+
if (context)
|
|
1090
|
+
return context;
|
|
1091
|
+
const message = error.message.toLowerCase();
|
|
1092
|
+
return message.includes("network") || message.includes("fetch") || message.includes("timeout") || message.includes("econnrefused") || message.includes("enotfound") ? "network" : message.includes("openai") || message.includes("gpt") || message.includes("api error") ? "openai_api" : message.includes("validation") || message.includes("structure") || message.includes("invalid") ? "validation" : message.includes("deserialize") || message.includes("parse") ? "deserialization" : message.includes("serialize") || message.includes("xml") ? "serialization" : message.includes("agent") || message.includes("sanity") ? "sanity_agent" : "unknown";
|
|
942
1093
|
}, updateTranslationMetadata = async ({
|
|
943
1094
|
client,
|
|
944
1095
|
targetLanguage,
|
|
@@ -1649,25 +1800,27 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1649
1800
|
attributes,
|
|
1650
1801
|
children: []
|
|
1651
1802
|
}, closingTag = `</${tagName}>`;
|
|
1652
|
-
if (content.
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
} else
|
|
1658
|
-
root.push(node);
|
|
1659
|
-
} else if (content.includes(closingTag))
|
|
1660
|
-
if (node.text = unescapeXml(content.replace(closingTag, "")), stack.length > 0) {
|
|
1803
|
+
if (content.includes(closingTag)) {
|
|
1804
|
+
const closeIdx = content.indexOf(closingTag);
|
|
1805
|
+
node.text = unescapeXml(content.substring(0, closeIdx));
|
|
1806
|
+
const remainder = content.substring(closeIdx + closingTag.length).trim();
|
|
1807
|
+
if (stack.length > 0) {
|
|
1661
1808
|
const parent = stack[stack.length - 1];
|
|
1662
1809
|
parent && parent.children.push(node);
|
|
1663
1810
|
} else
|
|
1664
1811
|
root.push(node);
|
|
1665
|
-
|
|
1812
|
+
remainder && (lines[i] = remainder, i--);
|
|
1813
|
+
} else if (attributes.type && typeof attributes.type == "string" && attributes.type.startsWith("translatable.")) {
|
|
1666
1814
|
const contentLines = [];
|
|
1667
1815
|
for (i++; i < lines.length; ) {
|
|
1668
1816
|
const currentLine = lines[i];
|
|
1669
|
-
if (currentLine.
|
|
1817
|
+
if (currentLine.includes("</field>")) {
|
|
1818
|
+
const idx = currentLine.indexOf("</field>"), before = currentLine.substring(0, idx).trimEnd();
|
|
1819
|
+
before.trim() && contentLines.push(before);
|
|
1820
|
+
const after = currentLine.substring(idx + 8).trim();
|
|
1821
|
+
after && (lines[i] = after, i--);
|
|
1670
1822
|
break;
|
|
1823
|
+
}
|
|
1671
1824
|
contentLines.push(currentLine), i++;
|
|
1672
1825
|
}
|
|
1673
1826
|
if (node.text = contentLines.join(`
|
|
@@ -1676,18 +1829,36 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1676
1829
|
parent && parent.children.push(node);
|
|
1677
1830
|
} else
|
|
1678
1831
|
root.push(node);
|
|
1679
|
-
} else if (attributes.type === "string" && content && content.includes(closingTag))
|
|
1680
|
-
|
|
1832
|
+
} else if (attributes.type === "string" && content && !content.includes(closingTag)) {
|
|
1833
|
+
const contentLines = [content];
|
|
1834
|
+
for (i++; i < lines.length; ) {
|
|
1835
|
+
const currentLine = lines[i];
|
|
1836
|
+
if (currentLine.includes(closingTag)) {
|
|
1837
|
+
const closingIndex = currentLine.indexOf(closingTag), remainingContent = currentLine.substring(0, closingIndex).trimEnd();
|
|
1838
|
+
remainingContent && contentLines.push(remainingContent);
|
|
1839
|
+
const after = currentLine.substring(closingIndex + closingTag.length).trim();
|
|
1840
|
+
after && (lines[i] = after, i--);
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1843
|
+
contentLines.push(currentLine), i++;
|
|
1844
|
+
}
|
|
1845
|
+
if (node.text = unescapeXml(contentLines.join(`
|
|
1846
|
+
`)), stack.length > 0) {
|
|
1681
1847
|
const parent = stack[stack.length - 1];
|
|
1682
1848
|
parent && parent.children.push(node);
|
|
1683
1849
|
} else
|
|
1684
1850
|
root.push(node);
|
|
1685
|
-
else if (attributes.type === "string" && !content) {
|
|
1851
|
+
} else if (attributes.type === "string" && !content) {
|
|
1686
1852
|
const contentLines = [];
|
|
1687
1853
|
for (i++; i < lines.length; ) {
|
|
1688
1854
|
const currentLine = lines[i];
|
|
1689
|
-
if (currentLine.
|
|
1855
|
+
if (currentLine.includes("</field>")) {
|
|
1856
|
+
const idx = currentLine.indexOf("</field>"), before = currentLine.substring(0, idx).trimEnd();
|
|
1857
|
+
before.trim() && contentLines.push(before);
|
|
1858
|
+
const after = currentLine.substring(idx + 8).trim();
|
|
1859
|
+
after && (lines[i] = after, i--);
|
|
1690
1860
|
break;
|
|
1861
|
+
}
|
|
1691
1862
|
contentLines.push(currentLine), i++;
|
|
1692
1863
|
}
|
|
1693
1864
|
if (node.text = contentLines.join(`
|
|
@@ -1700,25 +1871,122 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1700
1871
|
stack.push(node);
|
|
1701
1872
|
continue;
|
|
1702
1873
|
}
|
|
1703
|
-
if (trimmed.match(/^<\/(field|block|span)
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
if (
|
|
1708
|
-
|
|
1709
|
-
|
|
1874
|
+
if (trimmed.match(/^<\/(field|block|span)>/)) {
|
|
1875
|
+
let afterClosingTag = trimmed;
|
|
1876
|
+
for (; ; ) {
|
|
1877
|
+
const match = afterClosingTag.match(/^<\/(field|block|span)>/);
|
|
1878
|
+
if (!match) break;
|
|
1879
|
+
if (stack.length === 0) {
|
|
1880
|
+
devLog.warn("Closing tag found but stack is empty", {
|
|
1881
|
+
line: i,
|
|
1882
|
+
tagName: match[1],
|
|
1883
|
+
trimmed: afterClosingTag
|
|
1884
|
+
}), afterClosingTag = afterClosingTag.substring(match[0].length).trim(), afterClosingTag = afterClosingTag.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
matchedPatternCount++, devLog.debug("Matched closing tag", {
|
|
1888
|
+
line: i,
|
|
1889
|
+
tagName: match[1],
|
|
1890
|
+
trimmed: afterClosingTag,
|
|
1891
|
+
stackLength: stack.length,
|
|
1892
|
+
stackTopName: stack[stack.length - 1]?.name
|
|
1893
|
+
});
|
|
1894
|
+
const node = stack.pop();
|
|
1895
|
+
if (node)
|
|
1896
|
+
if (stack.length > 0) {
|
|
1897
|
+
const parent = stack[stack.length - 1];
|
|
1898
|
+
parent && parent.children.push(node);
|
|
1899
|
+
} else
|
|
1900
|
+
root.push(node);
|
|
1901
|
+
afterClosingTag = afterClosingTag.substring(match[0].length).trim(), afterClosingTag = afterClosingTag.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
1902
|
+
}
|
|
1903
|
+
if (afterClosingTag) {
|
|
1904
|
+
const remainingOpenMatch = afterClosingTag.match(/^<(field|block|span)([^>]*)>(.*)$/);
|
|
1905
|
+
if (remainingOpenMatch) {
|
|
1906
|
+
const [, tagName, attrsStr, content] = remainingOpenMatch, attributes = parseAttributes(attrsStr), remainingNode = {
|
|
1907
|
+
type: tagName,
|
|
1908
|
+
name: attributes.name,
|
|
1909
|
+
text: content ? unescapeXml(content) : void 0,
|
|
1910
|
+
attributes,
|
|
1911
|
+
children: []
|
|
1912
|
+
}, remainingClosingTag = `</${tagName}>`;
|
|
1913
|
+
if (content.includes(remainingClosingTag)) {
|
|
1914
|
+
const closeIdx = content.indexOf(remainingClosingTag);
|
|
1915
|
+
remainingNode.text = unescapeXml(content.substring(0, closeIdx));
|
|
1916
|
+
const remainder = content.substring(closeIdx + remainingClosingTag.length).trim();
|
|
1917
|
+
if (stack.length > 0) {
|
|
1918
|
+
const parent = stack[stack.length - 1];
|
|
1919
|
+
parent && parent.children.push(remainingNode);
|
|
1920
|
+
} else
|
|
1921
|
+
root.push(remainingNode);
|
|
1922
|
+
remainder && (lines[i] = remainder, i--);
|
|
1923
|
+
} else if (attributes.type === "string" && content)
|
|
1924
|
+
if (remainingNode.text = unescapeXml(content), stack.length > 0) {
|
|
1925
|
+
const parent = stack[stack.length - 1];
|
|
1926
|
+
parent && parent.children.push(remainingNode);
|
|
1927
|
+
} else
|
|
1928
|
+
root.push(remainingNode);
|
|
1929
|
+
else if (attributes.type === "string" && !content) {
|
|
1930
|
+
const contentLines = [];
|
|
1931
|
+
for (i++; i < lines.length; ) {
|
|
1932
|
+
const currentLine = lines[i];
|
|
1933
|
+
if (currentLine.includes(`</${tagName}>`)) {
|
|
1934
|
+
const idx = currentLine.indexOf(`</${tagName}>`), before = currentLine.substring(0, idx).trimEnd();
|
|
1935
|
+
before.trim() && contentLines.push(before);
|
|
1936
|
+
const after = currentLine.substring(idx + `</${tagName}>`.length).trim();
|
|
1937
|
+
after && (lines[i] = after, i--);
|
|
1938
|
+
break;
|
|
1939
|
+
}
|
|
1940
|
+
contentLines.push(currentLine), i++;
|
|
1941
|
+
}
|
|
1942
|
+
if (remainingNode.text = contentLines.join(`
|
|
1943
|
+
`).trim(), stack.length > 0) {
|
|
1944
|
+
const parent = stack[stack.length - 1];
|
|
1945
|
+
parent && parent.children.push(remainingNode);
|
|
1946
|
+
} else
|
|
1947
|
+
root.push(remainingNode);
|
|
1948
|
+
} else
|
|
1949
|
+
stack.push(remainingNode);
|
|
1710
1950
|
} else
|
|
1711
|
-
|
|
1951
|
+
lines[i] = afterClosingTag, i--;
|
|
1952
|
+
}
|
|
1712
1953
|
continue;
|
|
1713
1954
|
}
|
|
1955
|
+
if (trimmed.startsWith("</")) {
|
|
1956
|
+
const attemptedCloseMatch = trimmed.match(/^<\/(field|block|span)>/);
|
|
1957
|
+
attemptedCloseMatch ? stack.length === 0 && devLog.warn("Closing tag matched but stack is empty (unmatched closing tag)", {
|
|
1958
|
+
line: i,
|
|
1959
|
+
tagName: attemptedCloseMatch[1],
|
|
1960
|
+
trimmed
|
|
1961
|
+
}) : devLog.warn("Line starts with </ but did not match closing tag pattern", {
|
|
1962
|
+
line: i,
|
|
1963
|
+
trimmed,
|
|
1964
|
+
stackLength: stack.length,
|
|
1965
|
+
stackTopName: stack[stack.length - 1]?.name,
|
|
1966
|
+
stackTopType: stack[stack.length - 1]?.type
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1714
1969
|
processedLineCount <= 20 && devLog.debug("Line did not match any pattern", {
|
|
1715
1970
|
line: i,
|
|
1716
1971
|
trimmed,
|
|
1717
1972
|
startsWithLessThan: trimmed.startsWith("<"),
|
|
1718
1973
|
startsWithField: trimmed.startsWith("<field"),
|
|
1719
|
-
startsWithBacktick: trimmed.startsWith("`")
|
|
1974
|
+
startsWithBacktick: trimmed.startsWith("`"),
|
|
1975
|
+
startsWithClosingTag: trimmed.startsWith("</")
|
|
1720
1976
|
});
|
|
1721
1977
|
}
|
|
1978
|
+
if (stack.length > 0)
|
|
1979
|
+
for (devLog.warn("Nodes remaining in stack after parsing complete", {
|
|
1980
|
+
stackLength: stack.length,
|
|
1981
|
+
remainingNodes: stack.map((n) => ({
|
|
1982
|
+
type: n.type,
|
|
1983
|
+
name: n.name,
|
|
1984
|
+
attributes: n.attributes
|
|
1985
|
+
}))
|
|
1986
|
+
}); stack.length > 0; ) {
|
|
1987
|
+
const node = stack.pop();
|
|
1988
|
+
node && root.push(node);
|
|
1989
|
+
}
|
|
1722
1990
|
return devLog.debug("Parse complete", {
|
|
1723
1991
|
totalLines: lines.length,
|
|
1724
1992
|
processedLines: processedLineCount,
|
|
@@ -1766,8 +2034,28 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1766
2034
|
return;
|
|
1767
2035
|
}
|
|
1768
2036
|
if (node.attributes.type === "object") {
|
|
1769
|
-
|
|
1770
|
-
|
|
2037
|
+
const currentValue = get(doc, path);
|
|
2038
|
+
if (currentValue && typeof currentValue == "object" && !Array.isArray(currentValue)) {
|
|
2039
|
+
const currentValueObj = currentValue;
|
|
2040
|
+
for (const child of node.children)
|
|
2041
|
+
if (child.name) {
|
|
2042
|
+
const childValue = currentValueObj[child.name];
|
|
2043
|
+
if (/^\d+$/.test(child.name) && childValue === void 0) {
|
|
2044
|
+
devLog.warn("Skipping numeric child name in object field - field does not exist (likely parsing error)", {
|
|
2045
|
+
path: path.join("."),
|
|
2046
|
+
childName: child.name,
|
|
2047
|
+
availableKeys: Object.keys(currentValueObj),
|
|
2048
|
+
childType: child.attributes.type
|
|
2049
|
+
});
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
applyFieldChanges(doc, [...path, child.name], child);
|
|
2053
|
+
}
|
|
2054
|
+
} else
|
|
2055
|
+
devLog.warn("Object field type but value is not an object", {
|
|
2056
|
+
path: path.join("."),
|
|
2057
|
+
actualType: Array.isArray(currentValue) ? "array" : typeof currentValue
|
|
2058
|
+
});
|
|
1771
2059
|
return;
|
|
1772
2060
|
}
|
|
1773
2061
|
if (node.children.some((c) => c.type === "block")) {
|
|
@@ -1777,10 +2065,17 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1777
2065
|
});
|
|
1778
2066
|
return;
|
|
1779
2067
|
}
|
|
1780
|
-
node.text !== void 0 && !node.attributes.readonly
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
2068
|
+
if (node.text !== void 0 && !node.attributes.readonly) {
|
|
2069
|
+
const existingValue = get(doc, path);
|
|
2070
|
+
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)", {
|
|
2071
|
+
path: path.join("."),
|
|
2072
|
+
existingType: typeof existingValue,
|
|
2073
|
+
attemptedValue: node.text?.substring(0, 50)
|
|
2074
|
+
}) : devLog.debug("Skipping field not present in original document", {
|
|
2075
|
+
path: path.join("."),
|
|
2076
|
+
attemptedValue: node.text
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
1784
2079
|
}, applyBlockChanges = (block, blockNode) => {
|
|
1785
2080
|
if (blockNode.attributes._key && block._key !== blockNode.attributes._key) {
|
|
1786
2081
|
devLog.warn("Block key mismatch, skipping", {
|
|
@@ -1811,7 +2106,11 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1811
2106
|
});
|
|
1812
2107
|
return;
|
|
1813
2108
|
}
|
|
1814
|
-
fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (fieldName in block ? block[fieldName] = fieldNode.text : devLog.debug("Skipping block field not
|
|
2109
|
+
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)", {
|
|
2110
|
+
fieldName,
|
|
2111
|
+
existingType: typeof block[fieldName],
|
|
2112
|
+
attemptedValue: fieldNode.text?.substring(0, 50)
|
|
2113
|
+
}) : devLog.debug("Skipping block field not present in original", {
|
|
1815
2114
|
fieldName,
|
|
1816
2115
|
attemptedValue: fieldNode.text
|
|
1817
2116
|
}));
|
|
@@ -1983,16 +2282,18 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1983
2282
|
translationPrompts,
|
|
1984
2283
|
targetLanguage,
|
|
1985
2284
|
openaiConfig,
|
|
1986
|
-
previousError,
|
|
1987
|
-
errorContext
|
|
2285
|
+
previousError: _previousError,
|
|
2286
|
+
errorContext,
|
|
2287
|
+
telemetryConfig
|
|
1988
2288
|
}) => {
|
|
1989
|
-
const sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
2289
|
+
const startTime = Date.now(), sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
1990
2290
|
sourceId: sourceDocument._id,
|
|
1991
2291
|
sourceLanguage
|
|
1992
2292
|
};
|
|
2293
|
+
let serializedDocument, openaiResponseContent;
|
|
1993
2294
|
try {
|
|
1994
2295
|
const sourceText = serializeDocument(sourceDocument);
|
|
1995
|
-
devLog.info("Serialized document for OpenAI", {
|
|
2296
|
+
serializedDocument = sourceText, devLog.info("Serialized document for OpenAI", {
|
|
1996
2297
|
...logContext,
|
|
1997
2298
|
characterCount: sourceText.length,
|
|
1998
2299
|
estimatedTokens: Math.ceil(sourceText.length / 4),
|
|
@@ -2015,7 +2316,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2015
2316
|
model: openaiConfig.model,
|
|
2016
2317
|
reasoningEffort: openaiConfig.reasoningEffort
|
|
2017
2318
|
});
|
|
2018
|
-
devLog.info("OpenAI translation response received", {
|
|
2319
|
+
openaiResponseContent = response.content, devLog.info("OpenAI translation response received", {
|
|
2019
2320
|
...logContext,
|
|
2020
2321
|
tokensUsed: response.tokensUsed,
|
|
2021
2322
|
cost: response.cost,
|
|
@@ -2053,9 +2354,35 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2053
2354
|
})), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
|
|
2054
2355
|
tokensUsed: response.tokensUsed.total,
|
|
2055
2356
|
estimatedCost: response.cost
|
|
2056
|
-
}),
|
|
2357
|
+
}), {
|
|
2358
|
+
document: result,
|
|
2359
|
+
tokensUsed: response.tokensUsed.total,
|
|
2360
|
+
cost: response.cost
|
|
2361
|
+
};
|
|
2057
2362
|
} catch (error) {
|
|
2058
|
-
|
|
2363
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2364
|
+
throw devLog.error("OpenAI translation failed", err, logContext), emitTelemetryError(
|
|
2365
|
+
telemetryConfig,
|
|
2366
|
+
buildErrorBundle({
|
|
2367
|
+
documentId: sourceDocument._id,
|
|
2368
|
+
documentType: sourceDocument._type,
|
|
2369
|
+
targetLanguage,
|
|
2370
|
+
sourceLanguage,
|
|
2371
|
+
translationMode: "openai-only",
|
|
2372
|
+
error: err,
|
|
2373
|
+
errorType: classifyError(err, "openai_api"),
|
|
2374
|
+
startTime,
|
|
2375
|
+
originalDocument: sourceDocument,
|
|
2376
|
+
serializedDocument,
|
|
2377
|
+
openaiResponse: openaiResponseContent,
|
|
2378
|
+
translationPrompts: {
|
|
2379
|
+
globalPrompt: translationPrompts.globalPrompt,
|
|
2380
|
+
localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
|
|
2381
|
+
protectedPhrases: translationPrompts.protectedPhrases,
|
|
2382
|
+
glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms ? [translationPrompts.localeInstructions[targetLanguage].glossaryTerms] : void 0
|
|
2383
|
+
}
|
|
2384
|
+
})
|
|
2385
|
+
), error;
|
|
2059
2386
|
}
|
|
2060
2387
|
}, buildTranslationPrompt$1 = ({
|
|
2061
2388
|
sourceText,
|
|
@@ -2291,9 +2618,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2291
2618
|
releaseId,
|
|
2292
2619
|
excludeFields = [],
|
|
2293
2620
|
replaceExisting = !1,
|
|
2294
|
-
mainSourceDocumentId
|
|
2621
|
+
mainSourceDocumentId,
|
|
2622
|
+
telemetryConfig
|
|
2295
2623
|
}) => {
|
|
2296
|
-
const safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
|
|
2624
|
+
const startTime = Date.now(), sourceLanguage = sourceDocument[languageFieldName], safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
|
|
2297
2625
|
excludeFields.length !== safeExcludeFields.length && devLog.warn("Language field was in excludeFields - removing it to prevent translation errors", {
|
|
2298
2626
|
documentId: sourceDocument._id,
|
|
2299
2627
|
languageFieldName,
|
|
@@ -2332,29 +2660,55 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2332
2660
|
documentId: sourceDocument._id,
|
|
2333
2661
|
documentType: sourceDocument._type
|
|
2334
2662
|
}), documentToTranslate = filteredDocument;
|
|
2335
|
-
let translatedDoc, previousError = null;
|
|
2663
|
+
let translatedDoc, translationMetrics = {}, previousError = null;
|
|
2336
2664
|
const performTranslation = async (errorContext) => {
|
|
2337
2665
|
switch (translationMode) {
|
|
2338
|
-
case "sanity-only":
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2666
|
+
case "sanity-only": {
|
|
2667
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating", void 0, mainSourceDocumentId);
|
|
2668
|
+
try {
|
|
2669
|
+
return { document: await getSanityAgentTranslation({
|
|
2670
|
+
sourceDocument: documentToTranslate,
|
|
2671
|
+
targetLanguage,
|
|
2672
|
+
languageFieldName,
|
|
2673
|
+
schemaId,
|
|
2674
|
+
client,
|
|
2675
|
+
translationPrompts
|
|
2676
|
+
}) };
|
|
2677
|
+
} catch (error) {
|
|
2678
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2679
|
+
throw emitTelemetryError(
|
|
2680
|
+
telemetryConfig,
|
|
2681
|
+
buildErrorBundle({
|
|
2682
|
+
documentId: sourceDocument._id,
|
|
2683
|
+
documentType: sourceDocument._type,
|
|
2684
|
+
targetLanguage,
|
|
2685
|
+
sourceLanguage,
|
|
2686
|
+
translationMode: "sanity-only",
|
|
2687
|
+
error: err,
|
|
2688
|
+
errorType: classifyError(err, "sanity_agent"),
|
|
2689
|
+
startTime,
|
|
2690
|
+
originalDocument: sourceDocument,
|
|
2691
|
+
translationPrompts: {
|
|
2692
|
+
globalPrompt: translationPrompts.globalPrompt,
|
|
2693
|
+
localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
|
|
2694
|
+
protectedPhrases: translationPrompts.protectedPhrases
|
|
2695
|
+
}
|
|
2696
|
+
})
|
|
2697
|
+
), error;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2347
2700
|
case "openai-only": {
|
|
2348
2701
|
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2349
2702
|
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2350
|
-
|
|
2703
|
+
setJobStatus(
|
|
2351
2704
|
dispatch,
|
|
2352
2705
|
sourceDocument._id,
|
|
2353
2706
|
targetLanguage,
|
|
2354
2707
|
"translating_with_openai",
|
|
2355
2708
|
void 0,
|
|
2356
2709
|
mainSourceDocumentId
|
|
2357
|
-
)
|
|
2710
|
+
);
|
|
2711
|
+
const result = await getOpenAITranslation({
|
|
2358
2712
|
dispatch,
|
|
2359
2713
|
sourceDocument: documentToTranslate,
|
|
2360
2714
|
targetLanguage,
|
|
@@ -2362,14 +2716,23 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2362
2716
|
translationPrompts,
|
|
2363
2717
|
openaiConfig,
|
|
2364
2718
|
previousError: errorContext && previousError ? previousError : void 0,
|
|
2365
|
-
errorContext
|
|
2719
|
+
errorContext,
|
|
2720
|
+
telemetryConfig
|
|
2366
2721
|
});
|
|
2722
|
+
return result ? {
|
|
2723
|
+
document: result.document,
|
|
2724
|
+
tokensUsed: result.tokensUsed,
|
|
2725
|
+
cost: result.cost
|
|
2726
|
+
} : { document: null };
|
|
2367
2727
|
}
|
|
2368
2728
|
default:
|
|
2369
2729
|
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2370
2730
|
}
|
|
2371
|
-
};
|
|
2372
|
-
if (translatedDoc =
|
|
2731
|
+
}, translationResult = await performTranslation();
|
|
2732
|
+
if (translatedDoc = translationResult.document, translationMetrics = {
|
|
2733
|
+
tokensUsed: translationResult.tokensUsed,
|
|
2734
|
+
cost: translationResult.cost
|
|
2735
|
+
}, !translatedDoc)
|
|
2373
2736
|
return null;
|
|
2374
2737
|
let transformedResult;
|
|
2375
2738
|
Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
|
|
@@ -2432,9 +2795,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2432
2795
|
targetLanguage,
|
|
2433
2796
|
error: error instanceof Error ? error.message : String(error)
|
|
2434
2797
|
}), previousError = error instanceof Error ? error : new Error(String(error));
|
|
2435
|
-
const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure
|
|
2436
|
-
if (translatedDoc =
|
|
2798
|
+
const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure.`, retryResult = await performTranslation(errorContext);
|
|
2799
|
+
if (translatedDoc = retryResult.document, !translatedDoc)
|
|
2437
2800
|
throw new Error("Translation retry failed: no document returned");
|
|
2801
|
+
translationMetrics = {
|
|
2802
|
+
tokensUsed: retryResult.tokensUsed,
|
|
2803
|
+
cost: retryResult.cost
|
|
2804
|
+
};
|
|
2438
2805
|
let retryTransformedResult;
|
|
2439
2806
|
Object.keys(originalStructure).length > 0 ? retryTransformedResult = restoreTransforms({
|
|
2440
2807
|
translatedDocument: translatedDoc,
|
|
@@ -2465,11 +2832,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2465
2832
|
isRetry: !0
|
|
2466
2833
|
}
|
|
2467
2834
|
}
|
|
2468
|
-
),
|
|
2835
|
+
), {
|
|
2836
|
+
document: retryNewDoc,
|
|
2837
|
+
tokensUsed: translationMetrics.tokensUsed,
|
|
2838
|
+
cost: translationMetrics.cost
|
|
2839
|
+
};
|
|
2469
2840
|
}
|
|
2470
2841
|
throw error;
|
|
2471
2842
|
}
|
|
2472
|
-
return
|
|
2843
|
+
return {
|
|
2844
|
+
document: newDoc,
|
|
2845
|
+
tokensUsed: translationMetrics.tokensUsed,
|
|
2846
|
+
cost: translationMetrics.cost
|
|
2847
|
+
};
|
|
2473
2848
|
}, translateDocument = async ({
|
|
2474
2849
|
dispatch,
|
|
2475
2850
|
client,
|
|
@@ -2484,9 +2859,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2484
2859
|
releaseId,
|
|
2485
2860
|
excludeFields,
|
|
2486
2861
|
replaceExisting,
|
|
2487
|
-
mainSourceDocumentId
|
|
2862
|
+
mainSourceDocumentId,
|
|
2863
|
+
telemetryConfig
|
|
2488
2864
|
}) => {
|
|
2489
|
-
const logContext = {
|
|
2865
|
+
const jobStartTime = Date.now(), logContext = {
|
|
2490
2866
|
documentId: sourceDocument._id,
|
|
2491
2867
|
targetLanguage,
|
|
2492
2868
|
schemaId,
|
|
@@ -2498,7 +2874,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2498
2874
|
client,
|
|
2499
2875
|
sourceDocument,
|
|
2500
2876
|
sourceLanguage
|
|
2501
|
-
}), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage),
|
|
2877
|
+
}), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), translationResult = await handleTranslation({
|
|
2502
2878
|
dispatch,
|
|
2503
2879
|
client,
|
|
2504
2880
|
schemaId,
|
|
@@ -2512,8 +2888,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2512
2888
|
releaseId,
|
|
2513
2889
|
excludeFields,
|
|
2514
2890
|
replaceExisting,
|
|
2515
|
-
mainSourceDocumentId
|
|
2516
|
-
|
|
2891
|
+
mainSourceDocumentId,
|
|
2892
|
+
telemetryConfig
|
|
2893
|
+
}), newDocument = translationResult?.document;
|
|
2517
2894
|
if (!newDocument) {
|
|
2518
2895
|
devLog.error("Translation failed: no document returned", new Error("Translation returned null"), logContext), setJobStatus(
|
|
2519
2896
|
dispatch,
|
|
@@ -2550,12 +2927,27 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2550
2927
|
targetLanguage,
|
|
2551
2928
|
"completed",
|
|
2552
2929
|
{
|
|
2553
|
-
targetId: newDocument._id
|
|
2930
|
+
targetId: newDocument._id,
|
|
2931
|
+
tokensUsed: translationResult?.tokensUsed,
|
|
2932
|
+
estimatedCost: translationResult?.cost
|
|
2554
2933
|
},
|
|
2555
2934
|
mainSourceDocumentId
|
|
2556
2935
|
), devLog.info("Translation completed successfully", {
|
|
2557
2936
|
...logContext,
|
|
2558
2937
|
newDocumentId: newDocument._id
|
|
2938
|
+
}), emitTelemetryEvent(telemetryConfig, {
|
|
2939
|
+
name: "translation.job_completed",
|
|
2940
|
+
timestamp: createTimestamp(),
|
|
2941
|
+
data: {
|
|
2942
|
+
documentId: sourceDocument._id,
|
|
2943
|
+
documentType: sourceDocument._type,
|
|
2944
|
+
sourceLanguage,
|
|
2945
|
+
targetLanguage,
|
|
2946
|
+
translationMode,
|
|
2947
|
+
durationMs: Date.now() - jobStartTime,
|
|
2948
|
+
tokensUsed: translationResult?.tokensUsed,
|
|
2949
|
+
estimatedCost: translationResult?.cost
|
|
2950
|
+
}
|
|
2559
2951
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
2560
2952
|
} catch (error) {
|
|
2561
2953
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -2583,9 +2975,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2583
2975
|
translationMode,
|
|
2584
2976
|
openaiConfig,
|
|
2585
2977
|
excludeFields,
|
|
2586
|
-
releaseConfig
|
|
2978
|
+
releaseConfig,
|
|
2979
|
+
telemetryConfig
|
|
2587
2980
|
}) => {
|
|
2588
|
-
const logContext = {
|
|
2981
|
+
const batchStartTime = Date.now(), logContext = {
|
|
2589
2982
|
documentId: sourceDocument._id,
|
|
2590
2983
|
documentRev: sourceDocument._rev,
|
|
2591
2984
|
targetLanguages,
|
|
@@ -2599,7 +2992,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2599
2992
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
2600
2993
|
if (!sourceLanguage)
|
|
2601
2994
|
throw devLog.error("Source language not found"), new Error("Source language not found");
|
|
2602
|
-
if (
|
|
2995
|
+
if (emitTelemetryEvent(telemetryConfig, {
|
|
2996
|
+
name: "translation.batch_started",
|
|
2997
|
+
timestamp: createTimestamp(),
|
|
2998
|
+
data: {
|
|
2999
|
+
documentId: sourceDocument._id,
|
|
3000
|
+
documentType: sourceDocument._type,
|
|
3001
|
+
sourceLanguage,
|
|
3002
|
+
targetLanguages,
|
|
3003
|
+
translationMode,
|
|
3004
|
+
referenceCount: references.length,
|
|
3005
|
+
releaseMode: releaseConfig.mode
|
|
3006
|
+
}
|
|
3007
|
+
}), maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
2603
3008
|
type: "SET_OVERALL_STATUS",
|
|
2604
3009
|
payload: "translating"
|
|
2605
3010
|
}), releaseConfig.mode === "existing" && releaseConfig.existingReleaseId)
|
|
@@ -2649,8 +3054,28 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2649
3054
|
sourceDocument
|
|
2650
3055
|
});
|
|
2651
3056
|
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages, sourceDocument._id);
|
|
2652
|
-
const
|
|
3057
|
+
const shouldSkipConflict = (documentId, targetLanguage) => !releaseConfig.skipConflicts || releaseConfig.skipConflicts.length === 0 ? !1 : releaseConfig.skipConflicts.some(
|
|
3058
|
+
(conflict) => conflict.sourceDocumentId === documentId && conflict.targetLanguage === targetLanguage
|
|
3059
|
+
), skippedCount = sourceDocumentIds.reduce((count, documentId) => count + targetLanguages.filter((lang) => shouldSkipConflict(documentId, lang)).length, 0), translationTasks = sourceDocumentIds.flatMap(
|
|
2653
3060
|
(documentId) => targetLanguages.map(async (targetLanguage) => {
|
|
3061
|
+
if (shouldSkipConflict(documentId, targetLanguage)) {
|
|
3062
|
+
const docInfo = sourceDocuments.find((d) => d._id === documentId);
|
|
3063
|
+
devLog.info("Skipping conflicting translation", {
|
|
3064
|
+
documentId,
|
|
3065
|
+
targetLanguage,
|
|
3066
|
+
reason: "User chose to skip conflicts"
|
|
3067
|
+
}), dispatch({
|
|
3068
|
+
type: "SET_TRANSLATION_JOB_STATUS",
|
|
3069
|
+
payload: {
|
|
3070
|
+
_id: documentId,
|
|
3071
|
+
_type: docInfo?._type,
|
|
3072
|
+
language: targetLanguage,
|
|
3073
|
+
status: "skipped",
|
|
3074
|
+
title: docInfo?.title
|
|
3075
|
+
}
|
|
3076
|
+
});
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
2654
3079
|
try {
|
|
2655
3080
|
await translationSemaphore.withPermit(async () => {
|
|
2656
3081
|
const document = await client.withConfig({
|
|
@@ -2672,7 +3097,8 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2672
3097
|
excludeFields,
|
|
2673
3098
|
replaceExisting: releaseConfig.replaceExisting,
|
|
2674
3099
|
// Pass main source document ID for active translations tracking
|
|
2675
|
-
mainSourceDocumentId: sourceDocument._id
|
|
3100
|
+
mainSourceDocumentId: sourceDocument._id,
|
|
3101
|
+
telemetryConfig
|
|
2676
3102
|
});
|
|
2677
3103
|
versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
|
|
2678
3104
|
}
|
|
@@ -2689,13 +3115,28 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2689
3115
|
);
|
|
2690
3116
|
}
|
|
2691
3117
|
})
|
|
2692
|
-
), results = await Promise.allSettled(translationTasks), succeeded = results.filter((r) => r.status === "fulfilled").length, failed = results.filter((r) => r.status === "rejected").length;
|
|
3118
|
+
), results = await Promise.allSettled(translationTasks), succeeded = results.filter((r) => r.status === "fulfilled").length - skippedCount, failed = results.filter((r) => r.status === "rejected").length;
|
|
2693
3119
|
devLog.info("Translation tasks completed", {
|
|
2694
3120
|
...logContext,
|
|
2695
3121
|
total: results.length,
|
|
2696
3122
|
succeeded,
|
|
2697
3123
|
failed,
|
|
3124
|
+
skipped: skippedCount,
|
|
2698
3125
|
targetDocuments: versionedTargetDocumentIds.length
|
|
3126
|
+
}), emitTelemetryEvent(telemetryConfig, {
|
|
3127
|
+
name: "translation.batch_completed",
|
|
3128
|
+
timestamp: createTimestamp(),
|
|
3129
|
+
data: {
|
|
3130
|
+
documentId: sourceDocument._id,
|
|
3131
|
+
documentType: sourceDocument._type,
|
|
3132
|
+
sourceLanguage,
|
|
3133
|
+
targetLanguages,
|
|
3134
|
+
translationMode,
|
|
3135
|
+
succeeded,
|
|
3136
|
+
failed,
|
|
3137
|
+
skipped: skippedCount,
|
|
3138
|
+
durationMs: Date.now() - batchStartTime
|
|
3139
|
+
}
|
|
2699
3140
|
}), dispatch({
|
|
2700
3141
|
type: "SET_OVERALL_STATUS",
|
|
2701
3142
|
payload: "linking_references"
|
|
@@ -2708,7 +3149,15 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2708
3149
|
languageFieldName
|
|
2709
3150
|
});
|
|
2710
3151
|
})
|
|
2711
|
-
)
|
|
3152
|
+
), releaseConfig.mode === "existing" && releaseId && await updateBackwardReferences({
|
|
3153
|
+
client,
|
|
3154
|
+
releaseId,
|
|
3155
|
+
sourceDocumentIds,
|
|
3156
|
+
targetLanguages,
|
|
3157
|
+
sourceLanguage,
|
|
3158
|
+
languageFieldName,
|
|
3159
|
+
excludeDocumentIds: versionedTargetDocumentIds
|
|
3160
|
+
});
|
|
2712
3161
|
const hasAnyFailures = failed > 0;
|
|
2713
3162
|
hasAnyFailures && succeeded === 0 ? (devLog.error("All translation tasks failed", void 0, logContext), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id)) : hasAnyFailures ? (devLog.warn("Translation completed with some failures", {
|
|
2714
3163
|
...logContext,
|
|
@@ -2735,8 +3184,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2735
3184
|
}
|
|
2736
3185
|
throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id), error;
|
|
2737
3186
|
}
|
|
2738
|
-
}, ReleaseConflictWarning = ({
|
|
2739
|
-
|
|
3187
|
+
}, ReleaseConflictWarning = ({
|
|
3188
|
+
conflicts,
|
|
3189
|
+
totalTranslations,
|
|
3190
|
+
onCancel,
|
|
3191
|
+
onReplace,
|
|
3192
|
+
onSkipConflicts
|
|
3193
|
+
}) => {
|
|
3194
|
+
const uniqueDocuments = useMemo(() => new Set(conflicts.map((c) => c.sourceDocumentId)).size, [conflicts]), nonConflictingCount = totalTranslations - conflicts.length, hasNonConflicting = nonConflictingCount > 0, summaryText = useMemo(() => conflicts.length === 1 ? "1 translation" : uniqueDocuments === 1 ? `${conflicts.length} translations` : `${conflicts.length} translations across ${uniqueDocuments} documents`, [conflicts.length, uniqueDocuments]);
|
|
2740
3195
|
return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2741
3196
|
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
2742
3197
|
/* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
|
|
@@ -2760,13 +3215,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2760
3215
|
" \u2014 Go back and select a different release, or create a new one."
|
|
2761
3216
|
] }),
|
|
2762
3217
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2763
|
-
/* @__PURE__ */ jsx("strong", { children: "Replace" }),
|
|
3218
|
+
/* @__PURE__ */ jsx("strong", { children: "Replace All" }),
|
|
2764
3219
|
" \u2014 Delete the existing translations in this release and create new ones."
|
|
3220
|
+
] }),
|
|
3221
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3222
|
+
/* @__PURE__ */ jsx("strong", { children: "Skip Conflicts" }),
|
|
3223
|
+
" \u2014 Translate only the ",
|
|
3224
|
+
nonConflictingCount,
|
|
3225
|
+
" non-conflicting translation",
|
|
3226
|
+
nonConflictingCount !== 1 ? "s" : "",
|
|
3227
|
+
", leaving existing ones unchanged.",
|
|
3228
|
+
!hasNonConflicting && " (All translations have conflicts)"
|
|
2765
3229
|
] })
|
|
2766
3230
|
] }),
|
|
2767
|
-
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "
|
|
3231
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "space-between", children: [
|
|
2768
3232
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
|
|
2769
|
-
/* @__PURE__ */
|
|
3233
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", children: [
|
|
3234
|
+
/* @__PURE__ */ jsx(Button, { mode: "bleed", tone: "caution", text: "Replace All", onClick: onReplace }),
|
|
3235
|
+
/* @__PURE__ */ jsx(
|
|
3236
|
+
Button,
|
|
3237
|
+
{
|
|
3238
|
+
mode: "ghost",
|
|
3239
|
+
tone: "caution",
|
|
3240
|
+
text: "Skip Conflicts",
|
|
3241
|
+
onClick: onSkipConflicts,
|
|
3242
|
+
disabled: !hasNonConflicting
|
|
3243
|
+
}
|
|
3244
|
+
)
|
|
3245
|
+
] })
|
|
2770
3246
|
] })
|
|
2771
3247
|
] }) });
|
|
2772
3248
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
@@ -2780,12 +3256,12 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2780
3256
|
selectedTargetLanguages,
|
|
2781
3257
|
translationMode,
|
|
2782
3258
|
releaseConfig
|
|
2783
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1), filteredReferences = references.filter((ref) => selectedReferenceIds.has(ref._id));
|
|
3259
|
+
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1), filteredReferences = references.filter((ref) => selectedReferenceIds.has(ref._id)), totalTranslations = useMemo(() => (1 + filteredReferences.length) * selectedTargetLanguages.length, [filteredReferences.length, selectedTargetLanguages.length]);
|
|
2784
3260
|
useEffect(() => {
|
|
2785
3261
|
releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
|
|
2786
3262
|
}, [releaseConfig.mode, conflictContext]);
|
|
2787
3263
|
const startTranslation = useCallback(
|
|
2788
|
-
(replaceExisting, overrideReleaseId) => {
|
|
3264
|
+
(replaceExisting, overrideReleaseId, skipConflicts) => {
|
|
2789
3265
|
setConflictContext(null), translateDocumentSet({
|
|
2790
3266
|
sourceDocument: document,
|
|
2791
3267
|
targetLanguages: selectedTargetLanguages,
|
|
@@ -2802,6 +3278,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2802
3278
|
releaseConfig: {
|
|
2803
3279
|
...releaseConfig,
|
|
2804
3280
|
replaceExisting,
|
|
3281
|
+
skipConflicts,
|
|
2805
3282
|
// Use the override release ID if provided (from conflict context),
|
|
2806
3283
|
// otherwise use the current releaseConfig value
|
|
2807
3284
|
existingReleaseId: overrideReleaseId ?? releaseConfig.existingReleaseId
|
|
@@ -2848,10 +3325,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2848
3325
|
}
|
|
2849
3326
|
}
|
|
2850
3327
|
startTranslation(!1);
|
|
2851
|
-
}, [
|
|
3328
|
+
}, [
|
|
3329
|
+
releaseConfig,
|
|
3330
|
+
client,
|
|
3331
|
+
document,
|
|
3332
|
+
selectedTargetLanguages,
|
|
3333
|
+
config.languageFieldName,
|
|
3334
|
+
startTranslation,
|
|
3335
|
+
filteredReferences
|
|
3336
|
+
]), handleCancelConflict = useCallback(() => {
|
|
2852
3337
|
setConflictContext(null);
|
|
2853
3338
|
}, []), handleReplaceConflict = useCallback(() => {
|
|
2854
3339
|
conflictContext && startTranslation(!0, conflictContext.releaseId);
|
|
3340
|
+
}, [startTranslation, conflictContext]), handleSkipConflicts = useCallback(() => {
|
|
3341
|
+
conflictContext && startTranslation(!1, conflictContext.releaseId, conflictContext.conflicts);
|
|
2855
3342
|
}, [startTranslation, conflictContext]), handleSelectAll = useCallback(() => {
|
|
2856
3343
|
dispatch({
|
|
2857
3344
|
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
@@ -2867,8 +3354,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2867
3354
|
ReleaseConflictWarning,
|
|
2868
3355
|
{
|
|
2869
3356
|
conflicts: conflictContext.conflicts,
|
|
3357
|
+
totalTranslations,
|
|
2870
3358
|
onCancel: handleCancelConflict,
|
|
2871
|
-
onReplace: handleReplaceConflict
|
|
3359
|
+
onReplace: handleReplaceConflict,
|
|
3360
|
+
onSkipConflicts: handleSkipConflicts
|
|
2872
3361
|
}
|
|
2873
3362
|
) }) : /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
|
|
2874
3363
|
/* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
|
|
@@ -2950,8 +3439,183 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2950
3439
|
documentId: document._id
|
|
2951
3440
|
});
|
|
2952
3441
|
return /* @__PURE__ */ jsx(Card, { padding: 4, muted: !0, border: !0, children: /* @__PURE__ */ jsx(Grid, { columns: [1, 2], gap: 3, children: config?.supportedLanguages.map((lang) => /* @__PURE__ */ jsx(LanguageCheckbox, { lang, isLoading: loading }, lang.id)) }) });
|
|
3442
|
+
}, reconcileReleaseReferences = async ({
|
|
3443
|
+
client,
|
|
3444
|
+
releaseId,
|
|
3445
|
+
sourceLanguage,
|
|
3446
|
+
languageFieldName,
|
|
3447
|
+
onProgress
|
|
3448
|
+
}) => {
|
|
3449
|
+
devLog.info("Starting release reference reconciliation", {
|
|
3450
|
+
releaseId,
|
|
3451
|
+
sourceLanguage,
|
|
3452
|
+
component: "reconcileReleaseReferences"
|
|
3453
|
+
});
|
|
3454
|
+
try {
|
|
3455
|
+
const docsInRelease = await client.fetch(
|
|
3456
|
+
`*[_id match "versions." + $releaseId + ".*"] {
|
|
3457
|
+
_id,
|
|
3458
|
+
"language": @[$languageFieldName]
|
|
3459
|
+
}`,
|
|
3460
|
+
{ releaseId, languageFieldName }
|
|
3461
|
+
);
|
|
3462
|
+
if (docsInRelease.length === 0)
|
|
3463
|
+
return devLog.info("No documents found in release", {
|
|
3464
|
+
releaseId,
|
|
3465
|
+
component: "reconcileReleaseReferences"
|
|
3466
|
+
}), { total: 0, updated: 0, errors: 0 };
|
|
3467
|
+
devLog.info("Found documents in release", {
|
|
3468
|
+
releaseId,
|
|
3469
|
+
count: docsInRelease.length,
|
|
3470
|
+
component: "reconcileReleaseReferences"
|
|
3471
|
+
});
|
|
3472
|
+
let updated = 0, errors = 0;
|
|
3473
|
+
for (let i = 0; i < docsInRelease.length; i++) {
|
|
3474
|
+
const doc = docsInRelease[i];
|
|
3475
|
+
onProgress && onProgress(i + 1, docsInRelease.length);
|
|
3476
|
+
try {
|
|
3477
|
+
if (!doc.language) {
|
|
3478
|
+
devLog.warn("Document missing language field, skipping", {
|
|
3479
|
+
documentId: doc._id,
|
|
3480
|
+
component: "reconcileReleaseReferences"
|
|
3481
|
+
});
|
|
3482
|
+
continue;
|
|
3483
|
+
}
|
|
3484
|
+
await localizeReferences({
|
|
3485
|
+
client,
|
|
3486
|
+
documentId: doc._id,
|
|
3487
|
+
sourceLanguage,
|
|
3488
|
+
languageFieldName
|
|
3489
|
+
}), updated++;
|
|
3490
|
+
} catch (error) {
|
|
3491
|
+
errors++, devLog.error(
|
|
3492
|
+
"Failed to reconcile references for document",
|
|
3493
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
3494
|
+
{
|
|
3495
|
+
documentId: doc._id,
|
|
3496
|
+
component: "reconcileReleaseReferences"
|
|
3497
|
+
}
|
|
3498
|
+
);
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
const result = {
|
|
3502
|
+
total: docsInRelease.length,
|
|
3503
|
+
updated,
|
|
3504
|
+
errors
|
|
3505
|
+
};
|
|
3506
|
+
return devLog.info("Release reference reconciliation complete", {
|
|
3507
|
+
releaseId,
|
|
3508
|
+
...result,
|
|
3509
|
+
component: "reconcileReleaseReferences"
|
|
3510
|
+
}), result;
|
|
3511
|
+
} catch (error) {
|
|
3512
|
+
throw devLog.error("Failed to reconcile release references", error instanceof Error ? error : new Error(String(error)), {
|
|
3513
|
+
releaseId,
|
|
3514
|
+
component: "reconcileReleaseReferences"
|
|
3515
|
+
}), error;
|
|
3516
|
+
}
|
|
3517
|
+
}, ReconcileReferencesButton = ({ sourceLanguage }) => {
|
|
3518
|
+
const { releaseConfig, overallStatus, config } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [documentCount, setDocumentCount] = useState(null), [isLoadingCount, setIsLoadingCount] = useState(!1), [reconcilingStatus, setReconcilingStatus] = useState("idle"), [reconciliationProgress, setReconciliationProgress] = useState(
|
|
3519
|
+
void 0
|
|
3520
|
+
), releaseId = releaseConfig.existingReleaseId, languageFieldName = config?.languageFieldName ?? "language";
|
|
3521
|
+
useEffect(() => {
|
|
3522
|
+
if (!releaseId || releaseConfig.mode !== "existing") {
|
|
3523
|
+
setDocumentCount(null);
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3526
|
+
(async () => {
|
|
3527
|
+
setIsLoadingCount(!0);
|
|
3528
|
+
try {
|
|
3529
|
+
const count = await client.fetch('count(*[_id match "versions." + $releaseId + ".*"])', { releaseId });
|
|
3530
|
+
setDocumentCount(count), devLog.debug("Fetched release document count", {
|
|
3531
|
+
releaseId,
|
|
3532
|
+
count,
|
|
3533
|
+
component: "ReconcileReferencesButton"
|
|
3534
|
+
});
|
|
3535
|
+
} catch (error) {
|
|
3536
|
+
devLog.error(
|
|
3537
|
+
"Failed to fetch release document count",
|
|
3538
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
3539
|
+
{
|
|
3540
|
+
releaseId,
|
|
3541
|
+
component: "ReconcileReferencesButton"
|
|
3542
|
+
}
|
|
3543
|
+
), setDocumentCount(null);
|
|
3544
|
+
} finally {
|
|
3545
|
+
setIsLoadingCount(!1);
|
|
3546
|
+
}
|
|
3547
|
+
})();
|
|
3548
|
+
}, [releaseId, releaseConfig.mode, client]);
|
|
3549
|
+
const handleReconcile = useCallback(async () => {
|
|
3550
|
+
if (releaseId) {
|
|
3551
|
+
devLog.info("Starting reference reconciliation", {
|
|
3552
|
+
releaseId,
|
|
3553
|
+
sourceLanguage,
|
|
3554
|
+
component: "ReconcileReferencesButton"
|
|
3555
|
+
}), setReconcilingStatus("reconciling");
|
|
3556
|
+
try {
|
|
3557
|
+
const result = await reconcileReleaseReferences({
|
|
3558
|
+
client,
|
|
3559
|
+
releaseId,
|
|
3560
|
+
sourceLanguage,
|
|
3561
|
+
languageFieldName,
|
|
3562
|
+
onProgress: (current, total) => {
|
|
3563
|
+
setReconciliationProgress({ current, total });
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
devLog.info("Reference reconciliation completed", {
|
|
3567
|
+
releaseId,
|
|
3568
|
+
...result,
|
|
3569
|
+
component: "ReconcileReferencesButton"
|
|
3570
|
+
}), setReconcilingStatus("completed"), setTimeout(() => {
|
|
3571
|
+
setReconcilingStatus("idle"), setReconciliationProgress(void 0);
|
|
3572
|
+
}, 2e3);
|
|
3573
|
+
} catch (error) {
|
|
3574
|
+
devLog.error("Reference reconciliation failed", error instanceof Error ? error : new Error(String(error)), {
|
|
3575
|
+
releaseId,
|
|
3576
|
+
component: "ReconcileReferencesButton"
|
|
3577
|
+
}), setReconcilingStatus("error"), setReconciliationProgress(void 0);
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
}, [releaseId, sourceLanguage, languageFieldName, client]);
|
|
3581
|
+
if (releaseConfig.mode !== "existing" || !releaseId)
|
|
3582
|
+
return null;
|
|
3583
|
+
const isTranslating = overallStatus === "translating" || overallStatus === "linking_references", isReconciling = reconcilingStatus === "reconciling";
|
|
3584
|
+
return /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, tone: reconcilingStatus === "error" ? "critical" : "default", children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3585
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", gap: 3, children: [
|
|
3586
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3587
|
+
/* @__PURE__ */ jsx(Text, { size: 1, weight: "medium", children: "Reconcile References" }),
|
|
3588
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: isLoadingCount ? "Loading..." : documentCount === null ? "Unable to load document count" : documentCount === 0 ? "No documents in release" : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3589
|
+
documentCount,
|
|
3590
|
+
" document",
|
|
3591
|
+
documentCount !== 1 ? "s" : "",
|
|
3592
|
+
" in release"
|
|
3593
|
+
] }) })
|
|
3594
|
+
] }),
|
|
3595
|
+
/* @__PURE__ */ jsx(
|
|
3596
|
+
Button,
|
|
3597
|
+
{
|
|
3598
|
+
icon: isReconciling ? Spinner : SyncIcon,
|
|
3599
|
+
text: reconcilingStatus === "completed" ? "Done!" : reconcilingStatus === "error" ? "Retry" : isReconciling ? "Reconciling..." : "Reconcile",
|
|
3600
|
+
tone: reconcilingStatus === "completed" ? "positive" : reconcilingStatus === "error" ? "critical" : "default",
|
|
3601
|
+
mode: "ghost",
|
|
3602
|
+
onClick: handleReconcile,
|
|
3603
|
+
disabled: (isTranslating || isReconciling || isLoadingCount || documentCount === 0) && reconcilingStatus !== "error"
|
|
3604
|
+
}
|
|
3605
|
+
)
|
|
3606
|
+
] }),
|
|
3607
|
+
isReconciling && reconciliationProgress && /* @__PURE__ */ jsx(Flex, { align: "center", gap: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3608
|
+
"Processing ",
|
|
3609
|
+
reconciliationProgress.current,
|
|
3610
|
+
" of ",
|
|
3611
|
+
reconciliationProgress.total,
|
|
3612
|
+
"..."
|
|
3613
|
+
] }) }),
|
|
3614
|
+
reconcilingStatus === "error" && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Reconciliation failed. Click Retry to try again." }),
|
|
3615
|
+
reconcilingStatus === "idle" && documentCount !== null && documentCount > 0 && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Updates all document references to point to their translated versions in this release." })
|
|
3616
|
+
] }) });
|
|
2953
3617
|
}, ReleaseSelectionPanel = () => {
|
|
2954
|
-
const { dispatch, releaseConfig } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [releases, setReleases] = useState([]), [isLoading, setIsLoading] = useState(!1), [error, setError] = useState(null), [searchQuery, setSearchQuery] = useState("");
|
|
3618
|
+
const { dispatch, releaseConfig, document, config } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), languageFieldName = config?.languageFieldName ?? "language", sourceLanguage = document?.[languageFieldName], [releases, setReleases] = useState([]), [isLoading, setIsLoading] = useState(!1), [error, setError] = useState(null), [searchQuery, setSearchQuery] = useState(""), [showReconcileUI, setShowReconcileUI] = useState(!1);
|
|
2955
3619
|
useEffect(() => {
|
|
2956
3620
|
(async () => {
|
|
2957
3621
|
setIsLoading(!0), setError(null);
|
|
@@ -3064,12 +3728,33 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3064
3728
|
radius: 2,
|
|
3065
3729
|
tone: isSelected ? "primary" : "default",
|
|
3066
3730
|
style: { cursor: "pointer" },
|
|
3067
|
-
onClick: () =>
|
|
3731
|
+
onClick: () => {
|
|
3732
|
+
handleSelectRelease(releaseName), isSelected || setShowReconcileUI(!1);
|
|
3733
|
+
},
|
|
3068
3734
|
border: !0,
|
|
3069
3735
|
muted: isSelected,
|
|
3070
3736
|
children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
3071
|
-
/* @__PURE__ */
|
|
3072
|
-
|
|
3737
|
+
/* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", children: [
|
|
3738
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, children: [
|
|
3739
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: release.metadata.title }),
|
|
3740
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: formatDate(release._createdAt) })
|
|
3741
|
+
] }),
|
|
3742
|
+
isSelected && sourceLanguage && /* @__PURE__ */ jsx(
|
|
3743
|
+
Button,
|
|
3744
|
+
{
|
|
3745
|
+
icon: SyncIcon,
|
|
3746
|
+
text: showReconcileUI ? "Hide" : "Reconcile References",
|
|
3747
|
+
mode: "bleed",
|
|
3748
|
+
tone: "primary",
|
|
3749
|
+
fontSize: 0,
|
|
3750
|
+
padding: 2,
|
|
3751
|
+
onClick: (e) => {
|
|
3752
|
+
e.stopPropagation(), setShowReconcileUI(!showReconcileUI);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
)
|
|
3756
|
+
] }),
|
|
3757
|
+
isSelected && sourceLanguage && showReconcileUI && /* @__PURE__ */ jsx(ReconcileReferencesButton, { sourceLanguage })
|
|
3073
3758
|
] })
|
|
3074
3759
|
},
|
|
3075
3760
|
release._id
|
|
@@ -3296,13 +3981,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3296
3981
|
/* @__PURE__ */ jsx(Box, { style: { maxHeight: "400px", overflowY: "auto" }, children: /* @__PURE__ */ jsx(Stack, { space: 2, children: filteredReferences.length === 0 ? /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "No references match the selected filter." }) : filteredReferences.map((reference) => {
|
|
3297
3982
|
const isSelected = selectedReferenceIds.has(reference._id), displayTitle = reference.title || reference._id;
|
|
3298
3983
|
return /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
|
|
3299
|
-
/* @__PURE__ */ jsx(
|
|
3300
|
-
Checkbox,
|
|
3301
|
-
{
|
|
3302
|
-
checked: isSelected,
|
|
3303
|
-
onChange: () => handleToggleReference(reference._id)
|
|
3304
|
-
}
|
|
3305
|
-
),
|
|
3984
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: isSelected, onChange: () => handleToggleReference(reference._id) }),
|
|
3306
3985
|
/* @__PURE__ */ jsxs(Flex, { flex: 1, align: "center", gap: 2, children: [
|
|
3307
3986
|
/* @__PURE__ */ jsx(Text, { size: 1, children: displayTitle }),
|
|
3308
3987
|
/* @__PURE__ */ jsx(Badge, { tone: "primary", fontSize: 0, children: reference._type })
|
|
@@ -3311,15 +3990,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3311
3990
|
}) }) })
|
|
3312
3991
|
] }) });
|
|
3313
3992
|
}, ReferenceConfigPanel = () => {
|
|
3314
|
-
const {
|
|
3315
|
-
dispatch,
|
|
3316
|
-
document,
|
|
3317
|
-
shouldTranslateReferences,
|
|
3318
|
-
maxReferenceDepth,
|
|
3319
|
-
config,
|
|
3320
|
-
references,
|
|
3321
|
-
selectedReferenceIds
|
|
3322
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [isLoadingReferences, setIsLoadingReferences] = useState(!1), [showSelectionPanel, setShowSelectionPanel] = useState(!1), maxDepth = config.maxReferenceDepth || 3;
|
|
3993
|
+
const { dispatch, document, shouldTranslateReferences, maxReferenceDepth, config, references, selectedReferenceIds } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [isLoadingReferences, setIsLoadingReferences] = useState(!1), [showSelectionPanel, setShowSelectionPanel] = useState(!1), maxDepth = config.maxReferenceDepth || 3;
|
|
3323
3994
|
useEffect(() => {
|
|
3324
3995
|
(async () => {
|
|
3325
3996
|
if (setIsLoadingReferences(!0), !shouldTranslateReferences) {
|