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

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) {
@@ -939,6 +1039,57 @@ const fetchTranslationPrompts = async ({
939
1039
  payload
940
1040
  }), mainSourceDocumentId && updateActiveTranslationJob(mainSourceDocumentId, payload);
941
1041
  }
1042
+ }, emitTelemetryEvent = (config, event) => {
1043
+ if (config?.onEvent)
1044
+ try {
1045
+ config.onEvent(event), devLog.debug("Telemetry event emitted", { eventName: event.name });
1046
+ } catch (error) {
1047
+ devLog.warn("Failed to emit telemetry event", {
1048
+ eventName: event.name,
1049
+ error: error instanceof Error ? error.message : String(error)
1050
+ });
1051
+ }
1052
+ }, emitTelemetryError = (config, bundle) => {
1053
+ if (config?.onError)
1054
+ try {
1055
+ config.onError(bundle), devLog.debug("Telemetry error emitted", {
1056
+ documentId: bundle.documentId,
1057
+ errorType: bundle.error.type
1058
+ });
1059
+ } catch (error) {
1060
+ devLog.warn("Failed to emit telemetry error", {
1061
+ documentId: bundle.documentId,
1062
+ error: error instanceof Error ? error.message : String(error)
1063
+ });
1064
+ }
1065
+ }, buildErrorBundle = (params) => {
1066
+ const durationMs = Date.now() - params.startTime;
1067
+ return {
1068
+ documentId: params.documentId,
1069
+ documentType: params.documentType,
1070
+ targetLanguage: params.targetLanguage,
1071
+ sourceLanguage: params.sourceLanguage,
1072
+ translationMode: params.translationMode,
1073
+ error: {
1074
+ message: params.error.message,
1075
+ stack: params.error.stack,
1076
+ type: params.errorType
1077
+ },
1078
+ debug: {
1079
+ originalDocument: params.originalDocument,
1080
+ serializedDocument: params.serializedDocument,
1081
+ openaiResponse: params.openaiResponse,
1082
+ translationPrompts: params.translationPrompts,
1083
+ validationErrors: params.validationErrors
1084
+ },
1085
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1086
+ durationMs
1087
+ };
1088
+ }, createTimestamp = () => (/* @__PURE__ */ new Date()).toISOString(), classifyError = (error, context) => {
1089
+ if (context)
1090
+ return context;
1091
+ const message = error.message.toLowerCase();
1092
+ return message.includes("network") || message.includes("fetch") || message.includes("timeout") || message.includes("econnrefused") || message.includes("enotfound") ? "network" : message.includes("openai") || message.includes("gpt") || message.includes("api error") ? "openai_api" : message.includes("validation") || message.includes("structure") || message.includes("invalid") ? "validation" : message.includes("deserialize") || message.includes("parse") ? "deserialization" : message.includes("serialize") || message.includes("xml") ? "serialization" : message.includes("agent") || message.includes("sanity") ? "sanity_agent" : "unknown";
942
1093
  }, updateTranslationMetadata = async ({
943
1094
  client,
944
1095
  targetLanguage,
@@ -1659,13 +1810,6 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1659
1810
  } else
1660
1811
  root.push(node);
1661
1812
  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
1813
  } else if (attributes.type && typeof attributes.type == "string" && attributes.type.startsWith("translatable.")) {
1670
1814
  const contentLines = [];
1671
1815
  for (i++; i < lines.length; ) {
@@ -2138,16 +2282,18 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2138
2282
  translationPrompts,
2139
2283
  targetLanguage,
2140
2284
  openaiConfig,
2141
- previousError,
2142
- errorContext
2285
+ previousError: _previousError,
2286
+ errorContext,
2287
+ telemetryConfig
2143
2288
  }) => {
2144
- const sourceLanguage = sourceDocument[languageFieldName], logContext = {
2289
+ const startTime = Date.now(), sourceLanguage = sourceDocument[languageFieldName], logContext = {
2145
2290
  sourceId: sourceDocument._id,
2146
2291
  sourceLanguage
2147
2292
  };
2293
+ let serializedDocument, openaiResponseContent;
2148
2294
  try {
2149
2295
  const sourceText = serializeDocument(sourceDocument);
2150
- devLog.info("Serialized document for OpenAI", {
2296
+ serializedDocument = sourceText, devLog.info("Serialized document for OpenAI", {
2151
2297
  ...logContext,
2152
2298
  characterCount: sourceText.length,
2153
2299
  estimatedTokens: Math.ceil(sourceText.length / 4),
@@ -2170,7 +2316,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2170
2316
  model: openaiConfig.model,
2171
2317
  reasoningEffort: openaiConfig.reasoningEffort
2172
2318
  });
2173
- devLog.info("OpenAI translation response received", {
2319
+ openaiResponseContent = response.content, devLog.info("OpenAI translation response received", {
2174
2320
  ...logContext,
2175
2321
  tokensUsed: response.tokensUsed,
2176
2322
  cost: response.cost,
@@ -2208,9 +2354,35 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2208
2354
  })), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
2209
2355
  tokensUsed: response.tokensUsed.total,
2210
2356
  estimatedCost: response.cost
2211
- }), result;
2357
+ }), {
2358
+ document: result,
2359
+ tokensUsed: response.tokensUsed.total,
2360
+ cost: response.cost
2361
+ };
2212
2362
  } catch (error) {
2213
- throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
2363
+ const err = error instanceof Error ? error : new Error(String(error));
2364
+ throw devLog.error("OpenAI translation failed", err, logContext), emitTelemetryError(
2365
+ telemetryConfig,
2366
+ buildErrorBundle({
2367
+ documentId: sourceDocument._id,
2368
+ documentType: sourceDocument._type,
2369
+ targetLanguage,
2370
+ sourceLanguage,
2371
+ translationMode: "openai-only",
2372
+ error: err,
2373
+ errorType: classifyError(err, "openai_api"),
2374
+ startTime,
2375
+ originalDocument: sourceDocument,
2376
+ serializedDocument,
2377
+ openaiResponse: openaiResponseContent,
2378
+ translationPrompts: {
2379
+ globalPrompt: translationPrompts.globalPrompt,
2380
+ localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
2381
+ protectedPhrases: translationPrompts.protectedPhrases,
2382
+ glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms ? [translationPrompts.localeInstructions[targetLanguage].glossaryTerms] : void 0
2383
+ }
2384
+ })
2385
+ ), error;
2214
2386
  }
2215
2387
  }, buildTranslationPrompt$1 = ({
2216
2388
  sourceText,
@@ -2446,9 +2618,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2446
2618
  releaseId,
2447
2619
  excludeFields = [],
2448
2620
  replaceExisting = !1,
2449
- mainSourceDocumentId
2621
+ mainSourceDocumentId,
2622
+ telemetryConfig
2450
2623
  }) => {
2451
- const safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
2624
+ const startTime = Date.now(), sourceLanguage = sourceDocument[languageFieldName], safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
2452
2625
  excludeFields.length !== safeExcludeFields.length && devLog.warn("Language field was in excludeFields - removing it to prevent translation errors", {
2453
2626
  documentId: sourceDocument._id,
2454
2627
  languageFieldName,
@@ -2487,29 +2660,55 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2487
2660
  documentId: sourceDocument._id,
2488
2661
  documentType: sourceDocument._type
2489
2662
  }), documentToTranslate = filteredDocument;
2490
- let translatedDoc, previousError = null;
2663
+ let translatedDoc, translationMetrics = {}, previousError = null;
2491
2664
  const performTranslation = async (errorContext) => {
2492
2665
  switch (translationMode) {
2493
- case "sanity-only":
2494
- return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating", void 0, mainSourceDocumentId), await getSanityAgentTranslation({
2495
- sourceDocument: documentToTranslate,
2496
- targetLanguage,
2497
- languageFieldName,
2498
- schemaId,
2499
- client,
2500
- translationPrompts
2501
- });
2666
+ case "sanity-only": {
2667
+ setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating", void 0, mainSourceDocumentId);
2668
+ try {
2669
+ return { document: await getSanityAgentTranslation({
2670
+ sourceDocument: documentToTranslate,
2671
+ targetLanguage,
2672
+ languageFieldName,
2673
+ schemaId,
2674
+ client,
2675
+ translationPrompts
2676
+ }) };
2677
+ } catch (error) {
2678
+ const err = error instanceof Error ? error : new Error(String(error));
2679
+ throw emitTelemetryError(
2680
+ telemetryConfig,
2681
+ buildErrorBundle({
2682
+ documentId: sourceDocument._id,
2683
+ documentType: sourceDocument._type,
2684
+ targetLanguage,
2685
+ sourceLanguage,
2686
+ translationMode: "sanity-only",
2687
+ error: err,
2688
+ errorType: classifyError(err, "sanity_agent"),
2689
+ startTime,
2690
+ originalDocument: sourceDocument,
2691
+ translationPrompts: {
2692
+ globalPrompt: translationPrompts.globalPrompt,
2693
+ localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
2694
+ protectedPhrases: translationPrompts.protectedPhrases
2695
+ }
2696
+ })
2697
+ ), error;
2698
+ }
2699
+ }
2502
2700
  case "openai-only": {
2503
2701
  if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2504
2702
  throw new Error("OpenAI is not configured but openai-only mode was selected");
2505
- return setJobStatus(
2703
+ setJobStatus(
2506
2704
  dispatch,
2507
2705
  sourceDocument._id,
2508
2706
  targetLanguage,
2509
2707
  "translating_with_openai",
2510
2708
  void 0,
2511
2709
  mainSourceDocumentId
2512
- ), await getOpenAITranslation({
2710
+ );
2711
+ const result = await getOpenAITranslation({
2513
2712
  dispatch,
2514
2713
  sourceDocument: documentToTranslate,
2515
2714
  targetLanguage,
@@ -2517,14 +2716,23 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2517
2716
  translationPrompts,
2518
2717
  openaiConfig,
2519
2718
  previousError: errorContext && previousError ? previousError : void 0,
2520
- errorContext
2719
+ errorContext,
2720
+ telemetryConfig
2521
2721
  });
2722
+ return result ? {
2723
+ document: result.document,
2724
+ tokensUsed: result.tokensUsed,
2725
+ cost: result.cost
2726
+ } : { document: null };
2522
2727
  }
2523
2728
  default:
2524
2729
  throw new Error(`Invalid translation mode: ${translationMode}`);
2525
2730
  }
2526
- };
2527
- if (translatedDoc = await performTranslation(), !translatedDoc)
2731
+ }, translationResult = await performTranslation();
2732
+ if (translatedDoc = translationResult.document, translationMetrics = {
2733
+ tokensUsed: translationResult.tokensUsed,
2734
+ cost: translationResult.cost
2735
+ }, !translatedDoc)
2528
2736
  return null;
2529
2737
  let transformedResult;
2530
2738
  Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
@@ -2587,9 +2795,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2587
2795
  targetLanguage,
2588
2796
  error: error instanceof Error ? error.message : String(error)
2589
2797
  }), previousError = error instanceof Error ? error : new Error(String(error));
2590
- const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure.`;
2591
- if (translatedDoc = await performTranslation(errorContext), !translatedDoc)
2798
+ const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure.`, retryResult = await performTranslation(errorContext);
2799
+ if (translatedDoc = retryResult.document, !translatedDoc)
2592
2800
  throw new Error("Translation retry failed: no document returned");
2801
+ translationMetrics = {
2802
+ tokensUsed: retryResult.tokensUsed,
2803
+ cost: retryResult.cost
2804
+ };
2593
2805
  let retryTransformedResult;
2594
2806
  Object.keys(originalStructure).length > 0 ? retryTransformedResult = restoreTransforms({
2595
2807
  translatedDocument: translatedDoc,
@@ -2620,11 +2832,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2620
2832
  isRetry: !0
2621
2833
  }
2622
2834
  }
2623
- ), retryNewDoc;
2835
+ ), {
2836
+ document: retryNewDoc,
2837
+ tokensUsed: translationMetrics.tokensUsed,
2838
+ cost: translationMetrics.cost
2839
+ };
2624
2840
  }
2625
2841
  throw error;
2626
2842
  }
2627
- return newDoc;
2843
+ return {
2844
+ document: newDoc,
2845
+ tokensUsed: translationMetrics.tokensUsed,
2846
+ cost: translationMetrics.cost
2847
+ };
2628
2848
  }, translateDocument = async ({
2629
2849
  dispatch,
2630
2850
  client,
@@ -2639,9 +2859,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2639
2859
  releaseId,
2640
2860
  excludeFields,
2641
2861
  replaceExisting,
2642
- mainSourceDocumentId
2862
+ mainSourceDocumentId,
2863
+ telemetryConfig
2643
2864
  }) => {
2644
- const logContext = {
2865
+ const jobStartTime = Date.now(), logContext = {
2645
2866
  documentId: sourceDocument._id,
2646
2867
  targetLanguage,
2647
2868
  schemaId,
@@ -2653,7 +2874,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2653
2874
  client,
2654
2875
  sourceDocument,
2655
2876
  sourceLanguage
2656
- }), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
2877
+ }), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), translationResult = await handleTranslation({
2657
2878
  dispatch,
2658
2879
  client,
2659
2880
  schemaId,
@@ -2667,8 +2888,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2667
2888
  releaseId,
2668
2889
  excludeFields,
2669
2890
  replaceExisting,
2670
- mainSourceDocumentId
2671
- });
2891
+ mainSourceDocumentId,
2892
+ telemetryConfig
2893
+ }), newDocument = translationResult?.document;
2672
2894
  if (!newDocument) {
2673
2895
  devLog.error("Translation failed: no document returned", new Error("Translation returned null"), logContext), setJobStatus(
2674
2896
  dispatch,
@@ -2705,12 +2927,27 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2705
2927
  targetLanguage,
2706
2928
  "completed",
2707
2929
  {
2708
- targetId: newDocument._id
2930
+ targetId: newDocument._id,
2931
+ tokensUsed: translationResult?.tokensUsed,
2932
+ estimatedCost: translationResult?.cost
2709
2933
  },
2710
2934
  mainSourceDocumentId
2711
2935
  ), devLog.info("Translation completed successfully", {
2712
2936
  ...logContext,
2713
2937
  newDocumentId: newDocument._id
2938
+ }), emitTelemetryEvent(telemetryConfig, {
2939
+ name: "translation.job_completed",
2940
+ timestamp: createTimestamp(),
2941
+ data: {
2942
+ documentId: sourceDocument._id,
2943
+ documentType: sourceDocument._type,
2944
+ sourceLanguage,
2945
+ targetLanguage,
2946
+ translationMode,
2947
+ durationMs: Date.now() - jobStartTime,
2948
+ tokensUsed: translationResult?.tokensUsed,
2949
+ estimatedCost: translationResult?.cost
2950
+ }
2714
2951
  }), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
2715
2952
  } catch (error) {
2716
2953
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -2738,9 +2975,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2738
2975
  translationMode,
2739
2976
  openaiConfig,
2740
2977
  excludeFields,
2741
- releaseConfig
2978
+ releaseConfig,
2979
+ telemetryConfig
2742
2980
  }) => {
2743
- const logContext = {
2981
+ const batchStartTime = Date.now(), logContext = {
2744
2982
  documentId: sourceDocument._id,
2745
2983
  documentRev: sourceDocument._rev,
2746
2984
  targetLanguages,
@@ -2754,7 +2992,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2754
2992
  const sourceLanguage = sourceDocument[languageFieldName];
2755
2993
  if (!sourceLanguage)
2756
2994
  throw devLog.error("Source language not found"), new Error("Source language not found");
2757
- if (maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
2995
+ if (emitTelemetryEvent(telemetryConfig, {
2996
+ name: "translation.batch_started",
2997
+ timestamp: createTimestamp(),
2998
+ data: {
2999
+ documentId: sourceDocument._id,
3000
+ documentType: sourceDocument._type,
3001
+ sourceLanguage,
3002
+ targetLanguages,
3003
+ translationMode,
3004
+ referenceCount: references.length,
3005
+ releaseMode: releaseConfig.mode
3006
+ }
3007
+ }), maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
2758
3008
  type: "SET_OVERALL_STATUS",
2759
3009
  payload: "translating"
2760
3010
  }), releaseConfig.mode === "existing" && releaseConfig.existingReleaseId)
@@ -2804,8 +3054,28 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2804
3054
  sourceDocument
2805
3055
  });
2806
3056
  setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages, sourceDocument._id);
2807
- const translationTasks = sourceDocumentIds.flatMap(
3057
+ const shouldSkipConflict = (documentId, targetLanguage) => !releaseConfig.skipConflicts || releaseConfig.skipConflicts.length === 0 ? !1 : releaseConfig.skipConflicts.some(
3058
+ (conflict) => conflict.sourceDocumentId === documentId && conflict.targetLanguage === targetLanguage
3059
+ ), skippedCount = sourceDocumentIds.reduce((count, documentId) => count + targetLanguages.filter((lang) => shouldSkipConflict(documentId, lang)).length, 0), translationTasks = sourceDocumentIds.flatMap(
2808
3060
  (documentId) => targetLanguages.map(async (targetLanguage) => {
3061
+ if (shouldSkipConflict(documentId, targetLanguage)) {
3062
+ const docInfo = sourceDocuments.find((d) => d._id === documentId);
3063
+ devLog.info("Skipping conflicting translation", {
3064
+ documentId,
3065
+ targetLanguage,
3066
+ reason: "User chose to skip conflicts"
3067
+ }), dispatch({
3068
+ type: "SET_TRANSLATION_JOB_STATUS",
3069
+ payload: {
3070
+ _id: documentId,
3071
+ _type: docInfo?._type,
3072
+ language: targetLanguage,
3073
+ status: "skipped",
3074
+ title: docInfo?.title
3075
+ }
3076
+ });
3077
+ return;
3078
+ }
2809
3079
  try {
2810
3080
  await translationSemaphore.withPermit(async () => {
2811
3081
  const document = await client.withConfig({
@@ -2827,7 +3097,8 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2827
3097
  excludeFields,
2828
3098
  replaceExisting: releaseConfig.replaceExisting,
2829
3099
  // Pass main source document ID for active translations tracking
2830
- mainSourceDocumentId: sourceDocument._id
3100
+ mainSourceDocumentId: sourceDocument._id,
3101
+ telemetryConfig
2831
3102
  });
2832
3103
  versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
2833
3104
  }
@@ -2844,13 +3115,28 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2844
3115
  );
2845
3116
  }
2846
3117
  })
2847
- ), results = await Promise.allSettled(translationTasks), succeeded = results.filter((r) => r.status === "fulfilled").length, failed = results.filter((r) => r.status === "rejected").length;
3118
+ ), results = await Promise.allSettled(translationTasks), succeeded = results.filter((r) => r.status === "fulfilled").length - skippedCount, failed = results.filter((r) => r.status === "rejected").length;
2848
3119
  devLog.info("Translation tasks completed", {
2849
3120
  ...logContext,
2850
3121
  total: results.length,
2851
3122
  succeeded,
2852
3123
  failed,
3124
+ skipped: skippedCount,
2853
3125
  targetDocuments: versionedTargetDocumentIds.length
3126
+ }), emitTelemetryEvent(telemetryConfig, {
3127
+ name: "translation.batch_completed",
3128
+ timestamp: createTimestamp(),
3129
+ data: {
3130
+ documentId: sourceDocument._id,
3131
+ documentType: sourceDocument._type,
3132
+ sourceLanguage,
3133
+ targetLanguages,
3134
+ translationMode,
3135
+ succeeded,
3136
+ failed,
3137
+ skipped: skippedCount,
3138
+ durationMs: Date.now() - batchStartTime
3139
+ }
2854
3140
  }), dispatch({
2855
3141
  type: "SET_OVERALL_STATUS",
2856
3142
  payload: "linking_references"
@@ -2863,7 +3149,15 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2863
3149
  languageFieldName
2864
3150
  });
2865
3151
  })
2866
- );
3152
+ ), releaseConfig.mode === "existing" && releaseId && await updateBackwardReferences({
3153
+ client,
3154
+ releaseId,
3155
+ sourceDocumentIds,
3156
+ targetLanguages,
3157
+ sourceLanguage,
3158
+ languageFieldName,
3159
+ excludeDocumentIds: versionedTargetDocumentIds
3160
+ });
2867
3161
  const hasAnyFailures = failed > 0;
2868
3162
  hasAnyFailures && succeeded === 0 ? (devLog.error("All translation tasks failed", void 0, logContext), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id)) : hasAnyFailures ? (devLog.warn("Translation completed with some failures", {
2869
3163
  ...logContext,
@@ -2890,8 +3184,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2890
3184
  }
2891
3185
  throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id), error;
2892
3186
  }
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]);
3187
+ }, ReleaseConflictWarning = ({
3188
+ conflicts,
3189
+ totalTranslations,
3190
+ onCancel,
3191
+ onReplace,
3192
+ onSkipConflicts
3193
+ }) => {
3194
+ const uniqueDocuments = useMemo(() => new Set(conflicts.map((c) => c.sourceDocumentId)).size, [conflicts]), nonConflictingCount = totalTranslations - conflicts.length, hasNonConflicting = nonConflictingCount > 0, summaryText = useMemo(() => conflicts.length === 1 ? "1 translation" : uniqueDocuments === 1 ? `${conflicts.length} translations` : `${conflicts.length} translations across ${uniqueDocuments} documents`, [conflicts.length, uniqueDocuments]);
2895
3195
  return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
2896
3196
  /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
2897
3197
  /* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
@@ -2915,13 +3215,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2915
3215
  " \u2014 Go back and select a different release, or create a new one."
2916
3216
  ] }),
2917
3217
  /* @__PURE__ */ jsxs(Text, { size: 1, children: [
2918
- /* @__PURE__ */ jsx("strong", { children: "Replace" }),
3218
+ /* @__PURE__ */ jsx("strong", { children: "Replace All" }),
2919
3219
  " \u2014 Delete the existing translations in this release and create new ones."
3220
+ ] }),
3221
+ /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3222
+ /* @__PURE__ */ jsx("strong", { children: "Skip Conflicts" }),
3223
+ " \u2014 Translate only the ",
3224
+ nonConflictingCount,
3225
+ " non-conflicting translation",
3226
+ nonConflictingCount !== 1 ? "s" : "",
3227
+ ", leaving existing ones unchanged.",
3228
+ !hasNonConflicting && " (All translations have conflicts)"
2920
3229
  ] })
2921
3230
  ] }),
2922
- /* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", children: [
3231
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "space-between", children: [
2923
3232
  /* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
2924
- /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "caution", text: "Replace Existing", onClick: onReplace })
3233
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", children: [
3234
+ /* @__PURE__ */ jsx(Button, { mode: "bleed", tone: "caution", text: "Replace All", onClick: onReplace }),
3235
+ /* @__PURE__ */ jsx(
3236
+ Button,
3237
+ {
3238
+ mode: "ghost",
3239
+ tone: "caution",
3240
+ text: "Skip Conflicts",
3241
+ onClick: onSkipConflicts,
3242
+ disabled: !hasNonConflicting
3243
+ }
3244
+ )
3245
+ ] })
2925
3246
  ] })
2926
3247
  ] }) });
2927
3248
  }, ActionButtonBar = ({ setIsDialogOpen }) => {
@@ -2935,12 +3256,12 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2935
3256
  selectedTargetLanguages,
2936
3257
  translationMode,
2937
3258
  releaseConfig
2938
- } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1), filteredReferences = references.filter((ref) => selectedReferenceIds.has(ref._id));
3259
+ } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1), filteredReferences = references.filter((ref) => selectedReferenceIds.has(ref._id)), totalTranslations = useMemo(() => (1 + filteredReferences.length) * selectedTargetLanguages.length, [filteredReferences.length, selectedTargetLanguages.length]);
2939
3260
  useEffect(() => {
2940
3261
  releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
2941
3262
  }, [releaseConfig.mode, conflictContext]);
2942
3263
  const startTranslation = useCallback(
2943
- (replaceExisting, overrideReleaseId) => {
3264
+ (replaceExisting, overrideReleaseId, skipConflicts) => {
2944
3265
  setConflictContext(null), translateDocumentSet({
2945
3266
  sourceDocument: document,
2946
3267
  targetLanguages: selectedTargetLanguages,
@@ -2957,10 +3278,12 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2957
3278
  releaseConfig: {
2958
3279
  ...releaseConfig,
2959
3280
  replaceExisting,
3281
+ skipConflicts,
2960
3282
  // Use the override release ID if provided (from conflict context),
2961
3283
  // otherwise use the current releaseConfig value
2962
3284
  existingReleaseId: overrideReleaseId ?? releaseConfig.existingReleaseId
2963
- }
3285
+ },
3286
+ telemetryConfig: config.telemetry
2964
3287
  });
2965
3288
  },
2966
3289
  [
@@ -2974,6 +3297,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2974
3297
  config.maxReferenceDepth,
2975
3298
  config.openai,
2976
3299
  config.excludeFields,
3300
+ config.telemetry,
2977
3301
  filteredReferences,
2978
3302
  translationMode,
2979
3303
  releaseConfig
@@ -3003,10 +3327,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3003
3327
  }
3004
3328
  }
3005
3329
  startTranslation(!1);
3006
- }, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, filteredReferences]), handleCancelConflict = useCallback(() => {
3330
+ }, [
3331
+ releaseConfig,
3332
+ client,
3333
+ document,
3334
+ selectedTargetLanguages,
3335
+ config.languageFieldName,
3336
+ startTranslation,
3337
+ filteredReferences
3338
+ ]), handleCancelConflict = useCallback(() => {
3007
3339
  setConflictContext(null);
3008
3340
  }, []), handleReplaceConflict = useCallback(() => {
3009
3341
  conflictContext && startTranslation(!0, conflictContext.releaseId);
3342
+ }, [startTranslation, conflictContext]), handleSkipConflicts = useCallback(() => {
3343
+ conflictContext && startTranslation(!1, conflictContext.releaseId, conflictContext.conflicts);
3010
3344
  }, [startTranslation, conflictContext]), handleSelectAll = useCallback(() => {
3011
3345
  dispatch({
3012
3346
  type: "SET_SELECTED_TARGET_LANGUAGES",
@@ -3022,8 +3356,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3022
3356
  ReleaseConflictWarning,
3023
3357
  {
3024
3358
  conflicts: conflictContext.conflicts,
3359
+ totalTranslations,
3025
3360
  onCancel: handleCancelConflict,
3026
- onReplace: handleReplaceConflict
3361
+ onReplace: handleReplaceConflict,
3362
+ onSkipConflicts: handleSkipConflicts
3027
3363
  }
3028
3364
  ) }) : /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
3029
3365
  /* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
@@ -3105,8 +3441,183 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3105
3441
  documentId: document._id
3106
3442
  });
3107
3443
  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)) }) });
3444
+ }, reconcileReleaseReferences = async ({
3445
+ client,
3446
+ releaseId,
3447
+ sourceLanguage,
3448
+ languageFieldName,
3449
+ onProgress
3450
+ }) => {
3451
+ devLog.info("Starting release reference reconciliation", {
3452
+ releaseId,
3453
+ sourceLanguage,
3454
+ component: "reconcileReleaseReferences"
3455
+ });
3456
+ try {
3457
+ const docsInRelease = await client.fetch(
3458
+ `*[_id match "versions." + $releaseId + ".*"] {
3459
+ _id,
3460
+ "language": @[$languageFieldName]
3461
+ }`,
3462
+ { releaseId, languageFieldName }
3463
+ );
3464
+ if (docsInRelease.length === 0)
3465
+ return devLog.info("No documents found in release", {
3466
+ releaseId,
3467
+ component: "reconcileReleaseReferences"
3468
+ }), { total: 0, updated: 0, errors: 0 };
3469
+ devLog.info("Found documents in release", {
3470
+ releaseId,
3471
+ count: docsInRelease.length,
3472
+ component: "reconcileReleaseReferences"
3473
+ });
3474
+ let updated = 0, errors = 0;
3475
+ for (let i = 0; i < docsInRelease.length; i++) {
3476
+ const doc = docsInRelease[i];
3477
+ onProgress && onProgress(i + 1, docsInRelease.length);
3478
+ try {
3479
+ if (!doc.language) {
3480
+ devLog.warn("Document missing language field, skipping", {
3481
+ documentId: doc._id,
3482
+ component: "reconcileReleaseReferences"
3483
+ });
3484
+ continue;
3485
+ }
3486
+ await localizeReferences({
3487
+ client,
3488
+ documentId: doc._id,
3489
+ sourceLanguage,
3490
+ languageFieldName
3491
+ }), updated++;
3492
+ } catch (error) {
3493
+ errors++, devLog.error(
3494
+ "Failed to reconcile references for document",
3495
+ error instanceof Error ? error : new Error(String(error)),
3496
+ {
3497
+ documentId: doc._id,
3498
+ component: "reconcileReleaseReferences"
3499
+ }
3500
+ );
3501
+ }
3502
+ }
3503
+ const result = {
3504
+ total: docsInRelease.length,
3505
+ updated,
3506
+ errors
3507
+ };
3508
+ return devLog.info("Release reference reconciliation complete", {
3509
+ releaseId,
3510
+ ...result,
3511
+ component: "reconcileReleaseReferences"
3512
+ }), result;
3513
+ } catch (error) {
3514
+ throw devLog.error("Failed to reconcile release references", error instanceof Error ? error : new Error(String(error)), {
3515
+ releaseId,
3516
+ component: "reconcileReleaseReferences"
3517
+ }), error;
3518
+ }
3519
+ }, ReconcileReferencesButton = ({ sourceLanguage }) => {
3520
+ const { releaseConfig, overallStatus, config } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [documentCount, setDocumentCount] = useState(null), [isLoadingCount, setIsLoadingCount] = useState(!1), [reconcilingStatus, setReconcilingStatus] = useState("idle"), [reconciliationProgress, setReconciliationProgress] = useState(
3521
+ void 0
3522
+ ), releaseId = releaseConfig.existingReleaseId, languageFieldName = config?.languageFieldName ?? "language";
3523
+ useEffect(() => {
3524
+ if (!releaseId || releaseConfig.mode !== "existing") {
3525
+ setDocumentCount(null);
3526
+ return;
3527
+ }
3528
+ (async () => {
3529
+ setIsLoadingCount(!0);
3530
+ try {
3531
+ const count = await client.fetch('count(*[_id match "versions." + $releaseId + ".*"])', { releaseId });
3532
+ setDocumentCount(count), devLog.debug("Fetched release document count", {
3533
+ releaseId,
3534
+ count,
3535
+ component: "ReconcileReferencesButton"
3536
+ });
3537
+ } catch (error) {
3538
+ devLog.error(
3539
+ "Failed to fetch release document count",
3540
+ error instanceof Error ? error : new Error(String(error)),
3541
+ {
3542
+ releaseId,
3543
+ component: "ReconcileReferencesButton"
3544
+ }
3545
+ ), setDocumentCount(null);
3546
+ } finally {
3547
+ setIsLoadingCount(!1);
3548
+ }
3549
+ })();
3550
+ }, [releaseId, releaseConfig.mode, client]);
3551
+ const handleReconcile = useCallback(async () => {
3552
+ if (releaseId) {
3553
+ devLog.info("Starting reference reconciliation", {
3554
+ releaseId,
3555
+ sourceLanguage,
3556
+ component: "ReconcileReferencesButton"
3557
+ }), setReconcilingStatus("reconciling");
3558
+ try {
3559
+ const result = await reconcileReleaseReferences({
3560
+ client,
3561
+ releaseId,
3562
+ sourceLanguage,
3563
+ languageFieldName,
3564
+ onProgress: (current, total) => {
3565
+ setReconciliationProgress({ current, total });
3566
+ }
3567
+ });
3568
+ devLog.info("Reference reconciliation completed", {
3569
+ releaseId,
3570
+ ...result,
3571
+ component: "ReconcileReferencesButton"
3572
+ }), setReconcilingStatus("completed"), setTimeout(() => {
3573
+ setReconcilingStatus("idle"), setReconciliationProgress(void 0);
3574
+ }, 2e3);
3575
+ } catch (error) {
3576
+ devLog.error("Reference reconciliation failed", error instanceof Error ? error : new Error(String(error)), {
3577
+ releaseId,
3578
+ component: "ReconcileReferencesButton"
3579
+ }), setReconcilingStatus("error"), setReconciliationProgress(void 0);
3580
+ }
3581
+ }
3582
+ }, [releaseId, sourceLanguage, languageFieldName, client]);
3583
+ if (releaseConfig.mode !== "existing" || !releaseId)
3584
+ return null;
3585
+ const isTranslating = overallStatus === "translating" || overallStatus === "linking_references", isReconciling = reconcilingStatus === "reconciling";
3586
+ return /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, tone: reconcilingStatus === "error" ? "critical" : "default", children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
3587
+ /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", gap: 3, children: [
3588
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3589
+ /* @__PURE__ */ jsx(Text, { size: 1, weight: "medium", children: "Reconcile References" }),
3590
+ /* @__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: [
3591
+ documentCount,
3592
+ " document",
3593
+ documentCount !== 1 ? "s" : "",
3594
+ " in release"
3595
+ ] }) })
3596
+ ] }),
3597
+ /* @__PURE__ */ jsx(
3598
+ Button,
3599
+ {
3600
+ icon: isReconciling ? Spinner : SyncIcon,
3601
+ text: reconcilingStatus === "completed" ? "Done!" : reconcilingStatus === "error" ? "Retry" : isReconciling ? "Reconciling..." : "Reconcile",
3602
+ tone: reconcilingStatus === "completed" ? "positive" : reconcilingStatus === "error" ? "critical" : "default",
3603
+ mode: "ghost",
3604
+ onClick: handleReconcile,
3605
+ disabled: (isTranslating || isReconciling || isLoadingCount || documentCount === 0) && reconcilingStatus !== "error"
3606
+ }
3607
+ )
3608
+ ] }),
3609
+ isReconciling && reconciliationProgress && /* @__PURE__ */ jsx(Flex, { align: "center", gap: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
3610
+ "Processing ",
3611
+ reconciliationProgress.current,
3612
+ " of ",
3613
+ reconciliationProgress.total,
3614
+ "..."
3615
+ ] }) }),
3616
+ reconcilingStatus === "error" && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Reconciliation failed. Click Retry to try again." }),
3617
+ 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." })
3618
+ ] }) });
3108
3619
  }, ReleaseSelectionPanel = () => {
3109
- const { dispatch, releaseConfig } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [releases, setReleases] = useState([]), [isLoading, setIsLoading] = useState(!1), [error, setError] = useState(null), [searchQuery, setSearchQuery] = useState("");
3620
+ 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
3621
  useEffect(() => {
3111
3622
  (async () => {
3112
3623
  setIsLoading(!0), setError(null);
@@ -3219,12 +3730,33 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3219
3730
  radius: 2,
3220
3731
  tone: isSelected ? "primary" : "default",
3221
3732
  style: { cursor: "pointer" },
3222
- onClick: () => handleSelectRelease(releaseName),
3733
+ onClick: () => {
3734
+ handleSelectRelease(releaseName), isSelected || setShowReconcileUI(!1);
3735
+ },
3223
3736
  border: !0,
3224
3737
  muted: isSelected,
3225
3738
  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) })
3739
+ /* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", children: [
3740
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, children: [
3741
+ /* @__PURE__ */ jsx(Text, { size: 1, children: release.metadata.title }),
3742
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: formatDate(release._createdAt) })
3743
+ ] }),
3744
+ isSelected && sourceLanguage && /* @__PURE__ */ jsx(
3745
+ Button,
3746
+ {
3747
+ icon: SyncIcon,
3748
+ text: showReconcileUI ? "Hide" : "Reconcile References",
3749
+ mode: "bleed",
3750
+ tone: "primary",
3751
+ fontSize: 0,
3752
+ padding: 2,
3753
+ onClick: (e) => {
3754
+ e.stopPropagation(), setShowReconcileUI(!showReconcileUI);
3755
+ }
3756
+ }
3757
+ )
3758
+ ] }),
3759
+ isSelected && sourceLanguage && showReconcileUI && /* @__PURE__ */ jsx(ReconcileReferencesButton, { sourceLanguage })
3228
3760
  ] })
3229
3761
  },
3230
3762
  release._id
@@ -3451,13 +3983,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3451
3983
  /* @__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
3984
  const isSelected = selectedReferenceIds.has(reference._id), displayTitle = reference.title || reference._id;
3453
3985
  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
- ),
3986
+ /* @__PURE__ */ jsx(Checkbox, { checked: isSelected, onChange: () => handleToggleReference(reference._id) }),
3461
3987
  /* @__PURE__ */ jsxs(Flex, { flex: 1, align: "center", gap: 2, children: [
3462
3988
  /* @__PURE__ */ jsx(Text, { size: 1, children: displayTitle }),
3463
3989
  /* @__PURE__ */ jsx(Badge, { tone: "primary", fontSize: 0, children: reference._type })
@@ -3466,15 +3992,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3466
3992
  }) }) })
3467
3993
  ] }) });
3468
3994
  }, 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;
3995
+ 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
3996
  useEffect(() => {
3479
3997
  (async () => {
3480
3998
  if (setIsLoadingReferences(!0), !shouldTranslateReferences) {