@braze/sanity-plugin-ai-translation 0.2.68 → 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 +389 -44
- 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; ) {
|
|
@@ -2804,8 +2897,28 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2804
2897
|
sourceDocument
|
|
2805
2898
|
});
|
|
2806
2899
|
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages, sourceDocument._id);
|
|
2807
|
-
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(
|
|
2808
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
|
+
}
|
|
2809
2922
|
try {
|
|
2810
2923
|
await translationSemaphore.withPermit(async () => {
|
|
2811
2924
|
const document = await client.withConfig({
|
|
@@ -2844,12 +2957,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2844
2957
|
);
|
|
2845
2958
|
}
|
|
2846
2959
|
})
|
|
2847
|
-
), 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;
|
|
2848
2961
|
devLog.info("Translation tasks completed", {
|
|
2849
2962
|
...logContext,
|
|
2850
2963
|
total: results.length,
|
|
2851
2964
|
succeeded,
|
|
2852
2965
|
failed,
|
|
2966
|
+
skipped: skippedCount,
|
|
2853
2967
|
targetDocuments: versionedTargetDocumentIds.length
|
|
2854
2968
|
}), dispatch({
|
|
2855
2969
|
type: "SET_OVERALL_STATUS",
|
|
@@ -2863,7 +2977,15 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2863
2977
|
languageFieldName
|
|
2864
2978
|
});
|
|
2865
2979
|
})
|
|
2866
|
-
)
|
|
2980
|
+
), releaseConfig.mode === "existing" && releaseId && await updateBackwardReferences({
|
|
2981
|
+
client,
|
|
2982
|
+
releaseId,
|
|
2983
|
+
sourceDocumentIds,
|
|
2984
|
+
targetLanguages,
|
|
2985
|
+
sourceLanguage,
|
|
2986
|
+
languageFieldName,
|
|
2987
|
+
excludeDocumentIds: versionedTargetDocumentIds
|
|
2988
|
+
});
|
|
2867
2989
|
const hasAnyFailures = failed > 0;
|
|
2868
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", {
|
|
2869
2991
|
...logContext,
|
|
@@ -2890,8 +3012,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2890
3012
|
}
|
|
2891
3013
|
throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id), error;
|
|
2892
3014
|
}
|
|
2893
|
-
}, ReleaseConflictWarning = ({
|
|
2894
|
-
|
|
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]);
|
|
2895
3023
|
return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2896
3024
|
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
2897
3025
|
/* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
|
|
@@ -2915,13 +3043,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2915
3043
|
" \u2014 Go back and select a different release, or create a new one."
|
|
2916
3044
|
] }),
|
|
2917
3045
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2918
|
-
/* @__PURE__ */ jsx("strong", { children: "Replace" }),
|
|
3046
|
+
/* @__PURE__ */ jsx("strong", { children: "Replace All" }),
|
|
2919
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)"
|
|
2920
3057
|
] })
|
|
2921
3058
|
] }),
|
|
2922
|
-
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "
|
|
3059
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "space-between", children: [
|
|
2923
3060
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
|
|
2924
|
-
/* @__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
|
+
] })
|
|
2925
3074
|
] })
|
|
2926
3075
|
] }) });
|
|
2927
3076
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
@@ -2935,12 +3084,12 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2935
3084
|
selectedTargetLanguages,
|
|
2936
3085
|
translationMode,
|
|
2937
3086
|
releaseConfig
|
|
2938
|
-
} = 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]);
|
|
2939
3088
|
useEffect(() => {
|
|
2940
3089
|
releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
|
|
2941
3090
|
}, [releaseConfig.mode, conflictContext]);
|
|
2942
3091
|
const startTranslation = useCallback(
|
|
2943
|
-
(replaceExisting, overrideReleaseId) => {
|
|
3092
|
+
(replaceExisting, overrideReleaseId, skipConflicts) => {
|
|
2944
3093
|
setConflictContext(null), translateDocumentSet({
|
|
2945
3094
|
sourceDocument: document,
|
|
2946
3095
|
targetLanguages: selectedTargetLanguages,
|
|
@@ -2957,6 +3106,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2957
3106
|
releaseConfig: {
|
|
2958
3107
|
...releaseConfig,
|
|
2959
3108
|
replaceExisting,
|
|
3109
|
+
skipConflicts,
|
|
2960
3110
|
// Use the override release ID if provided (from conflict context),
|
|
2961
3111
|
// otherwise use the current releaseConfig value
|
|
2962
3112
|
existingReleaseId: overrideReleaseId ?? releaseConfig.existingReleaseId
|
|
@@ -3003,10 +3153,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3003
3153
|
}
|
|
3004
3154
|
}
|
|
3005
3155
|
startTranslation(!1);
|
|
3006
|
-
}, [
|
|
3156
|
+
}, [
|
|
3157
|
+
releaseConfig,
|
|
3158
|
+
client,
|
|
3159
|
+
document,
|
|
3160
|
+
selectedTargetLanguages,
|
|
3161
|
+
config.languageFieldName,
|
|
3162
|
+
startTranslation,
|
|
3163
|
+
filteredReferences
|
|
3164
|
+
]), handleCancelConflict = useCallback(() => {
|
|
3007
3165
|
setConflictContext(null);
|
|
3008
3166
|
}, []), handleReplaceConflict = useCallback(() => {
|
|
3009
3167
|
conflictContext && startTranslation(!0, conflictContext.releaseId);
|
|
3168
|
+
}, [startTranslation, conflictContext]), handleSkipConflicts = useCallback(() => {
|
|
3169
|
+
conflictContext && startTranslation(!1, conflictContext.releaseId, conflictContext.conflicts);
|
|
3010
3170
|
}, [startTranslation, conflictContext]), handleSelectAll = useCallback(() => {
|
|
3011
3171
|
dispatch({
|
|
3012
3172
|
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
@@ -3022,8 +3182,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3022
3182
|
ReleaseConflictWarning,
|
|
3023
3183
|
{
|
|
3024
3184
|
conflicts: conflictContext.conflicts,
|
|
3185
|
+
totalTranslations,
|
|
3025
3186
|
onCancel: handleCancelConflict,
|
|
3026
|
-
onReplace: handleReplaceConflict
|
|
3187
|
+
onReplace: handleReplaceConflict,
|
|
3188
|
+
onSkipConflicts: handleSkipConflicts
|
|
3027
3189
|
}
|
|
3028
3190
|
) }) : /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
|
|
3029
3191
|
/* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
|
|
@@ -3105,8 +3267,184 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3105
3267
|
documentId: document._id
|
|
3106
3268
|
});
|
|
3107
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
|
+
] }) });
|
|
3108
3446
|
}, ReleaseSelectionPanel = () => {
|
|
3109
|
-
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);
|
|
3110
3448
|
useEffect(() => {
|
|
3111
3449
|
(async () => {
|
|
3112
3450
|
setIsLoading(!0), setError(null);
|
|
@@ -3219,12 +3557,33 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3219
3557
|
radius: 2,
|
|
3220
3558
|
tone: isSelected ? "primary" : "default",
|
|
3221
3559
|
style: { cursor: "pointer" },
|
|
3222
|
-
onClick: () =>
|
|
3560
|
+
onClick: () => {
|
|
3561
|
+
handleSelectRelease(releaseName), isSelected || setShowReconcileUI(!1);
|
|
3562
|
+
},
|
|
3223
3563
|
border: !0,
|
|
3224
3564
|
muted: isSelected,
|
|
3225
3565
|
children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
3226
|
-
/* @__PURE__ */
|
|
3227
|
-
|
|
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 })
|
|
3228
3587
|
] })
|
|
3229
3588
|
},
|
|
3230
3589
|
release._id
|
|
@@ -3451,13 +3810,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3451
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) => {
|
|
3452
3811
|
const isSelected = selectedReferenceIds.has(reference._id), displayTitle = reference.title || reference._id;
|
|
3453
3812
|
return /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
|
|
3454
|
-
/* @__PURE__ */ jsx(
|
|
3455
|
-
Checkbox,
|
|
3456
|
-
{
|
|
3457
|
-
checked: isSelected,
|
|
3458
|
-
onChange: () => handleToggleReference(reference._id)
|
|
3459
|
-
}
|
|
3460
|
-
),
|
|
3813
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: isSelected, onChange: () => handleToggleReference(reference._id) }),
|
|
3461
3814
|
/* @__PURE__ */ jsxs(Flex, { flex: 1, align: "center", gap: 2, children: [
|
|
3462
3815
|
/* @__PURE__ */ jsx(Text, { size: 1, children: displayTitle }),
|
|
3463
3816
|
/* @__PURE__ */ jsx(Badge, { tone: "primary", fontSize: 0, children: reference._type })
|
|
@@ -3466,15 +3819,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3466
3819
|
}) }) })
|
|
3467
3820
|
] }) });
|
|
3468
3821
|
}, ReferenceConfigPanel = () => {
|
|
3469
|
-
const {
|
|
3470
|
-
dispatch,
|
|
3471
|
-
document,
|
|
3472
|
-
shouldTranslateReferences,
|
|
3473
|
-
maxReferenceDepth,
|
|
3474
|
-
config,
|
|
3475
|
-
references,
|
|
3476
|
-
selectedReferenceIds
|
|
3477
|
-
} = 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;
|
|
3478
3823
|
useEffect(() => {
|
|
3479
3824
|
(async () => {
|
|
3480
3825
|
if (setIsLoadingReferences(!0), !shouldTranslateReferences) {
|