@braze/sanity-plugin-ai-translation 0.2.67 → 0.2.69
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 +405 -49
- 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) {
|
|
@@ -1659,13 +1759,6 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1659
1759
|
} else
|
|
1660
1760
|
root.push(node);
|
|
1661
1761
|
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) {
|
|
1665
|
-
const parent = stack[stack.length - 1];
|
|
1666
|
-
parent && parent.children.push(node);
|
|
1667
|
-
} else
|
|
1668
|
-
root.push(node);
|
|
1669
1762
|
} else if (attributes.type && typeof attributes.type == "string" && attributes.type.startsWith("translatable.")) {
|
|
1670
1763
|
const contentLines = [];
|
|
1671
1764
|
for (i++; i < lines.length; ) {
|
|
@@ -1921,10 +2014,17 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1921
2014
|
});
|
|
1922
2015
|
return;
|
|
1923
2016
|
}
|
|
1924
|
-
node.text !== void 0 && !node.attributes.readonly
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2017
|
+
if (node.text !== void 0 && !node.attributes.readonly) {
|
|
2018
|
+
const existingValue = get(doc, path);
|
|
2019
|
+
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)", {
|
|
2020
|
+
path: path.join("."),
|
|
2021
|
+
existingType: typeof existingValue,
|
|
2022
|
+
attemptedValue: node.text?.substring(0, 50)
|
|
2023
|
+
}) : devLog.debug("Skipping field not present in original document", {
|
|
2024
|
+
path: path.join("."),
|
|
2025
|
+
attemptedValue: node.text
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
1928
2028
|
}, applyBlockChanges = (block, blockNode) => {
|
|
1929
2029
|
if (blockNode.attributes._key && block._key !== blockNode.attributes._key) {
|
|
1930
2030
|
devLog.warn("Block key mismatch, skipping", {
|
|
@@ -1955,7 +2055,11 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1955
2055
|
});
|
|
1956
2056
|
return;
|
|
1957
2057
|
}
|
|
1958
|
-
fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (fieldName in block ? block[fieldName] = fieldNode.text : devLog.debug("Skipping block field not
|
|
2058
|
+
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)", {
|
|
2059
|
+
fieldName,
|
|
2060
|
+
existingType: typeof block[fieldName],
|
|
2061
|
+
attemptedValue: fieldNode.text?.substring(0, 50)
|
|
2062
|
+
}) : devLog.debug("Skipping block field not present in original", {
|
|
1959
2063
|
fieldName,
|
|
1960
2064
|
attemptedValue: fieldNode.text
|
|
1961
2065
|
}));
|
|
@@ -2793,8 +2897,28 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2793
2897
|
sourceDocument
|
|
2794
2898
|
});
|
|
2795
2899
|
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages, sourceDocument._id);
|
|
2796
|
-
const
|
|
2900
|
+
const shouldSkipConflict = (documentId, targetLanguage) => !releaseConfig.skipConflicts || releaseConfig.skipConflicts.length === 0 ? !1 : releaseConfig.skipConflicts.some(
|
|
2901
|
+
(conflict) => conflict.sourceDocumentId === documentId && conflict.targetLanguage === targetLanguage
|
|
2902
|
+
), skippedCount = sourceDocumentIds.reduce((count, documentId) => count + targetLanguages.filter((lang) => shouldSkipConflict(documentId, lang)).length, 0), translationTasks = sourceDocumentIds.flatMap(
|
|
2797
2903
|
(documentId) => targetLanguages.map(async (targetLanguage) => {
|
|
2904
|
+
if (shouldSkipConflict(documentId, targetLanguage)) {
|
|
2905
|
+
const docInfo = sourceDocuments.find((d) => d._id === documentId);
|
|
2906
|
+
devLog.info("Skipping conflicting translation", {
|
|
2907
|
+
documentId,
|
|
2908
|
+
targetLanguage,
|
|
2909
|
+
reason: "User chose to skip conflicts"
|
|
2910
|
+
}), dispatch({
|
|
2911
|
+
type: "SET_TRANSLATION_JOB_STATUS",
|
|
2912
|
+
payload: {
|
|
2913
|
+
_id: documentId,
|
|
2914
|
+
_type: docInfo?._type,
|
|
2915
|
+
language: targetLanguage,
|
|
2916
|
+
status: "skipped",
|
|
2917
|
+
title: docInfo?.title
|
|
2918
|
+
}
|
|
2919
|
+
});
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2798
2922
|
try {
|
|
2799
2923
|
await translationSemaphore.withPermit(async () => {
|
|
2800
2924
|
const document = await client.withConfig({
|
|
@@ -2833,12 +2957,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2833
2957
|
);
|
|
2834
2958
|
}
|
|
2835
2959
|
})
|
|
2836
|
-
), results = await Promise.allSettled(translationTasks), succeeded = results.filter((r) => r.status === "fulfilled").length, failed = results.filter((r) => r.status === "rejected").length;
|
|
2960
|
+
), results = await Promise.allSettled(translationTasks), succeeded = results.filter((r) => r.status === "fulfilled").length - skippedCount, failed = results.filter((r) => r.status === "rejected").length;
|
|
2837
2961
|
devLog.info("Translation tasks completed", {
|
|
2838
2962
|
...logContext,
|
|
2839
2963
|
total: results.length,
|
|
2840
2964
|
succeeded,
|
|
2841
2965
|
failed,
|
|
2966
|
+
skipped: skippedCount,
|
|
2842
2967
|
targetDocuments: versionedTargetDocumentIds.length
|
|
2843
2968
|
}), dispatch({
|
|
2844
2969
|
type: "SET_OVERALL_STATUS",
|
|
@@ -2852,7 +2977,15 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2852
2977
|
languageFieldName
|
|
2853
2978
|
});
|
|
2854
2979
|
})
|
|
2855
|
-
)
|
|
2980
|
+
), releaseConfig.mode === "existing" && releaseId && await updateBackwardReferences({
|
|
2981
|
+
client,
|
|
2982
|
+
releaseId,
|
|
2983
|
+
sourceDocumentIds,
|
|
2984
|
+
targetLanguages,
|
|
2985
|
+
sourceLanguage,
|
|
2986
|
+
languageFieldName,
|
|
2987
|
+
excludeDocumentIds: versionedTargetDocumentIds
|
|
2988
|
+
});
|
|
2856
2989
|
const hasAnyFailures = failed > 0;
|
|
2857
2990
|
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", {
|
|
2858
2991
|
...logContext,
|
|
@@ -2879,8 +3012,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2879
3012
|
}
|
|
2880
3013
|
throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id), error;
|
|
2881
3014
|
}
|
|
2882
|
-
}, ReleaseConflictWarning = ({
|
|
2883
|
-
|
|
3015
|
+
}, ReleaseConflictWarning = ({
|
|
3016
|
+
conflicts,
|
|
3017
|
+
totalTranslations,
|
|
3018
|
+
onCancel,
|
|
3019
|
+
onReplace,
|
|
3020
|
+
onSkipConflicts
|
|
3021
|
+
}) => {
|
|
3022
|
+
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]);
|
|
2884
3023
|
return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2885
3024
|
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
2886
3025
|
/* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
|
|
@@ -2904,13 +3043,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2904
3043
|
" \u2014 Go back and select a different release, or create a new one."
|
|
2905
3044
|
] }),
|
|
2906
3045
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2907
|
-
/* @__PURE__ */ jsx("strong", { children: "Replace" }),
|
|
3046
|
+
/* @__PURE__ */ jsx("strong", { children: "Replace All" }),
|
|
2908
3047
|
" \u2014 Delete the existing translations in this release and create new ones."
|
|
3048
|
+
] }),
|
|
3049
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3050
|
+
/* @__PURE__ */ jsx("strong", { children: "Skip Conflicts" }),
|
|
3051
|
+
" \u2014 Translate only the ",
|
|
3052
|
+
nonConflictingCount,
|
|
3053
|
+
" non-conflicting translation",
|
|
3054
|
+
nonConflictingCount !== 1 ? "s" : "",
|
|
3055
|
+
", leaving existing ones unchanged.",
|
|
3056
|
+
!hasNonConflicting && " (All translations have conflicts)"
|
|
2909
3057
|
] })
|
|
2910
3058
|
] }),
|
|
2911
|
-
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "
|
|
3059
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "space-between", children: [
|
|
2912
3060
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
|
|
2913
|
-
/* @__PURE__ */
|
|
3061
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", children: [
|
|
3062
|
+
/* @__PURE__ */ jsx(Button, { mode: "bleed", tone: "caution", text: "Replace All", onClick: onReplace }),
|
|
3063
|
+
/* @__PURE__ */ jsx(
|
|
3064
|
+
Button,
|
|
3065
|
+
{
|
|
3066
|
+
mode: "ghost",
|
|
3067
|
+
tone: "caution",
|
|
3068
|
+
text: "Skip Conflicts",
|
|
3069
|
+
onClick: onSkipConflicts,
|
|
3070
|
+
disabled: !hasNonConflicting
|
|
3071
|
+
}
|
|
3072
|
+
)
|
|
3073
|
+
] })
|
|
2914
3074
|
] })
|
|
2915
3075
|
] }) });
|
|
2916
3076
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
@@ -2924,12 +3084,12 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2924
3084
|
selectedTargetLanguages,
|
|
2925
3085
|
translationMode,
|
|
2926
3086
|
releaseConfig
|
|
2927
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1), filteredReferences = references.filter((ref) => selectedReferenceIds.has(ref._id));
|
|
3087
|
+
} = 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]);
|
|
2928
3088
|
useEffect(() => {
|
|
2929
3089
|
releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
|
|
2930
3090
|
}, [releaseConfig.mode, conflictContext]);
|
|
2931
3091
|
const startTranslation = useCallback(
|
|
2932
|
-
(replaceExisting, overrideReleaseId) => {
|
|
3092
|
+
(replaceExisting, overrideReleaseId, skipConflicts) => {
|
|
2933
3093
|
setConflictContext(null), translateDocumentSet({
|
|
2934
3094
|
sourceDocument: document,
|
|
2935
3095
|
targetLanguages: selectedTargetLanguages,
|
|
@@ -2946,6 +3106,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2946
3106
|
releaseConfig: {
|
|
2947
3107
|
...releaseConfig,
|
|
2948
3108
|
replaceExisting,
|
|
3109
|
+
skipConflicts,
|
|
2949
3110
|
// Use the override release ID if provided (from conflict context),
|
|
2950
3111
|
// otherwise use the current releaseConfig value
|
|
2951
3112
|
existingReleaseId: overrideReleaseId ?? releaseConfig.existingReleaseId
|
|
@@ -2992,10 +3153,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2992
3153
|
}
|
|
2993
3154
|
}
|
|
2994
3155
|
startTranslation(!1);
|
|
2995
|
-
}, [
|
|
3156
|
+
}, [
|
|
3157
|
+
releaseConfig,
|
|
3158
|
+
client,
|
|
3159
|
+
document,
|
|
3160
|
+
selectedTargetLanguages,
|
|
3161
|
+
config.languageFieldName,
|
|
3162
|
+
startTranslation,
|
|
3163
|
+
filteredReferences
|
|
3164
|
+
]), handleCancelConflict = useCallback(() => {
|
|
2996
3165
|
setConflictContext(null);
|
|
2997
3166
|
}, []), handleReplaceConflict = useCallback(() => {
|
|
2998
3167
|
conflictContext && startTranslation(!0, conflictContext.releaseId);
|
|
3168
|
+
}, [startTranslation, conflictContext]), handleSkipConflicts = useCallback(() => {
|
|
3169
|
+
conflictContext && startTranslation(!1, conflictContext.releaseId, conflictContext.conflicts);
|
|
2999
3170
|
}, [startTranslation, conflictContext]), handleSelectAll = useCallback(() => {
|
|
3000
3171
|
dispatch({
|
|
3001
3172
|
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
@@ -3011,8 +3182,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3011
3182
|
ReleaseConflictWarning,
|
|
3012
3183
|
{
|
|
3013
3184
|
conflicts: conflictContext.conflicts,
|
|
3185
|
+
totalTranslations,
|
|
3014
3186
|
onCancel: handleCancelConflict,
|
|
3015
|
-
onReplace: handleReplaceConflict
|
|
3187
|
+
onReplace: handleReplaceConflict,
|
|
3188
|
+
onSkipConflicts: handleSkipConflicts
|
|
3016
3189
|
}
|
|
3017
3190
|
) }) : /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
|
|
3018
3191
|
/* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
|
|
@@ -3094,8 +3267,184 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3094
3267
|
documentId: document._id
|
|
3095
3268
|
});
|
|
3096
3269
|
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)) }) });
|
|
3270
|
+
}, reconcileReleaseReferences = async ({
|
|
3271
|
+
client,
|
|
3272
|
+
releaseId,
|
|
3273
|
+
sourceLanguage,
|
|
3274
|
+
languageFieldName,
|
|
3275
|
+
onProgress
|
|
3276
|
+
}) => {
|
|
3277
|
+
devLog.info("Starting release reference reconciliation", {
|
|
3278
|
+
releaseId,
|
|
3279
|
+
sourceLanguage,
|
|
3280
|
+
component: "reconcileReleaseReferences"
|
|
3281
|
+
});
|
|
3282
|
+
try {
|
|
3283
|
+
const docsInRelease = await client.fetch(
|
|
3284
|
+
`*[_id match "versions." + $releaseId + ".*"] {
|
|
3285
|
+
_id,
|
|
3286
|
+
"language": @[$languageFieldName]
|
|
3287
|
+
}`,
|
|
3288
|
+
{ releaseId, languageFieldName }
|
|
3289
|
+
);
|
|
3290
|
+
if (docsInRelease.length === 0)
|
|
3291
|
+
return devLog.info("No documents found in release", {
|
|
3292
|
+
releaseId,
|
|
3293
|
+
component: "reconcileReleaseReferences"
|
|
3294
|
+
}), { total: 0, updated: 0, errors: 0 };
|
|
3295
|
+
devLog.info("Found documents in release", {
|
|
3296
|
+
releaseId,
|
|
3297
|
+
count: docsInRelease.length,
|
|
3298
|
+
component: "reconcileReleaseReferences"
|
|
3299
|
+
});
|
|
3300
|
+
let updated = 0, errors = 0;
|
|
3301
|
+
for (let i = 0; i < docsInRelease.length; i++) {
|
|
3302
|
+
const doc = docsInRelease[i];
|
|
3303
|
+
onProgress && onProgress(i + 1, docsInRelease.length);
|
|
3304
|
+
try {
|
|
3305
|
+
if (!doc.language) {
|
|
3306
|
+
devLog.warn("Document missing language field, skipping", {
|
|
3307
|
+
documentId: doc._id,
|
|
3308
|
+
component: "reconcileReleaseReferences"
|
|
3309
|
+
});
|
|
3310
|
+
continue;
|
|
3311
|
+
}
|
|
3312
|
+
await localizeReferences({
|
|
3313
|
+
client,
|
|
3314
|
+
documentId: doc._id,
|
|
3315
|
+
sourceLanguage,
|
|
3316
|
+
languageFieldName
|
|
3317
|
+
}), updated++;
|
|
3318
|
+
} catch (error) {
|
|
3319
|
+
errors++, devLog.error(
|
|
3320
|
+
"Failed to reconcile references for document",
|
|
3321
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
3322
|
+
{
|
|
3323
|
+
documentId: doc._id,
|
|
3324
|
+
component: "reconcileReleaseReferences"
|
|
3325
|
+
}
|
|
3326
|
+
);
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
const result = {
|
|
3330
|
+
total: docsInRelease.length,
|
|
3331
|
+
updated,
|
|
3332
|
+
errors
|
|
3333
|
+
};
|
|
3334
|
+
return devLog.info("Release reference reconciliation complete", {
|
|
3335
|
+
releaseId,
|
|
3336
|
+
...result,
|
|
3337
|
+
component: "reconcileReleaseReferences"
|
|
3338
|
+
}), result;
|
|
3339
|
+
} catch (error) {
|
|
3340
|
+
throw devLog.error("Failed to reconcile release references", error instanceof Error ? error : new Error(String(error)), {
|
|
3341
|
+
releaseId,
|
|
3342
|
+
component: "reconcileReleaseReferences"
|
|
3343
|
+
}), error;
|
|
3344
|
+
}
|
|
3345
|
+
}, ReconcileReferencesButton = ({ sourceLanguage }) => {
|
|
3346
|
+
const { dispatch, releaseConfig, overallStatus, reconcilingStatus, reconciliationProgress, config } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [documentCount, setDocumentCount] = useState(null), [isLoadingCount, setIsLoadingCount] = useState(!1), releaseId = releaseConfig.existingReleaseId, languageFieldName = config?.languageFieldName ?? "language";
|
|
3347
|
+
useEffect(() => {
|
|
3348
|
+
if (!releaseId || releaseConfig.mode !== "existing") {
|
|
3349
|
+
setDocumentCount(null);
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
(async () => {
|
|
3353
|
+
setIsLoadingCount(!0);
|
|
3354
|
+
try {
|
|
3355
|
+
const count = await client.fetch('count(*[_id match "versions." + $releaseId + ".*"])', { releaseId });
|
|
3356
|
+
setDocumentCount(count), devLog.debug("Fetched release document count", {
|
|
3357
|
+
releaseId,
|
|
3358
|
+
count,
|
|
3359
|
+
component: "ReconcileReferencesButton"
|
|
3360
|
+
});
|
|
3361
|
+
} catch (error) {
|
|
3362
|
+
devLog.error(
|
|
3363
|
+
"Failed to fetch release document count",
|
|
3364
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
3365
|
+
{
|
|
3366
|
+
releaseId,
|
|
3367
|
+
component: "ReconcileReferencesButton"
|
|
3368
|
+
}
|
|
3369
|
+
), setDocumentCount(null);
|
|
3370
|
+
} finally {
|
|
3371
|
+
setIsLoadingCount(!1);
|
|
3372
|
+
}
|
|
3373
|
+
})();
|
|
3374
|
+
}, [releaseId, releaseConfig.mode, client]);
|
|
3375
|
+
const handleReconcile = useCallback(async () => {
|
|
3376
|
+
if (releaseId) {
|
|
3377
|
+
devLog.info("Starting reference reconciliation", {
|
|
3378
|
+
releaseId,
|
|
3379
|
+
sourceLanguage,
|
|
3380
|
+
component: "ReconcileReferencesButton"
|
|
3381
|
+
}), dispatch({ type: "SET_RECONCILING_STATUS", payload: "reconciling" });
|
|
3382
|
+
try {
|
|
3383
|
+
const result = await reconcileReleaseReferences({
|
|
3384
|
+
client,
|
|
3385
|
+
releaseId,
|
|
3386
|
+
sourceLanguage,
|
|
3387
|
+
languageFieldName,
|
|
3388
|
+
onProgress: (current, total) => {
|
|
3389
|
+
dispatch({
|
|
3390
|
+
type: "SET_RECONCILIATION_PROGRESS",
|
|
3391
|
+
payload: { current, total }
|
|
3392
|
+
});
|
|
3393
|
+
}
|
|
3394
|
+
});
|
|
3395
|
+
devLog.info("Reference reconciliation completed", {
|
|
3396
|
+
releaseId,
|
|
3397
|
+
...result,
|
|
3398
|
+
component: "ReconcileReferencesButton"
|
|
3399
|
+
}), dispatch({ type: "SET_RECONCILING_STATUS", payload: "completed" }), setTimeout(() => {
|
|
3400
|
+
dispatch({ type: "SET_RECONCILING_STATUS", payload: "idle" });
|
|
3401
|
+
}, 2e3);
|
|
3402
|
+
} catch (error) {
|
|
3403
|
+
devLog.error("Reference reconciliation failed", error instanceof Error ? error : new Error(String(error)), {
|
|
3404
|
+
releaseId,
|
|
3405
|
+
component: "ReconcileReferencesButton"
|
|
3406
|
+
}), dispatch({ type: "SET_RECONCILING_STATUS", payload: "error" });
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
}, [releaseId, sourceLanguage, languageFieldName, client, dispatch]);
|
|
3410
|
+
if (releaseConfig.mode !== "existing" || !releaseId)
|
|
3411
|
+
return null;
|
|
3412
|
+
const isTranslating = overallStatus === "translating" || overallStatus === "linking_references", isReconciling = reconcilingStatus === "reconciling";
|
|
3413
|
+
return /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, tone: reconcilingStatus === "error" ? "critical" : "default", children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3414
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", gap: 3, children: [
|
|
3415
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3416
|
+
/* @__PURE__ */ jsx(Text, { size: 1, weight: "medium", children: "Reconcile References" }),
|
|
3417
|
+
/* @__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: [
|
|
3418
|
+
documentCount,
|
|
3419
|
+
" document",
|
|
3420
|
+
documentCount !== 1 ? "s" : "",
|
|
3421
|
+
" in release"
|
|
3422
|
+
] }) })
|
|
3423
|
+
] }),
|
|
3424
|
+
/* @__PURE__ */ jsx(
|
|
3425
|
+
Button,
|
|
3426
|
+
{
|
|
3427
|
+
icon: isReconciling ? Spinner : SyncIcon,
|
|
3428
|
+
text: reconcilingStatus === "completed" ? "Done!" : reconcilingStatus === "error" ? "Retry" : isReconciling ? "Reconciling..." : "Reconcile",
|
|
3429
|
+
tone: reconcilingStatus === "completed" ? "positive" : reconcilingStatus === "error" ? "critical" : "default",
|
|
3430
|
+
mode: "ghost",
|
|
3431
|
+
onClick: handleReconcile,
|
|
3432
|
+
disabled: (isTranslating || isReconciling || isLoadingCount || documentCount === 0) && reconcilingStatus !== "error"
|
|
3433
|
+
}
|
|
3434
|
+
)
|
|
3435
|
+
] }),
|
|
3436
|
+
isReconciling && reconciliationProgress && /* @__PURE__ */ jsx(Flex, { align: "center", gap: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3437
|
+
"Processing ",
|
|
3438
|
+
reconciliationProgress.current,
|
|
3439
|
+
" of ",
|
|
3440
|
+
reconciliationProgress.total,
|
|
3441
|
+
"..."
|
|
3442
|
+
] }) }),
|
|
3443
|
+
reconcilingStatus === "error" && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Reconciliation failed. Click Retry to try again." }),
|
|
3444
|
+
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." })
|
|
3445
|
+
] }) });
|
|
3097
3446
|
}, ReleaseSelectionPanel = () => {
|
|
3098
|
-
const { dispatch, releaseConfig } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [releases, setReleases] = useState([]), [isLoading, setIsLoading] = useState(!1), [error, setError] = useState(null), [searchQuery, setSearchQuery] = useState("");
|
|
3447
|
+
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);
|
|
3099
3448
|
useEffect(() => {
|
|
3100
3449
|
(async () => {
|
|
3101
3450
|
setIsLoading(!0), setError(null);
|
|
@@ -3208,12 +3557,33 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3208
3557
|
radius: 2,
|
|
3209
3558
|
tone: isSelected ? "primary" : "default",
|
|
3210
3559
|
style: { cursor: "pointer" },
|
|
3211
|
-
onClick: () =>
|
|
3560
|
+
onClick: () => {
|
|
3561
|
+
handleSelectRelease(releaseName), isSelected || setShowReconcileUI(!1);
|
|
3562
|
+
},
|
|
3212
3563
|
border: !0,
|
|
3213
3564
|
muted: isSelected,
|
|
3214
3565
|
children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
3215
|
-
/* @__PURE__ */
|
|
3216
|
-
|
|
3566
|
+
/* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", children: [
|
|
3567
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, children: [
|
|
3568
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: release.metadata.title }),
|
|
3569
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: formatDate(release._createdAt) })
|
|
3570
|
+
] }),
|
|
3571
|
+
isSelected && sourceLanguage && /* @__PURE__ */ jsx(
|
|
3572
|
+
Button,
|
|
3573
|
+
{
|
|
3574
|
+
icon: SyncIcon,
|
|
3575
|
+
text: showReconcileUI ? "Hide" : "Reconcile References",
|
|
3576
|
+
mode: "bleed",
|
|
3577
|
+
tone: "primary",
|
|
3578
|
+
fontSize: 0,
|
|
3579
|
+
padding: 2,
|
|
3580
|
+
onClick: (e) => {
|
|
3581
|
+
e.stopPropagation(), setShowReconcileUI(!showReconcileUI);
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
)
|
|
3585
|
+
] }),
|
|
3586
|
+
releaseConfig.existingReleaseId && sourceLanguage && showReconcileUI && /* @__PURE__ */ jsx(ReconcileReferencesButton, { sourceLanguage })
|
|
3217
3587
|
] })
|
|
3218
3588
|
},
|
|
3219
3589
|
release._id
|
|
@@ -3440,13 +3810,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3440
3810
|
/* @__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) => {
|
|
3441
3811
|
const isSelected = selectedReferenceIds.has(reference._id), displayTitle = reference.title || reference._id;
|
|
3442
3812
|
return /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
|
|
3443
|
-
/* @__PURE__ */ jsx(
|
|
3444
|
-
Checkbox,
|
|
3445
|
-
{
|
|
3446
|
-
checked: isSelected,
|
|
3447
|
-
onChange: () => handleToggleReference(reference._id)
|
|
3448
|
-
}
|
|
3449
|
-
),
|
|
3813
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: isSelected, onChange: () => handleToggleReference(reference._id) }),
|
|
3450
3814
|
/* @__PURE__ */ jsxs(Flex, { flex: 1, align: "center", gap: 2, children: [
|
|
3451
3815
|
/* @__PURE__ */ jsx(Text, { size: 1, children: displayTitle }),
|
|
3452
3816
|
/* @__PURE__ */ jsx(Badge, { tone: "primary", fontSize: 0, children: reference._type })
|
|
@@ -3455,15 +3819,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3455
3819
|
}) }) })
|
|
3456
3820
|
] }) });
|
|
3457
3821
|
}, ReferenceConfigPanel = () => {
|
|
3458
|
-
const {
|
|
3459
|
-
dispatch,
|
|
3460
|
-
document,
|
|
3461
|
-
shouldTranslateReferences,
|
|
3462
|
-
maxReferenceDepth,
|
|
3463
|
-
config,
|
|
3464
|
-
references,
|
|
3465
|
-
selectedReferenceIds
|
|
3466
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [isLoadingReferences, setIsLoadingReferences] = useState(!1), [showSelectionPanel, setShowSelectionPanel] = useState(!1), maxDepth = config.maxReferenceDepth || 3;
|
|
3822
|
+
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;
|
|
3467
3823
|
useEffect(() => {
|
|
3468
3824
|
(async () => {
|
|
3469
3825
|
if (setIsLoadingReferences(!0), !shouldTranslateReferences) {
|