@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.d.ts +204 -0
- package/dist/index.js +601 -83
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getPublishedId, pathToString, useClient, defineType, defineField, definePlugin } from "sanity";
|
|
2
2
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { WarningOutlineIcon, SearchIcon, CheckmarkIcon, ErrorOutlineIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
|
|
3
|
+
import { WarningOutlineIcon, SyncIcon, SearchIcon, CheckmarkIcon, ErrorOutlineIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
|
|
4
4
|
import { createContext, useReducer, useRef, useEffect, useContext, useMemo, useState, useCallback } from "react";
|
|
5
5
|
import defaults from "lodash.defaults";
|
|
6
6
|
import { Card, Stack, Flex, Text, Box, Heading, Button, Spinner, Inline, Checkbox, Grid, Radio, TextInput, Badge, Select, Label, Code, Tooltip } from "@sanity/ui";
|
|
@@ -122,6 +122,8 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
122
122
|
mode: "new",
|
|
123
123
|
newReleaseTitle: ""
|
|
124
124
|
},
|
|
125
|
+
reconcilingStatus: "idle",
|
|
126
|
+
reconciliationProgress: void 0,
|
|
125
127
|
dispatch: () => {
|
|
126
128
|
}
|
|
127
129
|
}), translationReducer = (context, action) => {
|
|
@@ -207,8 +209,24 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
207
209
|
releaseConfig: {
|
|
208
210
|
...context.releaseConfig,
|
|
209
211
|
conflicts: void 0,
|
|
210
|
-
replaceExisting: void 0
|
|
211
|
-
|
|
212
|
+
replaceExisting: void 0,
|
|
213
|
+
skipConflicts: void 0
|
|
214
|
+
},
|
|
215
|
+
// Reset reconciliation state
|
|
216
|
+
reconcilingStatus: "idle",
|
|
217
|
+
reconciliationProgress: void 0
|
|
218
|
+
};
|
|
219
|
+
case "SET_RECONCILING_STATUS":
|
|
220
|
+
return {
|
|
221
|
+
...context,
|
|
222
|
+
reconcilingStatus: action.payload,
|
|
223
|
+
// Clear progress when returning to idle or on error
|
|
224
|
+
reconciliationProgress: action.payload === "idle" || action.payload === "error" ? void 0 : context.reconciliationProgress
|
|
225
|
+
};
|
|
226
|
+
case "SET_RECONCILIATION_PROGRESS":
|
|
227
|
+
return {
|
|
228
|
+
...context,
|
|
229
|
+
reconciliationProgress: action.payload
|
|
212
230
|
};
|
|
213
231
|
default:
|
|
214
232
|
return context;
|
|
@@ -238,7 +256,10 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
238
256
|
releaseConfig: {
|
|
239
257
|
mode: "new",
|
|
240
258
|
newReleaseTitle: defaultReleaseTitle
|
|
241
|
-
}
|
|
259
|
+
},
|
|
260
|
+
// Reconciliation state for existing releases
|
|
261
|
+
reconcilingStatus: "idle",
|
|
262
|
+
reconciliationProgress: void 0
|
|
242
263
|
};
|
|
243
264
|
activeTranslation && devLog.info("Reconnecting to active translation", {
|
|
244
265
|
documentId: document._id,
|
|
@@ -505,7 +526,7 @@ const DEFAULT_RETRY_OPTIONS = {
|
|
|
505
526
|
logRetries: !0
|
|
506
527
|
};
|
|
507
528
|
function calculateDelay(attempt, options) {
|
|
508
|
-
const delay = options.initialDelayMs *
|
|
529
|
+
const delay = options.initialDelayMs * options.backoffMultiplier ** attempt;
|
|
509
530
|
return Math.min(delay, options.maxDelayMs);
|
|
510
531
|
}
|
|
511
532
|
function sleep(ms) {
|
|
@@ -924,6 +945,85 @@ const fetchTranslationPrompts = async ({
|
|
|
924
945
|
translatedCount: translationMap.size,
|
|
925
946
|
component: "updateReferences"
|
|
926
947
|
}));
|
|
948
|
+
}, updateBackwardReferences = async ({
|
|
949
|
+
client,
|
|
950
|
+
releaseId,
|
|
951
|
+
sourceDocumentIds,
|
|
952
|
+
targetLanguages,
|
|
953
|
+
sourceLanguage,
|
|
954
|
+
languageFieldName,
|
|
955
|
+
excludeDocumentIds
|
|
956
|
+
}) => {
|
|
957
|
+
if (!sourceDocumentIds.length || !targetLanguages.length)
|
|
958
|
+
return;
|
|
959
|
+
const normalizedSourceIds = sourceDocumentIds.map((id) => getPublishedId(id));
|
|
960
|
+
devLog.debug("Starting backward reference update", {
|
|
961
|
+
releaseId,
|
|
962
|
+
sourceDocumentCount: normalizedSourceIds.length,
|
|
963
|
+
targetLanguages,
|
|
964
|
+
excludeCount: excludeDocumentIds.length,
|
|
965
|
+
component: "updateBackwardReferences"
|
|
966
|
+
});
|
|
967
|
+
try {
|
|
968
|
+
for (const targetLanguage of targetLanguages) {
|
|
969
|
+
const existingDocsWithStaleRefs = await client.fetch(
|
|
970
|
+
`*[
|
|
971
|
+
_id match "versions." + $releaseId + ".*" &&
|
|
972
|
+
@[$languageFieldName] == $targetLanguage &&
|
|
973
|
+
!(_id in $excludeIds)
|
|
974
|
+
] {
|
|
975
|
+
_id,
|
|
976
|
+
"hasStaleRefs": count(*[
|
|
977
|
+
_id in $sourceDocumentIds &&
|
|
978
|
+
^references(@._id)
|
|
979
|
+
]) > 0
|
|
980
|
+
}[hasStaleRefs == true]`,
|
|
981
|
+
{
|
|
982
|
+
releaseId,
|
|
983
|
+
targetLanguage,
|
|
984
|
+
languageFieldName,
|
|
985
|
+
excludeIds: excludeDocumentIds,
|
|
986
|
+
sourceDocumentIds: normalizedSourceIds
|
|
987
|
+
}
|
|
988
|
+
);
|
|
989
|
+
if (existingDocsWithStaleRefs.length === 0) {
|
|
990
|
+
devLog.debug("No existing documents with stale references found", {
|
|
991
|
+
releaseId,
|
|
992
|
+
targetLanguage,
|
|
993
|
+
component: "updateBackwardReferences"
|
|
994
|
+
});
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
devLog.info("Found existing documents with stale references", {
|
|
998
|
+
releaseId,
|
|
999
|
+
targetLanguage,
|
|
1000
|
+
count: existingDocsWithStaleRefs.length,
|
|
1001
|
+
documentIds: existingDocsWithStaleRefs.map((d) => d._id),
|
|
1002
|
+
component: "updateBackwardReferences"
|
|
1003
|
+
}), await Promise.all(
|
|
1004
|
+
existingDocsWithStaleRefs.map(async ({ _id }) => {
|
|
1005
|
+
await localizeReferences({
|
|
1006
|
+
client,
|
|
1007
|
+
documentId: _id,
|
|
1008
|
+
sourceLanguage,
|
|
1009
|
+
languageFieldName
|
|
1010
|
+
});
|
|
1011
|
+
})
|
|
1012
|
+
), devLog.info("Updated backward references", {
|
|
1013
|
+
releaseId,
|
|
1014
|
+
targetLanguage,
|
|
1015
|
+
updatedCount: existingDocsWithStaleRefs.length,
|
|
1016
|
+
component: "updateBackwardReferences"
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
devLog.error("Error updating backward references", error instanceof Error ? error : new Error(String(error)), {
|
|
1021
|
+
releaseId,
|
|
1022
|
+
sourceDocumentIds: normalizedSourceIds,
|
|
1023
|
+
targetLanguages,
|
|
1024
|
+
component: "updateBackwardReferences"
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
927
1027
|
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages, mainSourceDocumentId) => {
|
|
928
1028
|
for (const sourceDocument of sourceDocuments)
|
|
929
1029
|
for (const lang of targetLanguages) {
|
|
@@ -939,6 +1039,57 @@ const fetchTranslationPrompts = async ({
|
|
|
939
1039
|
payload
|
|
940
1040
|
}), mainSourceDocumentId && updateActiveTranslationJob(mainSourceDocumentId, payload);
|
|
941
1041
|
}
|
|
1042
|
+
}, emitTelemetryEvent = (config, event) => {
|
|
1043
|
+
if (config?.onEvent)
|
|
1044
|
+
try {
|
|
1045
|
+
config.onEvent(event), devLog.debug("Telemetry event emitted", { eventName: event.name });
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
devLog.warn("Failed to emit telemetry event", {
|
|
1048
|
+
eventName: event.name,
|
|
1049
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
}, emitTelemetryError = (config, bundle) => {
|
|
1053
|
+
if (config?.onError)
|
|
1054
|
+
try {
|
|
1055
|
+
config.onError(bundle), devLog.debug("Telemetry error emitted", {
|
|
1056
|
+
documentId: bundle.documentId,
|
|
1057
|
+
errorType: bundle.error.type
|
|
1058
|
+
});
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
devLog.warn("Failed to emit telemetry error", {
|
|
1061
|
+
documentId: bundle.documentId,
|
|
1062
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}, buildErrorBundle = (params) => {
|
|
1066
|
+
const durationMs = Date.now() - params.startTime;
|
|
1067
|
+
return {
|
|
1068
|
+
documentId: params.documentId,
|
|
1069
|
+
documentType: params.documentType,
|
|
1070
|
+
targetLanguage: params.targetLanguage,
|
|
1071
|
+
sourceLanguage: params.sourceLanguage,
|
|
1072
|
+
translationMode: params.translationMode,
|
|
1073
|
+
error: {
|
|
1074
|
+
message: params.error.message,
|
|
1075
|
+
stack: params.error.stack,
|
|
1076
|
+
type: params.errorType
|
|
1077
|
+
},
|
|
1078
|
+
debug: {
|
|
1079
|
+
originalDocument: params.originalDocument,
|
|
1080
|
+
serializedDocument: params.serializedDocument,
|
|
1081
|
+
openaiResponse: params.openaiResponse,
|
|
1082
|
+
translationPrompts: params.translationPrompts,
|
|
1083
|
+
validationErrors: params.validationErrors
|
|
1084
|
+
},
|
|
1085
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1086
|
+
durationMs
|
|
1087
|
+
};
|
|
1088
|
+
}, createTimestamp = () => (/* @__PURE__ */ new Date()).toISOString(), classifyError = (error, context) => {
|
|
1089
|
+
if (context)
|
|
1090
|
+
return context;
|
|
1091
|
+
const message = error.message.toLowerCase();
|
|
1092
|
+
return message.includes("network") || message.includes("fetch") || message.includes("timeout") || message.includes("econnrefused") || message.includes("enotfound") ? "network" : message.includes("openai") || message.includes("gpt") || message.includes("api error") ? "openai_api" : message.includes("validation") || message.includes("structure") || message.includes("invalid") ? "validation" : message.includes("deserialize") || message.includes("parse") ? "deserialization" : message.includes("serialize") || message.includes("xml") ? "serialization" : message.includes("agent") || message.includes("sanity") ? "sanity_agent" : "unknown";
|
|
942
1093
|
}, updateTranslationMetadata = async ({
|
|
943
1094
|
client,
|
|
944
1095
|
targetLanguage,
|
|
@@ -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
|
-
}),
|
|
2357
|
+
}), {
|
|
2358
|
+
document: result,
|
|
2359
|
+
tokensUsed: response.tokensUsed.total,
|
|
2360
|
+
cost: response.cost
|
|
2361
|
+
};
|
|
2212
2362
|
} catch (error) {
|
|
2213
|
-
|
|
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
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
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 =
|
|
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 =
|
|
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
|
-
),
|
|
2835
|
+
), {
|
|
2836
|
+
document: retryNewDoc,
|
|
2837
|
+
tokensUsed: translationMetrics.tokensUsed,
|
|
2838
|
+
cost: translationMetrics.cost
|
|
2839
|
+
};
|
|
2624
2840
|
}
|
|
2625
2841
|
throw error;
|
|
2626
2842
|
}
|
|
2627
|
-
return
|
|
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),
|
|
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 (
|
|
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
|
|
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 = ({
|
|
2894
|
-
|
|
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: "
|
|
3231
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "space-between", children: [
|
|
2923
3232
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
|
|
2924
|
-
/* @__PURE__ */
|
|
3233
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", children: [
|
|
3234
|
+
/* @__PURE__ */ jsx(Button, { mode: "bleed", tone: "caution", text: "Replace All", onClick: onReplace }),
|
|
3235
|
+
/* @__PURE__ */ jsx(
|
|
3236
|
+
Button,
|
|
3237
|
+
{
|
|
3238
|
+
mode: "ghost",
|
|
3239
|
+
tone: "caution",
|
|
3240
|
+
text: "Skip Conflicts",
|
|
3241
|
+
onClick: onSkipConflicts,
|
|
3242
|
+
disabled: !hasNonConflicting
|
|
3243
|
+
}
|
|
3244
|
+
)
|
|
3245
|
+
] })
|
|
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
|
-
}, [
|
|
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: () =>
|
|
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__ */
|
|
3227
|
-
|
|
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) {
|