@braze/sanity-plugin-ai-translation 0.2.5 → 0.2.6
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/README.md +394 -191
- package/dist/index.d.ts +51 -0
- package/dist/index.js +992 -137
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getPublishedId, pathToString, useClient, defineType, defineField, definePlugin } from "sanity";
|
|
2
2
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { WarningOutlineIcon, SearchIcon, CheckmarkIcon, ErrorOutlineIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
|
|
4
|
-
import { createContext, useReducer, useEffect, useContext, useMemo, useState, useCallback } from "react";
|
|
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";
|
|
7
7
|
import { toPlainText } from "@portabletext/react";
|
|
@@ -62,7 +62,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
62
62
|
error: error instanceof Error ? error.message : "Network error"
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
}, activeTranslations = /* @__PURE__ */ new Map(), registerActiveTranslation = (documentId, releaseId) => {
|
|
65
|
+
}, undraftId = (id) => id.replace(/^drafts\./, ""), activeTranslations = /* @__PURE__ */ new Map(), registerActiveTranslation = (documentId, releaseId) => {
|
|
66
66
|
const entry = {
|
|
67
67
|
state: {
|
|
68
68
|
documentId,
|
|
@@ -80,7 +80,9 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
80
80
|
}, updateActiveTranslationJob = (documentId, job) => {
|
|
81
81
|
const entry = activeTranslations.get(documentId);
|
|
82
82
|
if (entry) {
|
|
83
|
-
const jobKey = `${job._id}_${job.language}`, existingJob = entry.state.translationJobs.get(jobKey),
|
|
83
|
+
const jobKey = `${undraftId(job._id)}_${job.language}`, existingJob = entry.state.translationJobs.get(jobKey), jobWithDefinedValues = Object.fromEntries(
|
|
84
|
+
Object.entries(job).filter(([_, value]) => value !== void 0)
|
|
85
|
+
), mergedJob = existingJob ? { ...existingJob, ...jobWithDefinedValues } : jobWithDefinedValues;
|
|
84
86
|
entry.state.translationJobs.set(jobKey, mergedJob), notifySubscribers(entry);
|
|
85
87
|
}
|
|
86
88
|
}, getActiveTranslation = (documentId) => activeTranslations.get(documentId)?.state ?? null, subscribeToActiveTranslation = (documentId, callback) => {
|
|
@@ -112,6 +114,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
112
114
|
shouldTranslateReferences: !1,
|
|
113
115
|
maxReferenceDepth: 1,
|
|
114
116
|
references: [],
|
|
117
|
+
selectedReferenceIds: /* @__PURE__ */ new Set(),
|
|
115
118
|
translationMetadata: null,
|
|
116
119
|
translationMode: "sanity-only",
|
|
117
120
|
openaiValidated: !1,
|
|
@@ -165,8 +168,18 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
165
168
|
...context,
|
|
166
169
|
references: action.payload
|
|
167
170
|
};
|
|
171
|
+
case "SET_SELECTED_REFERENCE_IDS":
|
|
172
|
+
return {
|
|
173
|
+
...context,
|
|
174
|
+
selectedReferenceIds: action.payload
|
|
175
|
+
};
|
|
168
176
|
case "SET_TRANSLATION_JOB_STATUS": {
|
|
169
|
-
|
|
177
|
+
if (!action.payload._id)
|
|
178
|
+
return devLog.warn("SET_TRANSLATION_JOB_STATUS action missing _id", {
|
|
179
|
+
...logContext,
|
|
180
|
+
payload: action.payload
|
|
181
|
+
}), context;
|
|
182
|
+
const jobKey = `${undraftId(action.payload._id)}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs), existingJob = newTranslationJobs.get(jobKey), mergedJob = defaults(action.payload, existingJob);
|
|
170
183
|
return newTranslationJobs.set(jobKey, mergedJob), {
|
|
171
184
|
...context,
|
|
172
185
|
translationJobs: newTranslationJobs
|
|
@@ -189,6 +202,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
189
202
|
...context,
|
|
190
203
|
overallStatus: "idle",
|
|
191
204
|
translationJobs: /* @__PURE__ */ new Map(),
|
|
205
|
+
selectedReferenceIds: /* @__PURE__ */ new Set(),
|
|
192
206
|
// Clear any release conflicts from previous run
|
|
193
207
|
releaseConfig: {
|
|
194
208
|
...context.releaseConfig,
|
|
@@ -216,6 +230,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
216
230
|
shouldTranslateReferences: !1,
|
|
217
231
|
maxReferenceDepth: 1,
|
|
218
232
|
references: [],
|
|
233
|
+
selectedReferenceIds: /* @__PURE__ */ new Set(),
|
|
219
234
|
// Restore jobs from active translation if available
|
|
220
235
|
translationJobs: activeTranslation?.translationJobs ?? /* @__PURE__ */ new Map(),
|
|
221
236
|
translationMode: config?.openai?.defaultMode || "sanity-only",
|
|
@@ -231,7 +246,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
231
246
|
jobCount: activeTranslation.translationJobs.size,
|
|
232
247
|
component: "TranslationContextProvider"
|
|
233
248
|
});
|
|
234
|
-
const [context, dispatch] = useReducer(translationReducer, initialContext);
|
|
249
|
+
const [context, dispatch] = useReducer(translationReducer, initialContext), lastProcessedRefsRef = useRef("");
|
|
235
250
|
return useEffect(() => {
|
|
236
251
|
const handleActiveTranslationUpdate = (state) => {
|
|
237
252
|
dispatch({
|
|
@@ -271,7 +286,28 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
271
286
|
});
|
|
272
287
|
}
|
|
273
288
|
})();
|
|
274
|
-
}, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]),
|
|
289
|
+
}, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), useEffect(() => {
|
|
290
|
+
const currentRefsKey = context.references.map((ref) => ref._id).sort().join(",");
|
|
291
|
+
if (currentRefsKey !== lastProcessedRefsRef.current) {
|
|
292
|
+
if (context.references.length > 0) {
|
|
293
|
+
const currentReferenceIds = new Set(context.references.map((ref) => ref._id)), previousReferenceIds = new Set(
|
|
294
|
+
lastProcessedRefsRef.current ? lastProcessedRefsRef.current.split(",") : []
|
|
295
|
+
), hasNewReferences = Array.from(currentReferenceIds).some((id) => !previousReferenceIds.has(id)), currentSelection = context.selectedReferenceIds;
|
|
296
|
+
if (currentSelection.size === 0 || hasNewReferences) {
|
|
297
|
+
const newSelection = new Set(currentSelection);
|
|
298
|
+
currentReferenceIds.forEach((id) => {
|
|
299
|
+
newSelection.add(id);
|
|
300
|
+
});
|
|
301
|
+
const currentIds = Array.from(currentSelection).sort(), newIds = Array.from(newSelection).sort();
|
|
302
|
+
(currentIds.length !== newIds.length || !currentIds.every((id, i) => id === newIds[i])) && dispatch({
|
|
303
|
+
type: "SET_SELECTED_REFERENCE_IDS",
|
|
304
|
+
payload: newSelection
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
lastProcessedRefsRef.current = currentRefsKey;
|
|
309
|
+
}
|
|
310
|
+
}, [context.references, context.selectedReferenceIds]), devLog.debug("Translation context state initialized", {
|
|
275
311
|
documentId: document._id,
|
|
276
312
|
overallStatus: context.overallStatus,
|
|
277
313
|
selectedLanguages: context.selectedTargetLanguages,
|
|
@@ -288,7 +324,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
288
324
|
return context;
|
|
289
325
|
}, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
290
326
|
(translation) => translation._key === targetLanguage
|
|
291
|
-
)?.value._ref || null,
|
|
327
|
+
)?.value._ref || null, TRANSLATION_METADATA_TYPE = "translation.metadata", getTranslationMetadata = async ({
|
|
292
328
|
client,
|
|
293
329
|
sourceDocument,
|
|
294
330
|
sourceLanguage
|
|
@@ -409,6 +445,115 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
409
445
|
}
|
|
410
446
|
return allConflicts;
|
|
411
447
|
};
|
|
448
|
+
function isNetworkError(error) {
|
|
449
|
+
if (!error) return !1;
|
|
450
|
+
if (error instanceof TypeError) {
|
|
451
|
+
const message = error.message.toLowerCase();
|
|
452
|
+
return message.includes("fetch") || message.includes("network") || message.includes("connection") || message.includes("failed to fetch") || message.includes("networkerror");
|
|
453
|
+
}
|
|
454
|
+
if (error && typeof error == "object" && "code" in error) {
|
|
455
|
+
const code = error.code;
|
|
456
|
+
return ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "ECONNREFUSED", "EHOSTUNREACH"].includes(code);
|
|
457
|
+
}
|
|
458
|
+
if (error && typeof error == "object" && "response" in error) {
|
|
459
|
+
const response = error.response;
|
|
460
|
+
if (response?.status)
|
|
461
|
+
return response.status >= 500 || response.status === 429;
|
|
462
|
+
}
|
|
463
|
+
if (error instanceof Error) {
|
|
464
|
+
const message = error.message;
|
|
465
|
+
if (/\b(50[0-9]|429)\b/.test(message))
|
|
466
|
+
return !0;
|
|
467
|
+
}
|
|
468
|
+
return !1;
|
|
469
|
+
}
|
|
470
|
+
function isSanityMutationError(error) {
|
|
471
|
+
if (!error) return !1;
|
|
472
|
+
if (error instanceof Error) {
|
|
473
|
+
const message = error.message.toLowerCase();
|
|
474
|
+
return [
|
|
475
|
+
"validation",
|
|
476
|
+
"schema",
|
|
477
|
+
"invalid document",
|
|
478
|
+
"invalid field",
|
|
479
|
+
"type mismatch",
|
|
480
|
+
"reference",
|
|
481
|
+
"patch",
|
|
482
|
+
"mutation",
|
|
483
|
+
"document structure",
|
|
484
|
+
"required field",
|
|
485
|
+
"field type",
|
|
486
|
+
"sanity"
|
|
487
|
+
].some((pattern) => message.includes(pattern));
|
|
488
|
+
}
|
|
489
|
+
if (error && typeof error == "object") {
|
|
490
|
+
const errorObj = error;
|
|
491
|
+
if (("statusCode" in errorObj || "response" in errorObj || "details" in errorObj) && "statusCode" in errorObj) {
|
|
492
|
+
const statusCode = errorObj.statusCode;
|
|
493
|
+
if (statusCode >= 400 && statusCode < 500 && statusCode !== 429)
|
|
494
|
+
return !0;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return !1;
|
|
498
|
+
}
|
|
499
|
+
const DEFAULT_RETRY_OPTIONS = {
|
|
500
|
+
maxAttempts: 2,
|
|
501
|
+
initialDelayMs: 0,
|
|
502
|
+
maxDelayMs: 5e3,
|
|
503
|
+
backoffMultiplier: 2,
|
|
504
|
+
retryableErrors: isNetworkError,
|
|
505
|
+
logRetries: !0
|
|
506
|
+
};
|
|
507
|
+
function calculateDelay(attempt, options) {
|
|
508
|
+
const delay = options.initialDelayMs * Math.pow(options.backoffMultiplier, attempt);
|
|
509
|
+
return Math.min(delay, options.maxDelayMs);
|
|
510
|
+
}
|
|
511
|
+
function sleep(ms) {
|
|
512
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
513
|
+
}
|
|
514
|
+
async function retryWithBackoff(operation, options = {}) {
|
|
515
|
+
const opts = {
|
|
516
|
+
...DEFAULT_RETRY_OPTIONS,
|
|
517
|
+
...options,
|
|
518
|
+
retryableErrors: options.retryableErrors || DEFAULT_RETRY_OPTIONS.retryableErrors
|
|
519
|
+
};
|
|
520
|
+
let lastError;
|
|
521
|
+
const maxAttempts = opts.maxAttempts + 1;
|
|
522
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++)
|
|
523
|
+
try {
|
|
524
|
+
const result = await operation();
|
|
525
|
+
return attempt > 1 && opts.logRetries && devLog.info(`Operation succeeded after ${attempt} attempts`, {
|
|
526
|
+
...opts.context,
|
|
527
|
+
attempt,
|
|
528
|
+
totalAttempts: attempt
|
|
529
|
+
}), result;
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (lastError = error, !opts.retryableErrors(error))
|
|
532
|
+
throw opts.logRetries && devLog.warn("Error is not retryable, aborting retries", {
|
|
533
|
+
...opts.context,
|
|
534
|
+
attempt,
|
|
535
|
+
error: error instanceof Error ? error.message : String(error)
|
|
536
|
+
}), error;
|
|
537
|
+
if (attempt >= maxAttempts)
|
|
538
|
+
throw opts.logRetries && devLog.error(
|
|
539
|
+
`Operation failed after ${maxAttempts} attempts`,
|
|
540
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
541
|
+
{
|
|
542
|
+
...opts.context,
|
|
543
|
+
totalAttempts: maxAttempts
|
|
544
|
+
}
|
|
545
|
+
), error;
|
|
546
|
+
const delay = calculateDelay(attempt - 1, opts);
|
|
547
|
+
opts.logRetries && devLog.warn(`Operation failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {
|
|
548
|
+
...opts.context,
|
|
549
|
+
attempt,
|
|
550
|
+
totalAttempts: maxAttempts,
|
|
551
|
+
delayMs: delay,
|
|
552
|
+
error: error instanceof Error ? error.message : String(error)
|
|
553
|
+
}), delay > 0 && await sleep(delay);
|
|
554
|
+
}
|
|
555
|
+
throw lastError;
|
|
556
|
+
}
|
|
412
557
|
class Semaphore {
|
|
413
558
|
permits;
|
|
414
559
|
queue = [];
|
|
@@ -759,7 +904,21 @@ const fetchTranslationPrompts = async ({
|
|
|
759
904
|
}
|
|
760
905
|
}), hasChanges = !0);
|
|
761
906
|
}
|
|
762
|
-
hasChanges && (await
|
|
907
|
+
hasChanges && (await retryWithBackoff(
|
|
908
|
+
async () => {
|
|
909
|
+
await patch.commit();
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
maxAttempts: 2,
|
|
913
|
+
initialDelayMs: 0,
|
|
914
|
+
retryableErrors: isNetworkError,
|
|
915
|
+
context: {
|
|
916
|
+
operation: "localizeReferences",
|
|
917
|
+
documentId,
|
|
918
|
+
referenceCount: refs.length
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
), devLog.info("Updated references in batch", {
|
|
763
922
|
documentId,
|
|
764
923
|
count: refs.length,
|
|
765
924
|
translatedCount: translationMap.size,
|
|
@@ -813,11 +972,71 @@ const fetchTranslationPrompts = async ({
|
|
|
813
972
|
targetRev: newDocument._rev,
|
|
814
973
|
targetSnapshot: JSON.stringify(newDocument)
|
|
815
974
|
}, translationPatch = client.patch(translationMetadata._id).setIfMissing({ translations: [] });
|
|
816
|
-
isNewTranslation ? await
|
|
975
|
+
isNewTranslation ? await retryWithBackoff(
|
|
976
|
+
async () => {
|
|
977
|
+
await translationPatch.insert("after", "translations[-1]", [refToNewTranslation]).commit();
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
maxAttempts: 2,
|
|
981
|
+
initialDelayMs: 0,
|
|
982
|
+
retryableErrors: isNetworkError,
|
|
983
|
+
context: {
|
|
984
|
+
operation: "updateTranslationMetadata.translations",
|
|
985
|
+
sourceDocumentId: sourceDocument._id,
|
|
986
|
+
targetLanguage,
|
|
987
|
+
isNewTranslation: !0
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
) : await retryWithBackoff(
|
|
991
|
+
async () => {
|
|
992
|
+
await translationPatch.insert("replace", `translations[_key == "${targetLanguage}"]`, [refToNewTranslation]).commit();
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
maxAttempts: 2,
|
|
996
|
+
initialDelayMs: 0,
|
|
997
|
+
retryableErrors: isNetworkError,
|
|
998
|
+
context: {
|
|
999
|
+
operation: "updateTranslationMetadata.translations",
|
|
1000
|
+
sourceDocumentId: sourceDocument._id,
|
|
1001
|
+
targetLanguage,
|
|
1002
|
+
isNewTranslation: !1
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
);
|
|
817
1006
|
const historyPatch = client.patch(translationMetadata._id).setIfMissing({ translationHistory: [] });
|
|
818
1007
|
translationMetadata.translationHistory?.some(
|
|
819
1008
|
(history) => history.language === targetLanguage
|
|
820
|
-
) ? await
|
|
1009
|
+
) ? await retryWithBackoff(
|
|
1010
|
+
async () => {
|
|
1011
|
+
await historyPatch.insert("replace", `translationHistory[_key == "${targetLanguage}"]`, [newTranslationHistory]).commit();
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
maxAttempts: 2,
|
|
1015
|
+
initialDelayMs: 0,
|
|
1016
|
+
retryableErrors: isNetworkError,
|
|
1017
|
+
context: {
|
|
1018
|
+
operation: "updateTranslationMetadata.history",
|
|
1019
|
+
sourceDocumentId: sourceDocument._id,
|
|
1020
|
+
targetLanguage,
|
|
1021
|
+
isNewHistory: !1
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
) : await retryWithBackoff(
|
|
1025
|
+
async () => {
|
|
1026
|
+
await historyPatch.insert("after", "translationHistory[-1]", [newTranslationHistory]).commit();
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
maxAttempts: 2,
|
|
1030
|
+
initialDelayMs: 0,
|
|
1031
|
+
retryableErrors: isNetworkError,
|
|
1032
|
+
context: {
|
|
1033
|
+
operation: "updateTranslationMetadata.history",
|
|
1034
|
+
sourceDocumentId: sourceDocument._id,
|
|
1035
|
+
targetLanguage,
|
|
1036
|
+
isNewHistory: !0
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
821
1040
|
} catch (error) {
|
|
822
1041
|
throw devLog.error("Error updating translation metadata", error instanceof Error ? error : new Error(String(error)), {
|
|
823
1042
|
sourceDocumentId: sourceDocument._id,
|
|
@@ -1068,7 +1287,9 @@ function restoreTransforms({
|
|
|
1068
1287
|
schemaType,
|
|
1069
1288
|
document: translatedDocument
|
|
1070
1289
|
};
|
|
1071
|
-
devLog.debug("Restoring transform for field", { fieldPath, schemaType })
|
|
1290
|
+
devLog.debug("Restoring transform for field", { fieldPath, schemaType });
|
|
1291
|
+
const restoredValue = transform.fromTranslatable(value, originalValue, context);
|
|
1292
|
+
restoredObj[key] = restoredValue;
|
|
1072
1293
|
} else
|
|
1073
1294
|
restoredObj[key] = restoreTransforms({
|
|
1074
1295
|
translatedDocument: value,
|
|
@@ -1161,14 +1382,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1161
1382
|
}), devLog.debug("OpenAI GPT-5 Full Prompt", {
|
|
1162
1383
|
messages: request.messages
|
|
1163
1384
|
});
|
|
1164
|
-
const response = await
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1385
|
+
const response = await retryWithBackoff(
|
|
1386
|
+
async () => {
|
|
1387
|
+
const fetchResponse = await fetch("https://api.openai.com/v1/responses", {
|
|
1388
|
+
method: "POST",
|
|
1389
|
+
headers: {
|
|
1390
|
+
"Content-Type": "application/json",
|
|
1391
|
+
Authorization: `Bearer ${apiKey}`
|
|
1392
|
+
},
|
|
1393
|
+
body: JSON.stringify(requestBody)
|
|
1394
|
+
});
|
|
1395
|
+
if (!fetchResponse.ok) {
|
|
1396
|
+
const errorMessage = `GPT-5 API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
|
|
1397
|
+
throw errorObj.response = { status: fetchResponse.status }, errorObj;
|
|
1398
|
+
}
|
|
1399
|
+
return fetchResponse;
|
|
1169
1400
|
},
|
|
1170
|
-
|
|
1171
|
-
|
|
1401
|
+
{
|
|
1402
|
+
maxAttempts: 2,
|
|
1403
|
+
initialDelayMs: 0,
|
|
1404
|
+
retryableErrors: isNetworkError,
|
|
1405
|
+
context: { model: requestBody.model, endpoint: "responses" }
|
|
1406
|
+
}
|
|
1407
|
+
);
|
|
1172
1408
|
if (!response.ok) {
|
|
1173
1409
|
const error = await response.json();
|
|
1174
1410
|
throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
|
|
@@ -1207,14 +1443,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1207
1443
|
}), devLog.debug("OpenAI Chat Completions Full Prompt", {
|
|
1208
1444
|
messages: request.messages
|
|
1209
1445
|
});
|
|
1210
|
-
const response = await
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1446
|
+
const response = await retryWithBackoff(
|
|
1447
|
+
async () => {
|
|
1448
|
+
const fetchResponse = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1449
|
+
method: "POST",
|
|
1450
|
+
headers: {
|
|
1451
|
+
"Content-Type": "application/json",
|
|
1452
|
+
Authorization: `Bearer ${apiKey}`
|
|
1453
|
+
},
|
|
1454
|
+
body: JSON.stringify(requestBody)
|
|
1455
|
+
});
|
|
1456
|
+
if (!fetchResponse.ok) {
|
|
1457
|
+
const errorMessage = `OpenAI API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
|
|
1458
|
+
throw errorObj.response = { status: fetchResponse.status }, errorObj;
|
|
1459
|
+
}
|
|
1460
|
+
return fetchResponse;
|
|
1215
1461
|
},
|
|
1216
|
-
|
|
1217
|
-
|
|
1462
|
+
{
|
|
1463
|
+
maxAttempts: 2,
|
|
1464
|
+
initialDelayMs: 0,
|
|
1465
|
+
retryableErrors: isNetworkError,
|
|
1466
|
+
context: { model, endpoint: "chat/completions" }
|
|
1467
|
+
}
|
|
1468
|
+
);
|
|
1218
1469
|
if (!response.ok) {
|
|
1219
1470
|
const error = await response.json();
|
|
1220
1471
|
throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
|
|
@@ -1276,16 +1527,63 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1276
1527
|
}, modelPricing = pricing[model] || pricing["gpt-5"], cachedInputRate = model === "gpt-5.1" ? 0.125 / 1e6 : modelPricing.input;
|
|
1277
1528
|
return Math.max(0, tokens.prompt - tokens.cached) * modelPricing.input + tokens.cached * cachedInputRate + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
|
|
1278
1529
|
}, deserializeDocument = (originalDoc, reviewedText) => {
|
|
1279
|
-
|
|
1530
|
+
devLog.debug("Raw GPT-5 response received", {
|
|
1531
|
+
length: reviewedText.length,
|
|
1532
|
+
preview: reviewedText.substring(0, 1e3),
|
|
1533
|
+
hasReasoning: reviewedText.includes("<reasoning>"),
|
|
1534
|
+
hasCodeBlocks: reviewedText.includes("```")
|
|
1535
|
+
});
|
|
1536
|
+
const cleanedText = preprocessResponse(reviewedText);
|
|
1537
|
+
cleanedText.length !== reviewedText.length && devLog.debug("Response after preprocessing", {
|
|
1538
|
+
originalLength: reviewedText.length,
|
|
1539
|
+
cleanedLength: cleanedText.length,
|
|
1540
|
+
preview: cleanedText.substring(0, 500)
|
|
1541
|
+
});
|
|
1542
|
+
const parsed = parseReviewFormat(cleanedText, originalDoc);
|
|
1543
|
+
devLog.debug("Parsed response structure", {
|
|
1544
|
+
rootNodeCount: parsed.length,
|
|
1545
|
+
rootNodeNames: parsed.map((n) => n.type === "field" && n.name ? n.name : "unknown")
|
|
1546
|
+
});
|
|
1547
|
+
const result = cloneDeep(originalDoc);
|
|
1280
1548
|
return applyTextChanges(result, parsed), result;
|
|
1281
|
-
},
|
|
1549
|
+
}, preprocessResponse = (text) => {
|
|
1550
|
+
const originalLength = text.length;
|
|
1551
|
+
let cleaned = text;
|
|
1552
|
+
const beforeReasoning = cleaned.length;
|
|
1553
|
+
cleaned = cleaned.replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, "");
|
|
1554
|
+
const reasoningRemoved = beforeReasoning !== cleaned.length, beforeCodeBlocks = cleaned.length;
|
|
1555
|
+
cleaned = cleaned.replace(/^```[a-z]*\s*$/gim, ""), cleaned = cleaned.replace(/^```\s*$/gim, "");
|
|
1556
|
+
const codeBlocksRemoved = beforeCodeBlocks !== cleaned.length, finalLength = cleaned.trim().length;
|
|
1557
|
+
return (reasoningRemoved || codeBlocksRemoved) && devLog.debug("Preprocessed GPT-5 response", {
|
|
1558
|
+
originalLength,
|
|
1559
|
+
finalLength,
|
|
1560
|
+
reasoningRemoved,
|
|
1561
|
+
codeBlocksRemoved,
|
|
1562
|
+
preview: cleaned.trim().substring(0, 200)
|
|
1563
|
+
}), cleaned.trim();
|
|
1564
|
+
}, parseReviewFormat = (text, originalDoc) => {
|
|
1282
1565
|
const root = [], stack = [], lines = text.split(`
|
|
1283
1566
|
`);
|
|
1567
|
+
devLog.debug("Parsing response text", {
|
|
1568
|
+
lineCount: lines.length,
|
|
1569
|
+
firstLines: lines.slice(0, 10).map((l, idx) => ({ line: idx, content: l.substring(0, 100) })),
|
|
1570
|
+
hasFieldTags: text.includes("<field"),
|
|
1571
|
+
hasBlockTags: text.includes("<block"),
|
|
1572
|
+
hasSpanTags: text.includes("<span"),
|
|
1573
|
+
textLength: text.length,
|
|
1574
|
+
textPreview: text.substring(0, 500)
|
|
1575
|
+
});
|
|
1576
|
+
let processedLineCount = 0, skippedEmptyLineCount = 0, matchedPatternCount = 0;
|
|
1284
1577
|
for (let i = 0; i < lines.length; i++) {
|
|
1285
1578
|
const trimmed = lines[i].trim();
|
|
1286
|
-
if (!trimmed)
|
|
1579
|
+
if (!trimmed) {
|
|
1580
|
+
skippedEmptyLineCount++;
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
processedLineCount++;
|
|
1287
1584
|
const compactStringMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
1288
1585
|
if (compactStringMatch && !trimmed.includes("<")) {
|
|
1586
|
+
matchedPatternCount++;
|
|
1289
1587
|
const [, fieldName, text2] = compactStringMatch, node = {
|
|
1290
1588
|
type: "field",
|
|
1291
1589
|
name: fieldName,
|
|
@@ -1302,6 +1600,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1302
1600
|
}
|
|
1303
1601
|
const compactStartMatch = trimmed.match(/^---\s+(\w+)\s+---$/);
|
|
1304
1602
|
if (compactStartMatch) {
|
|
1603
|
+
matchedPatternCount++;
|
|
1305
1604
|
const fieldName = compactStartMatch[1], contentLines = [];
|
|
1306
1605
|
for (i++; i < lines.length; ) {
|
|
1307
1606
|
const currentLine = lines[i];
|
|
@@ -1311,8 +1610,16 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1311
1610
|
}
|
|
1312
1611
|
const content = contentLines.join(`
|
|
1313
1612
|
`);
|
|
1314
|
-
let contentType
|
|
1315
|
-
|
|
1613
|
+
let contentType;
|
|
1614
|
+
try {
|
|
1615
|
+
const fieldValue = get(originalDoc, [fieldName]);
|
|
1616
|
+
if (fieldValue && typeof fieldValue == "object" && fieldValue !== null) {
|
|
1617
|
+
const fieldType = fieldValue._type;
|
|
1618
|
+
typeof fieldType == "string" && fieldType.startsWith("translatable.") && (contentType = fieldType);
|
|
1619
|
+
}
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1622
|
+
contentType || (content.includes("<block") || content.includes("<span") ? contentType = "translatable.richtext" : contentType = "translatable.vtt.captions");
|
|
1316
1623
|
const node = {
|
|
1317
1624
|
type: "field",
|
|
1318
1625
|
name: fieldName,
|
|
@@ -1329,6 +1636,12 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1329
1636
|
}
|
|
1330
1637
|
const openMatch = trimmed.match(/^<(field|block|span)([^>]*)>(.*)$/);
|
|
1331
1638
|
if (openMatch) {
|
|
1639
|
+
matchedPatternCount++, devLog.debug("Matched opening tag", {
|
|
1640
|
+
line: i,
|
|
1641
|
+
tagName: openMatch[1],
|
|
1642
|
+
attributes: openMatch[2],
|
|
1643
|
+
contentPreview: openMatch[3]?.substring(0, 50)
|
|
1644
|
+
});
|
|
1332
1645
|
const [, tagName, attrsStr, content] = openMatch, attributes = parseAttributes(attrsStr), node = {
|
|
1333
1646
|
type: tagName,
|
|
1334
1647
|
name: attributes.name,
|
|
@@ -1349,7 +1662,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1349
1662
|
parent && parent.children.push(node);
|
|
1350
1663
|
} else
|
|
1351
1664
|
root.push(node);
|
|
1352
|
-
else if (attributes.type
|
|
1665
|
+
else if (attributes.type && typeof attributes.type == "string" && attributes.type.startsWith("translatable.")) {
|
|
1353
1666
|
const contentLines = [];
|
|
1354
1667
|
for (i++; i < lines.length; ) {
|
|
1355
1668
|
const currentLine = lines[i];
|
|
@@ -1388,6 +1701,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1388
1701
|
continue;
|
|
1389
1702
|
}
|
|
1390
1703
|
if (trimmed.match(/^<\/(field|block|span)>$/) && stack.length > 0) {
|
|
1704
|
+
matchedPatternCount++;
|
|
1391
1705
|
const node = stack.pop();
|
|
1392
1706
|
if (node)
|
|
1393
1707
|
if (stack.length > 0) {
|
|
@@ -1395,9 +1709,24 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1395
1709
|
parent && parent.children.push(node);
|
|
1396
1710
|
} else
|
|
1397
1711
|
root.push(node);
|
|
1712
|
+
continue;
|
|
1398
1713
|
}
|
|
1714
|
+
processedLineCount <= 20 && devLog.debug("Line did not match any pattern", {
|
|
1715
|
+
line: i,
|
|
1716
|
+
trimmed,
|
|
1717
|
+
startsWithLessThan: trimmed.startsWith("<"),
|
|
1718
|
+
startsWithField: trimmed.startsWith("<field"),
|
|
1719
|
+
startsWithBacktick: trimmed.startsWith("`")
|
|
1720
|
+
});
|
|
1399
1721
|
}
|
|
1400
|
-
return
|
|
1722
|
+
return devLog.debug("Parse complete", {
|
|
1723
|
+
totalLines: lines.length,
|
|
1724
|
+
processedLines: processedLineCount,
|
|
1725
|
+
skippedEmptyLines: skippedEmptyLineCount,
|
|
1726
|
+
rootNodeCount: root.length,
|
|
1727
|
+
stackLength: stack.length,
|
|
1728
|
+
matchedPatterns: matchedPatternCount
|
|
1729
|
+
}), root;
|
|
1401
1730
|
}, parseAttributes = (attrStr) => {
|
|
1402
1731
|
const attrs = {}, attrRegex = /(\w+)="([^"]*)"/g;
|
|
1403
1732
|
let match = attrRegex.exec(attrStr);
|
|
@@ -1405,20 +1734,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1405
1734
|
attrs[match[1]] = unescapeXml(match[2]), match = attrRegex.exec(attrStr);
|
|
1406
1735
|
return attrs;
|
|
1407
1736
|
}, unescapeXml = (str) => str.replace(/'/g, "'").replace(/"/g, '"').replace(/>/g, ">").replace(/</g, "<").replace(/&/g, "&"), applyTextChanges = (doc, parsedNodes) => {
|
|
1737
|
+
devLog.debug("Applying text changes from parsed nodes", {
|
|
1738
|
+
nodeCount: parsedNodes.length,
|
|
1739
|
+
nodeNames: parsedNodes.map((n) => n.type === "field" && n.name ? n.name : "unknown")
|
|
1740
|
+
});
|
|
1408
1741
|
for (const node of parsedNodes)
|
|
1409
1742
|
if (node.type === "field" && node.name) {
|
|
1410
|
-
const path = [node.name];
|
|
1743
|
+
const path = [node.name], beforeValue = get(doc, path);
|
|
1411
1744
|
applyFieldChanges(doc, path, node);
|
|
1745
|
+
const afterValue = get(doc, path);
|
|
1746
|
+
beforeValue !== afterValue && devLog.debug("Field updated during deserialization", {
|
|
1747
|
+
path: path.join("."),
|
|
1748
|
+
before: typeof beforeValue == "string" ? beforeValue.substring(0, 50) : beforeValue,
|
|
1749
|
+
after: typeof afterValue == "string" ? afterValue.substring(0, 50) : afterValue
|
|
1750
|
+
});
|
|
1412
1751
|
}
|
|
1413
1752
|
}, applyFieldChanges = (doc, path, node) => {
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.vtt.captions" && node.text && set(doc, [...path, "translationText"], node.text);
|
|
1417
|
-
return;
|
|
1418
|
-
}
|
|
1419
|
-
if (node.attributes.type === "translatable.richtext") {
|
|
1753
|
+
const nodeType = node.attributes.type;
|
|
1754
|
+
if (typeof nodeType == "string" && nodeType.startsWith("translatable.")) {
|
|
1420
1755
|
const fieldValue = get(doc, path);
|
|
1421
|
-
fieldValue && typeof fieldValue == "object" && fieldValue
|
|
1756
|
+
if (fieldValue && typeof fieldValue == "object" && fieldValue !== null) {
|
|
1757
|
+
const fieldType = fieldValue._type;
|
|
1758
|
+
typeof fieldType == "string" && fieldType === nodeType && node.text && set(doc, [...path, "translationText"], node.text);
|
|
1759
|
+
}
|
|
1422
1760
|
return;
|
|
1423
1761
|
}
|
|
1424
1762
|
if (node.attributes.type === "array") {
|
|
@@ -1506,12 +1844,9 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1506
1844
|
return;
|
|
1507
1845
|
}
|
|
1508
1846
|
if (isPlainObject(value)) {
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
}
|
|
1513
|
-
if (value._type === "translatable.richtext" && typeof value.translationText == "string") {
|
|
1514
|
-
lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
|
|
1847
|
+
const valueObj = value, valueType = valueObj._type, translationText = valueObj.translationText;
|
|
1848
|
+
if (typeof valueType == "string" && valueType.startsWith("translatable.") && typeof translationText == "string") {
|
|
1849
|
+
lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
|
|
1515
1850
|
return;
|
|
1516
1851
|
}
|
|
1517
1852
|
lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
|
|
@@ -1647,7 +1982,9 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1647
1982
|
languageFieldName,
|
|
1648
1983
|
translationPrompts,
|
|
1649
1984
|
targetLanguage,
|
|
1650
|
-
openaiConfig
|
|
1985
|
+
openaiConfig,
|
|
1986
|
+
previousError,
|
|
1987
|
+
errorContext
|
|
1651
1988
|
}) => {
|
|
1652
1989
|
const sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
1653
1990
|
sourceId: sourceDocument._id,
|
|
@@ -1665,7 +2002,8 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1665
2002
|
sourceText,
|
|
1666
2003
|
sourceLanguage,
|
|
1667
2004
|
targetLanguage,
|
|
1668
|
-
translationPrompts
|
|
2005
|
+
translationPrompts,
|
|
2006
|
+
errorContext
|
|
1669
2007
|
}), response = await callGPT5(openaiConfig.apiKey, {
|
|
1670
2008
|
messages: [
|
|
1671
2009
|
{
|
|
@@ -1723,14 +2061,21 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1723
2061
|
sourceText,
|
|
1724
2062
|
sourceLanguage,
|
|
1725
2063
|
targetLanguage,
|
|
1726
|
-
translationPrompts
|
|
2064
|
+
translationPrompts,
|
|
2065
|
+
errorContext
|
|
1727
2066
|
}) => {
|
|
1728
2067
|
const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
|
|
1729
2068
|
return `# TRANSLATION TASK
|
|
1730
2069
|
|
|
1731
2070
|
Translate this ${sourceLanguage} document into ${targetLanguage}.
|
|
1732
2071
|
|
|
1733
|
-
|
|
2072
|
+
${errorContext ? `## \u26A0\uFE0F IMPORTANT: Previous Attempt Error
|
|
2073
|
+
|
|
2074
|
+
${errorContext}
|
|
2075
|
+
|
|
2076
|
+
Please carefully review the error above and ensure your translation fixes the issue.
|
|
2077
|
+
|
|
2078
|
+
` : ""}## Guidelines
|
|
1734
2079
|
|
|
1735
2080
|
${globalPrompt ? `### General Guidelines
|
|
1736
2081
|
${globalPrompt}
|
|
@@ -1904,21 +2249,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
1904
2249
|
languageFieldName,
|
|
1905
2250
|
targetLanguage
|
|
1906
2251
|
});
|
|
1907
|
-
return await
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2252
|
+
return await retryWithBackoff(
|
|
2253
|
+
async () => await client.agent.action.translate({
|
|
2254
|
+
schemaId,
|
|
2255
|
+
toLanguage,
|
|
2256
|
+
fromLanguage,
|
|
2257
|
+
documentId: sourceDocument._id,
|
|
2258
|
+
noWrite: !0,
|
|
2259
|
+
// Do not write to the document, just return the translated content
|
|
2260
|
+
languageFieldPath: [languageFieldName],
|
|
2261
|
+
styleGuide,
|
|
2262
|
+
protectedPhrases: translationPrompts.protectedPhrases,
|
|
2263
|
+
conditionalPaths: {
|
|
2264
|
+
defaultHidden: !1,
|
|
2265
|
+
defaultReadOnly: !1
|
|
2266
|
+
}
|
|
2267
|
+
}),
|
|
2268
|
+
{
|
|
2269
|
+
maxAttempts: 2,
|
|
2270
|
+
initialDelayMs: 0,
|
|
2271
|
+
retryableErrors: isNetworkError,
|
|
2272
|
+
context: {
|
|
2273
|
+
operation: "agent.action.translate",
|
|
2274
|
+
documentId: sourceDocument._id,
|
|
2275
|
+
targetLanguage,
|
|
2276
|
+
schemaId
|
|
2277
|
+
}
|
|
1920
2278
|
}
|
|
1921
|
-
|
|
2279
|
+
);
|
|
1922
2280
|
}, handleTranslation = async ({
|
|
1923
2281
|
dispatch,
|
|
1924
2282
|
client,
|
|
@@ -1974,43 +2332,44 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
1974
2332
|
documentId: sourceDocument._id,
|
|
1975
2333
|
documentType: sourceDocument._type
|
|
1976
2334
|
}), documentToTranslate = filteredDocument;
|
|
1977
|
-
let translatedDoc;
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
sourceDocument
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2335
|
+
let translatedDoc, previousError = null;
|
|
2336
|
+
const performTranslation = async (errorContext) => {
|
|
2337
|
+
switch (translationMode) {
|
|
2338
|
+
case "sanity-only":
|
|
2339
|
+
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating", void 0, mainSourceDocumentId), await getSanityAgentTranslation({
|
|
2340
|
+
sourceDocument: documentToTranslate,
|
|
2341
|
+
targetLanguage,
|
|
2342
|
+
languageFieldName,
|
|
2343
|
+
schemaId,
|
|
2344
|
+
client,
|
|
2345
|
+
translationPrompts
|
|
2346
|
+
});
|
|
2347
|
+
case "openai-only": {
|
|
2348
|
+
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2349
|
+
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2350
|
+
return setJobStatus(
|
|
2351
|
+
dispatch,
|
|
2352
|
+
sourceDocument._id,
|
|
2353
|
+
targetLanguage,
|
|
2354
|
+
"translating_with_openai",
|
|
2355
|
+
void 0,
|
|
2356
|
+
mainSourceDocumentId
|
|
2357
|
+
), await getOpenAITranslation({
|
|
2358
|
+
dispatch,
|
|
2359
|
+
sourceDocument: documentToTranslate,
|
|
2360
|
+
targetLanguage,
|
|
2361
|
+
languageFieldName,
|
|
2362
|
+
translationPrompts,
|
|
2363
|
+
openaiConfig,
|
|
2364
|
+
previousError: errorContext && previousError ? previousError : void 0,
|
|
2365
|
+
errorContext
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
default:
|
|
2369
|
+
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2009
2370
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
}
|
|
2013
|
-
if (!translatedDoc)
|
|
2371
|
+
};
|
|
2372
|
+
if (translatedDoc = await performTranslation(), !translatedDoc)
|
|
2014
2373
|
return null;
|
|
2015
2374
|
let transformedResult;
|
|
2016
2375
|
Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
|
|
@@ -2041,15 +2400,76 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2041
2400
|
...finalDoc,
|
|
2042
2401
|
_id: targetDocumentVersionId
|
|
2043
2402
|
};
|
|
2044
|
-
|
|
2403
|
+
replaceExisting && await client.getDocument(targetDocumentVersionId) && (devLog.info("Replacing existing version in release", {
|
|
2045
2404
|
documentId: sourceDocument._id,
|
|
2046
2405
|
targetDocumentVersionId,
|
|
2047
2406
|
releaseId
|
|
2048
|
-
}), await client.delete(targetDocumentVersionId))
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2407
|
+
}), await client.delete(targetDocumentVersionId));
|
|
2408
|
+
try {
|
|
2409
|
+
await retryWithBackoff(
|
|
2410
|
+
async () => {
|
|
2411
|
+
await client.action({
|
|
2412
|
+
actionType: "sanity.action.document.version.create",
|
|
2413
|
+
publishedId: targetDocumentPublishedId,
|
|
2414
|
+
document: newDoc
|
|
2415
|
+
});
|
|
2416
|
+
},
|
|
2417
|
+
{
|
|
2418
|
+
maxAttempts: 2,
|
|
2419
|
+
initialDelayMs: 0,
|
|
2420
|
+
retryableErrors: isNetworkError,
|
|
2421
|
+
context: {
|
|
2422
|
+
documentId: sourceDocument._id,
|
|
2423
|
+
targetLanguage,
|
|
2424
|
+
operation: "version.create"
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
);
|
|
2428
|
+
} catch (error) {
|
|
2429
|
+
if (translationMode === "openai-only" && isSanityMutationError(error)) {
|
|
2430
|
+
devLog.warn("Mutation error after OpenAI translation, retrying full translation", {
|
|
2431
|
+
documentId: sourceDocument._id,
|
|
2432
|
+
targetLanguage,
|
|
2433
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2434
|
+
}), previousError = error instanceof Error ? error : new Error(String(error));
|
|
2435
|
+
const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure.`;
|
|
2436
|
+
if (translatedDoc = await performTranslation(errorContext), !translatedDoc)
|
|
2437
|
+
throw new Error("Translation retry failed: no document returned");
|
|
2438
|
+
let retryTransformedResult;
|
|
2439
|
+
Object.keys(originalStructure).length > 0 ? retryTransformedResult = restoreTransforms({
|
|
2440
|
+
translatedDocument: translatedDoc,
|
|
2441
|
+
originalStructure,
|
|
2442
|
+
registry: globalTransformRegistry,
|
|
2443
|
+
schemaType: sourceDocument._type
|
|
2444
|
+
}) : retryTransformedResult = translatedDoc;
|
|
2445
|
+
const retryNewDoc = {
|
|
2446
|
+
...restoreExcludedFields(retryTransformedResult, excludedValuesToRestore),
|
|
2447
|
+
_id: targetDocumentVersionId
|
|
2448
|
+
};
|
|
2449
|
+
return await retryWithBackoff(
|
|
2450
|
+
async () => {
|
|
2451
|
+
await client.action({
|
|
2452
|
+
actionType: "sanity.action.document.version.create",
|
|
2453
|
+
publishedId: targetDocumentPublishedId,
|
|
2454
|
+
document: retryNewDoc
|
|
2455
|
+
});
|
|
2456
|
+
},
|
|
2457
|
+
{
|
|
2458
|
+
maxAttempts: 2,
|
|
2459
|
+
initialDelayMs: 0,
|
|
2460
|
+
retryableErrors: isNetworkError,
|
|
2461
|
+
context: {
|
|
2462
|
+
documentId: sourceDocument._id,
|
|
2463
|
+
targetLanguage,
|
|
2464
|
+
operation: "version.create",
|
|
2465
|
+
isRetry: !0
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
), retryNewDoc;
|
|
2469
|
+
}
|
|
2470
|
+
throw error;
|
|
2471
|
+
}
|
|
2472
|
+
return newDoc;
|
|
2053
2473
|
}, translateDocument = async ({
|
|
2054
2474
|
dispatch,
|
|
2055
2475
|
client,
|
|
@@ -2189,13 +2609,25 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2189
2609
|
});
|
|
2190
2610
|
else {
|
|
2191
2611
|
const releaseTitle = releaseConfig.newReleaseTitle, releaseDescription = `AI translation batch created on ${(/* @__PURE__ */ new Date()).toLocaleDateString()} at ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`;
|
|
2192
|
-
releaseId = (await
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2612
|
+
releaseId = (await retryWithBackoff(
|
|
2613
|
+
async () => await client.releases.create({
|
|
2614
|
+
metadata: {
|
|
2615
|
+
title: releaseTitle,
|
|
2616
|
+
description: releaseDescription,
|
|
2617
|
+
releaseType: "undecided"
|
|
2618
|
+
}
|
|
2619
|
+
}),
|
|
2620
|
+
{
|
|
2621
|
+
maxAttempts: 2,
|
|
2622
|
+
initialDelayMs: 0,
|
|
2623
|
+
retryableErrors: isNetworkError,
|
|
2624
|
+
context: {
|
|
2625
|
+
operation: "releases.create",
|
|
2626
|
+
releaseTitle,
|
|
2627
|
+
...logContext
|
|
2628
|
+
}
|
|
2197
2629
|
}
|
|
2198
|
-
|
|
2630
|
+
)).releaseId, createdNewRelease = !0, devLog.info("Content Release created", {
|
|
2199
2631
|
...logContext,
|
|
2200
2632
|
releaseId,
|
|
2201
2633
|
releaseTitle
|
|
@@ -2344,10 +2776,11 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2344
2776
|
overallStatus,
|
|
2345
2777
|
dispatch,
|
|
2346
2778
|
references,
|
|
2779
|
+
selectedReferenceIds,
|
|
2347
2780
|
selectedTargetLanguages,
|
|
2348
2781
|
translationMode,
|
|
2349
2782
|
releaseConfig
|
|
2350
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1);
|
|
2783
|
+
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1), filteredReferences = references.filter((ref) => selectedReferenceIds.has(ref._id));
|
|
2351
2784
|
useEffect(() => {
|
|
2352
2785
|
releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
|
|
2353
2786
|
}, [releaseConfig.mode, conflictContext]);
|
|
@@ -2360,7 +2793,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2360
2793
|
dispatch,
|
|
2361
2794
|
languageFieldName: config.languageFieldName,
|
|
2362
2795
|
schemaId: config.schemaId,
|
|
2363
|
-
references,
|
|
2796
|
+
references: filteredReferences,
|
|
2364
2797
|
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
2365
2798
|
maxReferenceDepth: config.maxReferenceDepth,
|
|
2366
2799
|
translationMode,
|
|
@@ -2386,7 +2819,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2386
2819
|
config.maxReferenceDepth,
|
|
2387
2820
|
config.openai,
|
|
2388
2821
|
config.excludeFields,
|
|
2389
|
-
|
|
2822
|
+
filteredReferences,
|
|
2390
2823
|
translationMode,
|
|
2391
2824
|
releaseConfig
|
|
2392
2825
|
]
|
|
@@ -2404,7 +2837,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2404
2837
|
sourceDocument: document,
|
|
2405
2838
|
targetLanguages: selectedTargetLanguages,
|
|
2406
2839
|
languageFieldName: config.languageFieldName,
|
|
2407
|
-
references
|
|
2840
|
+
references: filteredReferences
|
|
2408
2841
|
});
|
|
2409
2842
|
if (setIsCheckingConflicts(!1), foundConflicts.length > 0) {
|
|
2410
2843
|
setConflictContext({
|
|
@@ -2415,7 +2848,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2415
2848
|
}
|
|
2416
2849
|
}
|
|
2417
2850
|
startTranslation(!1);
|
|
2418
|
-
}, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation,
|
|
2851
|
+
}, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, filteredReferences]), handleCancelConflict = useCallback(() => {
|
|
2419
2852
|
setConflictContext(null);
|
|
2420
2853
|
}, []), handleReplaceConflict = useCallback(() => {
|
|
2421
2854
|
conflictContext && startTranslation(!0, conflictContext.releaseId);
|
|
@@ -2789,8 +3222,104 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2789
3222
|
});
|
|
2790
3223
|
}
|
|
2791
3224
|
return result;
|
|
3225
|
+
}, ReferenceSelectionPanel = () => {
|
|
3226
|
+
const { references, selectedReferenceIds, dispatch } = useTranslationContext(), [typeFilter, setTypeFilter] = useState("all"), documentTypes = useMemo(() => {
|
|
3227
|
+
const types = new Set(references.map((ref) => ref._type));
|
|
3228
|
+
return Array.from(types).sort();
|
|
3229
|
+
}, [references]), filteredReferences = useMemo(() => typeFilter === "all" ? references : references.filter((ref) => ref._type === typeFilter), [references, typeFilter]), selectedCount = useMemo(() => filteredReferences.filter((ref) => selectedReferenceIds.has(ref._id)).length, [filteredReferences, selectedReferenceIds]), handleToggleReference = (referenceId) => {
|
|
3230
|
+
const newSelection = new Set(selectedReferenceIds);
|
|
3231
|
+
newSelection.has(referenceId) ? newSelection.delete(referenceId) : newSelection.add(referenceId), dispatch({
|
|
3232
|
+
type: "SET_SELECTED_REFERENCE_IDS",
|
|
3233
|
+
payload: newSelection
|
|
3234
|
+
});
|
|
3235
|
+
}, handleSelectAll = () => {
|
|
3236
|
+
const newSelection = new Set(selectedReferenceIds);
|
|
3237
|
+
filteredReferences.forEach((ref) => {
|
|
3238
|
+
newSelection.add(ref._id);
|
|
3239
|
+
}), dispatch({
|
|
3240
|
+
type: "SET_SELECTED_REFERENCE_IDS",
|
|
3241
|
+
payload: newSelection
|
|
3242
|
+
});
|
|
3243
|
+
}, handleSelectNone = () => {
|
|
3244
|
+
const newSelection = new Set(selectedReferenceIds);
|
|
3245
|
+
filteredReferences.forEach((ref) => {
|
|
3246
|
+
newSelection.delete(ref._id);
|
|
3247
|
+
}), dispatch({
|
|
3248
|
+
type: "SET_SELECTED_REFERENCE_IDS",
|
|
3249
|
+
payload: newSelection
|
|
3250
|
+
});
|
|
3251
|
+
};
|
|
3252
|
+
return references.length === 0 ? null : /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3253
|
+
/* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", gap: 2, children: [
|
|
3254
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
3255
|
+
/* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "Select References" }),
|
|
3256
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3257
|
+
"(",
|
|
3258
|
+
selectedCount,
|
|
3259
|
+
" of ",
|
|
3260
|
+
filteredReferences.length,
|
|
3261
|
+
" selected)"
|
|
3262
|
+
] })
|
|
3263
|
+
] }),
|
|
3264
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
3265
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 1, onClick: handleSelectAll, text: "Select All" }),
|
|
3266
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 1, onClick: handleSelectNone, text: "Select None" })
|
|
3267
|
+
] })
|
|
3268
|
+
] }),
|
|
3269
|
+
documentTypes.length > 1 && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
3270
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Filter by type:" }),
|
|
3271
|
+
/* @__PURE__ */ jsxs(
|
|
3272
|
+
Select,
|
|
3273
|
+
{
|
|
3274
|
+
value: typeFilter,
|
|
3275
|
+
onChange: (event) => setTypeFilter(event.target.value),
|
|
3276
|
+
style: { minWidth: "150px" },
|
|
3277
|
+
children: [
|
|
3278
|
+
/* @__PURE__ */ jsxs("option", { value: "all", children: [
|
|
3279
|
+
"All types (",
|
|
3280
|
+
references.length,
|
|
3281
|
+
")"
|
|
3282
|
+
] }),
|
|
3283
|
+
documentTypes.map((type) => {
|
|
3284
|
+
const count = references.filter((ref) => ref._type === type).length;
|
|
3285
|
+
return /* @__PURE__ */ jsxs("option", { value: type, children: [
|
|
3286
|
+
type,
|
|
3287
|
+
" (",
|
|
3288
|
+
count,
|
|
3289
|
+
")"
|
|
3290
|
+
] }, type);
|
|
3291
|
+
})
|
|
3292
|
+
]
|
|
3293
|
+
}
|
|
3294
|
+
)
|
|
3295
|
+
] }),
|
|
3296
|
+
/* @__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) => {
|
|
3297
|
+
const isSelected = selectedReferenceIds.has(reference._id), displayTitle = reference.title || reference._id;
|
|
3298
|
+
return /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
|
|
3299
|
+
/* @__PURE__ */ jsx(
|
|
3300
|
+
Checkbox,
|
|
3301
|
+
{
|
|
3302
|
+
checked: isSelected,
|
|
3303
|
+
onChange: () => handleToggleReference(reference._id)
|
|
3304
|
+
}
|
|
3305
|
+
),
|
|
3306
|
+
/* @__PURE__ */ jsxs(Flex, { flex: 1, align: "center", gap: 2, children: [
|
|
3307
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: displayTitle }),
|
|
3308
|
+
/* @__PURE__ */ jsx(Badge, { tone: "primary", fontSize: 0, children: reference._type })
|
|
3309
|
+
] })
|
|
3310
|
+
] }) }, reference._id);
|
|
3311
|
+
}) }) })
|
|
3312
|
+
] }) });
|
|
2792
3313
|
}, ReferenceConfigPanel = () => {
|
|
2793
|
-
const {
|
|
3314
|
+
const {
|
|
3315
|
+
dispatch,
|
|
3316
|
+
document,
|
|
3317
|
+
shouldTranslateReferences,
|
|
3318
|
+
maxReferenceDepth,
|
|
3319
|
+
config,
|
|
3320
|
+
references,
|
|
3321
|
+
selectedReferenceIds
|
|
3322
|
+
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [isLoadingReferences, setIsLoadingReferences] = useState(!1), [showSelectionPanel, setShowSelectionPanel] = useState(!1), maxDepth = config.maxReferenceDepth || 3;
|
|
2794
3323
|
useEffect(() => {
|
|
2795
3324
|
(async () => {
|
|
2796
3325
|
if (setIsLoadingReferences(!0), !shouldTranslateReferences) {
|
|
@@ -2860,7 +3389,24 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2860
3389
|
)
|
|
2861
3390
|
] }) : null
|
|
2862
3391
|
] }),
|
|
2863
|
-
shouldTranslateReferences ? /* @__PURE__ */
|
|
3392
|
+
shouldTranslateReferences ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3393
|
+
/* @__PURE__ */ jsxs(Flex, { padding: 2, justify: "space-between", align: "center", children: [
|
|
3394
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: isLoadingReferences ? "Loading references..." : references.length === 0 ? "No translatable references found." : (() => {
|
|
3395
|
+
const selectedCount = references.filter((ref) => selectedReferenceIds.has(ref._id)).length, totalCount = references.length;
|
|
3396
|
+
return selectedCount === totalCount ? `${totalCount} reference${totalCount === 1 ? "" : "s"} will be translated along with this document.` : `${selectedCount} of ${totalCount} reference${totalCount === 1 ? "" : "s"} will be translated along with this document.`;
|
|
3397
|
+
})() }),
|
|
3398
|
+
!isLoadingReferences && references.length > 0 && /* @__PURE__ */ jsx(
|
|
3399
|
+
Button,
|
|
3400
|
+
{
|
|
3401
|
+
mode: "ghost",
|
|
3402
|
+
size: 1,
|
|
3403
|
+
text: showSelectionPanel ? "Hide selection" : "Select specific references...",
|
|
3404
|
+
onClick: () => setShowSelectionPanel(!showSelectionPanel)
|
|
3405
|
+
}
|
|
3406
|
+
)
|
|
3407
|
+
] }),
|
|
3408
|
+
!isLoadingReferences && references.length > 0 && showSelectionPanel && /* @__PURE__ */ jsx(ReferenceSelectionPanel, {})
|
|
3409
|
+
] }) : null
|
|
2864
3410
|
] });
|
|
2865
3411
|
}, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2866
3412
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
@@ -3436,10 +3982,14 @@ function serializeVTTCaptionText(captions) {
|
|
|
3436
3982
|
firstCaption: captions.length > 0 ? captions[0] : null
|
|
3437
3983
|
}), !Array.isArray(captions) || captions.length === 0)
|
|
3438
3984
|
return "";
|
|
3439
|
-
const result = captions.map((caption, index) =>
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3985
|
+
const result = captions.map((caption, index) => {
|
|
3986
|
+
devLog.debug(`Processing caption ${index}:`, {
|
|
3987
|
+
text: caption.text,
|
|
3988
|
+
hasText: typeof caption.text == "string"
|
|
3989
|
+
});
|
|
3990
|
+
const escapedText = (caption.text || "").replace(/\n/g, "\\n");
|
|
3991
|
+
return `[${index}] ${escapedText}`;
|
|
3992
|
+
}).join(`
|
|
3443
3993
|
`);
|
|
3444
3994
|
return devLog.debug("serializeVTTCaptionText - output:", {
|
|
3445
3995
|
resultLength: result.length,
|
|
@@ -3456,29 +4006,46 @@ function deserializeVTTCaptionText(translationText, originalMetadata) {
|
|
|
3456
4006
|
return [];
|
|
3457
4007
|
const result = [], lines = translationText.split(`
|
|
3458
4008
|
`), lineRegex = /^\[(\d+)\]\s*(.*)$/;
|
|
4009
|
+
let currentIndex = null, currentTextParts = [];
|
|
3459
4010
|
for (const line of lines) {
|
|
3460
4011
|
const match = line.match(lineRegex);
|
|
3461
|
-
if (
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
4012
|
+
if (match) {
|
|
4013
|
+
if (currentIndex !== null) {
|
|
4014
|
+
const text = currentTextParts.join(`
|
|
4015
|
+
`).replace(/\\n/g, `
|
|
4016
|
+
`), metadata = originalMetadata.find((m) => m._originalIndex === currentIndex);
|
|
4017
|
+
if (metadata) {
|
|
4018
|
+
const { _originalIndex, ...metadataWithoutIndex } = metadata;
|
|
4019
|
+
result.push({
|
|
4020
|
+
...metadataWithoutIndex,
|
|
4021
|
+
text
|
|
4022
|
+
});
|
|
4023
|
+
} else
|
|
4024
|
+
devLog.warn("Missing metadata for caption", { index: currentIndex }), result.push({
|
|
4025
|
+
_type: "vtt.cue",
|
|
4026
|
+
_key: `caption-${currentIndex}`,
|
|
4027
|
+
text,
|
|
4028
|
+
startTime: 0,
|
|
4029
|
+
endTime: 0
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
currentIndex = Number.parseInt(match[1], 10), currentTextParts = [match[2]];
|
|
4033
|
+
} else currentIndex !== null ? currentTextParts.push(line) : devLog.warn("Failed to parse caption line", { line });
|
|
4034
|
+
}
|
|
4035
|
+
if (currentIndex !== null) {
|
|
4036
|
+
const text = currentTextParts.join(`
|
|
4037
|
+
`).replace(/\\n/g, `
|
|
4038
|
+
`), metadata = originalMetadata.find((m) => m._originalIndex === currentIndex);
|
|
3471
4039
|
if (metadata) {
|
|
3472
|
-
devLog.debug(`Found matching metadata for index ${index}`);
|
|
3473
4040
|
const { _originalIndex, ...metadataWithoutIndex } = metadata;
|
|
3474
4041
|
result.push({
|
|
3475
4042
|
...metadataWithoutIndex,
|
|
3476
4043
|
text
|
|
3477
4044
|
});
|
|
3478
4045
|
} else
|
|
3479
|
-
devLog.warn("Missing metadata for caption", { index }), result.push({
|
|
4046
|
+
devLog.warn("Missing metadata for caption", { index: currentIndex }), result.push({
|
|
3480
4047
|
_type: "vtt.cue",
|
|
3481
|
-
_key: `caption-${
|
|
4048
|
+
_key: `caption-${currentIndex}`,
|
|
3482
4049
|
text,
|
|
3483
4050
|
startTime: 0,
|
|
3484
4051
|
endTime: 0
|
|
@@ -3489,6 +4056,293 @@ function deserializeVTTCaptionText(translationText, originalMetadata) {
|
|
|
3489
4056
|
resultExample: result.length > 0 ? result[0] : null
|
|
3490
4057
|
}), result;
|
|
3491
4058
|
}
|
|
4059
|
+
const LOCALE_TO_ISO = {
|
|
4060
|
+
// English variants
|
|
4061
|
+
"en-us": "eng",
|
|
4062
|
+
"en-US": "eng",
|
|
4063
|
+
"en-gb": "eng",
|
|
4064
|
+
"en-GB": "eng",
|
|
4065
|
+
"en-ca": "eng",
|
|
4066
|
+
"en-CA": "eng",
|
|
4067
|
+
"en-au": "eng",
|
|
4068
|
+
"en-AU": "eng",
|
|
4069
|
+
// Spanish variants
|
|
4070
|
+
es: "spa",
|
|
4071
|
+
"es-es": "spa",
|
|
4072
|
+
"es-ES": "spa",
|
|
4073
|
+
"es-mx": "spa",
|
|
4074
|
+
"es-MX": "spa",
|
|
4075
|
+
// French variants
|
|
4076
|
+
fr: "fra",
|
|
4077
|
+
"fr-fr": "fra",
|
|
4078
|
+
"fr-FR": "fra",
|
|
4079
|
+
"fr-ca": "fra",
|
|
4080
|
+
"fr-CA": "fra",
|
|
4081
|
+
// German variants
|
|
4082
|
+
de: "ger",
|
|
4083
|
+
"de-de": "ger",
|
|
4084
|
+
"de-DE": "ger",
|
|
4085
|
+
"de-at": "ger",
|
|
4086
|
+
"de-AT": "ger",
|
|
4087
|
+
// Italian variants
|
|
4088
|
+
it: "ita",
|
|
4089
|
+
"it-it": "ita",
|
|
4090
|
+
"it-IT": "ita",
|
|
4091
|
+
// Portuguese variants
|
|
4092
|
+
pt: "por",
|
|
4093
|
+
"pt-br": "por",
|
|
4094
|
+
"pt-BR": "por",
|
|
4095
|
+
"pt-pt": "por",
|
|
4096
|
+
"pt-PT": "por",
|
|
4097
|
+
// Russian variants
|
|
4098
|
+
ru: "rus",
|
|
4099
|
+
"ru-ru": "rus",
|
|
4100
|
+
"ru-RU": "rus",
|
|
4101
|
+
// Japanese variants
|
|
4102
|
+
ja: "jpn",
|
|
4103
|
+
"ja-jp": "jpn",
|
|
4104
|
+
"ja-JP": "jpn",
|
|
4105
|
+
// Korean variants
|
|
4106
|
+
ko: "kor",
|
|
4107
|
+
"ko-kr": "kor",
|
|
4108
|
+
"ko-KR": "kor",
|
|
4109
|
+
// Chinese variants
|
|
4110
|
+
zh: "chi",
|
|
4111
|
+
"zh-cn": "chi",
|
|
4112
|
+
"zh-CN": "chi",
|
|
4113
|
+
"zh-tw": "chi",
|
|
4114
|
+
"zh-TW": "chi",
|
|
4115
|
+
"zh-hk": "chi",
|
|
4116
|
+
"zh-HK": "chi"
|
|
4117
|
+
}, normalizeToIsoLanguageCode = (code) => {
|
|
4118
|
+
const normalizedCode = code.toLowerCase();
|
|
4119
|
+
return LOCALE_TO_ISO[normalizedCode];
|
|
4120
|
+
}, wistiaCaptionTransform = {
|
|
4121
|
+
matcher: {
|
|
4122
|
+
fieldName: "wistiaCaptions"
|
|
4123
|
+
},
|
|
4124
|
+
toTranslatable: (wistiaCaptions, context) => {
|
|
4125
|
+
if (devLog.info("Wistia captions toTranslatable - input:", {
|
|
4126
|
+
fieldPath: context.fieldPath,
|
|
4127
|
+
hasCaptionContent: !!wistiaCaptions?.captionContent,
|
|
4128
|
+
captionContentLength: wistiaCaptions?.captionContent?.length ?? 0,
|
|
4129
|
+
isoLanguage: wistiaCaptions?.isoLanguage
|
|
4130
|
+
}), !wistiaCaptions || !wistiaCaptions.captionContent)
|
|
4131
|
+
return devLog.debug("Wistia caption field is missing or has no content, returning empty transform", {
|
|
4132
|
+
fieldPath: context.fieldPath,
|
|
4133
|
+
value: wistiaCaptions
|
|
4134
|
+
}), {
|
|
4135
|
+
_type: "translatable.wistia.captions",
|
|
4136
|
+
translationText: "",
|
|
4137
|
+
_originalMetadata: {
|
|
4138
|
+
captionFileName: wistiaCaptions?.captionFileName,
|
|
4139
|
+
isoLanguage: wistiaCaptions?.isoLanguage,
|
|
4140
|
+
captionUploadStatus: wistiaCaptions?.captionUploadStatus,
|
|
4141
|
+
captionUploadMessage: wistiaCaptions?.captionUploadMessage,
|
|
4142
|
+
_originalIndex: 0
|
|
4143
|
+
},
|
|
4144
|
+
_srtCues: []
|
|
4145
|
+
};
|
|
4146
|
+
devLog.info("Applying Wistia caption transform", {
|
|
4147
|
+
fieldPath: context.fieldPath,
|
|
4148
|
+
captionContentPreview: wistiaCaptions.captionContent.substring(0, 200)
|
|
4149
|
+
});
|
|
4150
|
+
const srtCues = parseSRTContent(wistiaCaptions.captionContent);
|
|
4151
|
+
devLog.debug("Parsed SRT cues:", {
|
|
4152
|
+
fieldPath: context.fieldPath,
|
|
4153
|
+
cueCount: srtCues.length,
|
|
4154
|
+
firstCue: srtCues.length > 0 ? srtCues[0] : null
|
|
4155
|
+
});
|
|
4156
|
+
const translationText = serializeWistiaCaptionText(srtCues), originalMetadata = {
|
|
4157
|
+
captionFileName: wistiaCaptions.captionFileName,
|
|
4158
|
+
isoLanguage: wistiaCaptions.isoLanguage,
|
|
4159
|
+
captionUploadStatus: wistiaCaptions.captionUploadStatus,
|
|
4160
|
+
captionUploadMessage: wistiaCaptions.captionUploadMessage,
|
|
4161
|
+
_originalIndex: 0
|
|
4162
|
+
}, srtCuesMetadata = srtCues.map((cue) => ({
|
|
4163
|
+
index: cue.index,
|
|
4164
|
+
startTime: cue.startTime,
|
|
4165
|
+
endTime: cue.endTime
|
|
4166
|
+
}));
|
|
4167
|
+
return devLog.info("Wistia captions serialized:", {
|
|
4168
|
+
fieldPath: context.fieldPath,
|
|
4169
|
+
translationTextLength: translationText.length,
|
|
4170
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
|
|
4171
|
+
cueCount: srtCues.length
|
|
4172
|
+
}), {
|
|
4173
|
+
_type: "translatable.wistia.captions",
|
|
4174
|
+
translationText,
|
|
4175
|
+
_originalMetadata: originalMetadata,
|
|
4176
|
+
_srtCues: srtCuesMetadata
|
|
4177
|
+
};
|
|
4178
|
+
},
|
|
4179
|
+
fromTranslatable: (translatedValue, originalValue, context) => {
|
|
4180
|
+
if (devLog.info("Wistia captions fromTranslatable - input:", {
|
|
4181
|
+
fieldPath: context.fieldPath,
|
|
4182
|
+
valueType: typeof translatedValue,
|
|
4183
|
+
isCorrectType: translatedValue._type === "translatable.wistia.captions",
|
|
4184
|
+
hasText: !!translatedValue.translationText,
|
|
4185
|
+
hasMetadata: !!translatedValue._originalMetadata,
|
|
4186
|
+
hasCues: Array.isArray(translatedValue._srtCues)
|
|
4187
|
+
}), !translatedValue || translatedValue._type !== "translatable.wistia.captions" || !translatedValue._originalMetadata || !Array.isArray(translatedValue._srtCues))
|
|
4188
|
+
return devLog.debug("Value is not a translatable Wistia caption object, returning original", {
|
|
4189
|
+
fieldPath: context.fieldPath,
|
|
4190
|
+
valueType: typeof translatedValue,
|
|
4191
|
+
translatedValue
|
|
4192
|
+
}), originalValue || {};
|
|
4193
|
+
devLog.info("Processing translated Wistia captions", {
|
|
4194
|
+
fieldPath: context.fieldPath,
|
|
4195
|
+
cueCount: translatedValue._srtCues.length
|
|
4196
|
+
});
|
|
4197
|
+
const { translationText, _originalMetadata, _srtCues } = translatedValue;
|
|
4198
|
+
devLog.info("Wistia captions translationText:", {
|
|
4199
|
+
fieldPath: context.fieldPath,
|
|
4200
|
+
translationTextLength: translationText.length,
|
|
4201
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
|
|
4202
|
+
});
|
|
4203
|
+
const reconstructedCaptionContent = deserializeWistiaCaptionText(translationText, _srtCues), documentLanguage = getDocumentLanguage(context.document);
|
|
4204
|
+
let updatedIsoLanguage = _originalMetadata.isoLanguage;
|
|
4205
|
+
if (documentLanguage) {
|
|
4206
|
+
const isoCode = normalizeToIsoLanguageCode(documentLanguage);
|
|
4207
|
+
isoCode ? (updatedIsoLanguage = isoCode, devLog.debug("Updated isoLanguage based on document language", {
|
|
4208
|
+
fieldPath: context.fieldPath,
|
|
4209
|
+
documentLanguage,
|
|
4210
|
+
isoCode
|
|
4211
|
+
})) : devLog.debug("Could not convert document language to ISO code, preserving original", {
|
|
4212
|
+
fieldPath: context.fieldPath,
|
|
4213
|
+
documentLanguage,
|
|
4214
|
+
originalIsoLanguage: _originalMetadata.isoLanguage
|
|
4215
|
+
});
|
|
4216
|
+
} else
|
|
4217
|
+
devLog.debug("Document language field not found, preserving original isoLanguage", {
|
|
4218
|
+
fieldPath: context.fieldPath,
|
|
4219
|
+
originalIsoLanguage: _originalMetadata.isoLanguage
|
|
4220
|
+
});
|
|
4221
|
+
const result = {
|
|
4222
|
+
captionContent: reconstructedCaptionContent,
|
|
4223
|
+
captionFileName: _originalMetadata.captionFileName,
|
|
4224
|
+
isoLanguage: updatedIsoLanguage,
|
|
4225
|
+
captionUploadStatus: _originalMetadata.captionUploadStatus,
|
|
4226
|
+
captionUploadMessage: _originalMetadata.captionUploadMessage
|
|
4227
|
+
};
|
|
4228
|
+
return devLog.info("Wistia captions final result:", {
|
|
4229
|
+
fieldPath: context.fieldPath,
|
|
4230
|
+
hasCaptionContent: !!result.captionContent,
|
|
4231
|
+
captionContentLength: result.captionContent?.length ?? 0,
|
|
4232
|
+
isoLanguage: result.isoLanguage
|
|
4233
|
+
}), result;
|
|
4234
|
+
}
|
|
4235
|
+
};
|
|
4236
|
+
function parseSRTContent(content) {
|
|
4237
|
+
if (devLog.debug("parseSRTContent - input:", {
|
|
4238
|
+
contentLength: content.length,
|
|
4239
|
+
contentPreview: content.substring(0, 100) + (content.length > 100 ? "..." : "")
|
|
4240
|
+
}), !content || content.trim() === "")
|
|
4241
|
+
return [];
|
|
4242
|
+
let normalizedContent = content;
|
|
4243
|
+
content.includes("\\n") && !content.includes(`
|
|
4244
|
+
`) && (normalizedContent = content.replace(/\\n/g, `
|
|
4245
|
+
`), devLog.debug("Unescaped newlines in SRT content", {
|
|
4246
|
+
originalLength: content.length,
|
|
4247
|
+
normalizedLength: normalizedContent.length
|
|
4248
|
+
}));
|
|
4249
|
+
const cues = [], cueBlocks = normalizedContent.split(/\n\s*\n/);
|
|
4250
|
+
for (const block of cueBlocks) {
|
|
4251
|
+
const lines = block.trim().split(`
|
|
4252
|
+
`);
|
|
4253
|
+
if (lines.length < 3)
|
|
4254
|
+
continue;
|
|
4255
|
+
const cueNumber = Number.parseInt(lines[0].trim(), 10);
|
|
4256
|
+
if (Number.isNaN(cueNumber)) {
|
|
4257
|
+
devLog.warn("Invalid SRT cue number", { line: lines[0] });
|
|
4258
|
+
continue;
|
|
4259
|
+
}
|
|
4260
|
+
const timestampLine = lines[1].trim(), timestampMatch = timestampLine.match(/^(\d{2}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}[,.]\d{3})$/);
|
|
4261
|
+
if (!timestampMatch) {
|
|
4262
|
+
devLog.warn("Invalid SRT timestamp format", { line: timestampLine });
|
|
4263
|
+
continue;
|
|
4264
|
+
}
|
|
4265
|
+
const startTime = timestampMatch[1].replace(".", ","), endTime = timestampMatch[2].replace(".", ","), text = lines.slice(2).join(`
|
|
4266
|
+
`).trim();
|
|
4267
|
+
text && cues.push({
|
|
4268
|
+
index: cueNumber,
|
|
4269
|
+
startTime,
|
|
4270
|
+
endTime,
|
|
4271
|
+
text
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
return devLog.debug("parseSRTContent - output:", {
|
|
4275
|
+
cueCount: cues.length,
|
|
4276
|
+
firstCue: cues.length > 0 ? cues[0] : null
|
|
4277
|
+
}), cues;
|
|
4278
|
+
}
|
|
4279
|
+
function serializeWistiaCaptionText(cues) {
|
|
4280
|
+
if (devLog.debug("serializeWistiaCaptionText - input:", {
|
|
4281
|
+
cuesCount: cues.length,
|
|
4282
|
+
firstCue: cues.length > 0 ? cues[0] : null
|
|
4283
|
+
}), !Array.isArray(cues) || cues.length === 0)
|
|
4284
|
+
return "";
|
|
4285
|
+
const result = cues.map((cue) => {
|
|
4286
|
+
devLog.debug(`Processing cue ${cue.index}:`, {
|
|
4287
|
+
text: cue.text,
|
|
4288
|
+
hasText: typeof cue.text == "string"
|
|
4289
|
+
});
|
|
4290
|
+
const escapedText = cue.text.replace(/\n/g, "\\n");
|
|
4291
|
+
return `[${cue.index}] ${escapedText}`;
|
|
4292
|
+
}).join(`
|
|
4293
|
+
`);
|
|
4294
|
+
return devLog.debug("serializeWistiaCaptionText - output:", {
|
|
4295
|
+
resultLength: result.length,
|
|
4296
|
+
resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
|
|
4297
|
+
}), result;
|
|
4298
|
+
}
|
|
4299
|
+
function deserializeWistiaCaptionText(translationText, srtCuesMetadata) {
|
|
4300
|
+
if (devLog.debug("deserializeWistiaCaptionText - input:", {
|
|
4301
|
+
translationTextLength: translationText.length,
|
|
4302
|
+
translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
|
|
4303
|
+
metadataCount: srtCuesMetadata.length,
|
|
4304
|
+
metadataExample: srtCuesMetadata.length > 0 ? srtCuesMetadata[0] : null
|
|
4305
|
+
}), !translationText || translationText.trim() === "")
|
|
4306
|
+
return "";
|
|
4307
|
+
const result = [], lines = translationText.split(`
|
|
4308
|
+
`), lineRegex = /^\[(\d+)\]\s*(.*)$/, metadataByIndex = /* @__PURE__ */ new Map();
|
|
4309
|
+
for (const metadata of srtCuesMetadata)
|
|
4310
|
+
metadataByIndex.set(metadata.index, { startTime: metadata.startTime, endTime: metadata.endTime });
|
|
4311
|
+
let currentIndex = null, currentTextParts = [];
|
|
4312
|
+
for (const line of lines) {
|
|
4313
|
+
const match = line.match(lineRegex);
|
|
4314
|
+
if (match) {
|
|
4315
|
+
if (currentIndex !== null) {
|
|
4316
|
+
const text = currentTextParts.join(`
|
|
4317
|
+
`).replace(/\\n/g, `
|
|
4318
|
+
`), metadata = metadataByIndex.get(currentIndex);
|
|
4319
|
+
metadata ? (result.push(`${currentIndex}`), result.push(`${metadata.startTime} --> ${metadata.endTime}`), result.push(text), result.push("")) : devLog.warn("Missing metadata for caption", { index: currentIndex });
|
|
4320
|
+
}
|
|
4321
|
+
currentIndex = Number.parseInt(match[1], 10), currentTextParts = [match[2]];
|
|
4322
|
+
} else currentIndex !== null ? currentTextParts.push(line) : devLog.warn("Failed to parse caption line", { line });
|
|
4323
|
+
}
|
|
4324
|
+
if (currentIndex !== null) {
|
|
4325
|
+
const text = currentTextParts.join(`
|
|
4326
|
+
`).replace(/\\n/g, `
|
|
4327
|
+
`), metadata = metadataByIndex.get(currentIndex);
|
|
4328
|
+
metadata ? (result.push(`${currentIndex}`), result.push(`${metadata.startTime} --> ${metadata.endTime}`), result.push(text), result.push("")) : devLog.warn("Missing metadata for caption", { index: currentIndex });
|
|
4329
|
+
}
|
|
4330
|
+
result.length > 0 && result[result.length - 1] === "" && result.pop();
|
|
4331
|
+
const reconstructed = result.join(`
|
|
4332
|
+
`);
|
|
4333
|
+
return devLog.debug("deserializeWistiaCaptionText - output:", {
|
|
4334
|
+
resultLength: reconstructed.length,
|
|
4335
|
+
resultPreview: reconstructed.substring(0, 200) + (reconstructed.length > 200 ? "..." : "")
|
|
4336
|
+
}), reconstructed;
|
|
4337
|
+
}
|
|
4338
|
+
function getDocumentLanguage(document) {
|
|
4339
|
+
if (!document || typeof document != "object")
|
|
4340
|
+
return;
|
|
4341
|
+
const languageFieldNames = ["language", "lang", "locale"];
|
|
4342
|
+
for (const fieldName of languageFieldNames)
|
|
4343
|
+
if (fieldName in document && typeof document[fieldName] == "string")
|
|
4344
|
+
return document[fieldName];
|
|
4345
|
+
}
|
|
3492
4346
|
const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
|
|
3493
4347
|
globalTransformRegistry.register(transform);
|
|
3494
4348
|
})), {
|
|
@@ -3508,6 +4362,7 @@ export {
|
|
|
3508
4362
|
restoreExcludedFields,
|
|
3509
4363
|
richTextTransform,
|
|
3510
4364
|
translationHistorySchema,
|
|
3511
|
-
vttCaptionTransform
|
|
4365
|
+
vttCaptionTransform,
|
|
4366
|
+
wistiaCaptionTransform
|
|
3512
4367
|
};
|
|
3513
4368
|
//# sourceMappingURL=index.js.map
|