@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 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 * Math.pow(options.backoffMultiplier, attempt);
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 translationTasks = sourceDocumentIds.flatMap(
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 = ({ conflicts, onCancel, onReplace }) => {
2894
- const uniqueDocuments = useMemo(() => new Set(conflicts.map((c) => c.sourceDocumentId)).size, [conflicts]), summaryText = useMemo(() => conflicts.length === 1 ? "1 translation" : uniqueDocuments === 1 ? `${conflicts.length} translations` : `${conflicts.length} translations across ${uniqueDocuments} documents`, [conflicts.length, uniqueDocuments]);
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: "flex-end", children: [
3059
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "space-between", children: [
2923
3060
  /* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
2924
- /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "caution", text: "Replace Existing", onClick: onReplace })
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
- }, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, filteredReferences]), handleCancelConflict = useCallback(() => {
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: () => handleSelectRelease(releaseName),
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__ */ jsx(Text, { size: 1, children: release.metadata.title }),
3227
- /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: formatDate(release._createdAt) })
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) {