@braze/sanity-plugin-ai-translation 0.2.5 → 0.2.7
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 +255 -0
- package/dist/index.js +1726 -200
- 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
|
-
import { WarningOutlineIcon, SearchIcon, CheckmarkIcon, ErrorOutlineIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
|
|
4
|
-
import { createContext, useReducer, useEffect, useContext, useMemo, useState, useCallback } from "react";
|
|
3
|
+
import { WarningOutlineIcon, SyncIcon, SearchIcon, CheckmarkIcon, ErrorOutlineIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
|
|
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,
|
|
@@ -119,6 +122,8 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
119
122
|
mode: "new",
|
|
120
123
|
newReleaseTitle: ""
|
|
121
124
|
},
|
|
125
|
+
reconcilingStatus: "idle",
|
|
126
|
+
reconciliationProgress: void 0,
|
|
122
127
|
dispatch: () => {
|
|
123
128
|
}
|
|
124
129
|
}), translationReducer = (context, action) => {
|
|
@@ -165,8 +170,18 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
165
170
|
...context,
|
|
166
171
|
references: action.payload
|
|
167
172
|
};
|
|
173
|
+
case "SET_SELECTED_REFERENCE_IDS":
|
|
174
|
+
return {
|
|
175
|
+
...context,
|
|
176
|
+
selectedReferenceIds: action.payload
|
|
177
|
+
};
|
|
168
178
|
case "SET_TRANSLATION_JOB_STATUS": {
|
|
169
|
-
|
|
179
|
+
if (!action.payload._id)
|
|
180
|
+
return devLog.warn("SET_TRANSLATION_JOB_STATUS action missing _id", {
|
|
181
|
+
...logContext,
|
|
182
|
+
payload: action.payload
|
|
183
|
+
}), context;
|
|
184
|
+
const jobKey = `${undraftId(action.payload._id)}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs), existingJob = newTranslationJobs.get(jobKey), mergedJob = defaults(action.payload, existingJob);
|
|
170
185
|
return newTranslationJobs.set(jobKey, mergedJob), {
|
|
171
186
|
...context,
|
|
172
187
|
translationJobs: newTranslationJobs
|
|
@@ -189,12 +204,29 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
189
204
|
...context,
|
|
190
205
|
overallStatus: "idle",
|
|
191
206
|
translationJobs: /* @__PURE__ */ new Map(),
|
|
207
|
+
selectedReferenceIds: /* @__PURE__ */ new Set(),
|
|
192
208
|
// Clear any release conflicts from previous run
|
|
193
209
|
releaseConfig: {
|
|
194
210
|
...context.releaseConfig,
|
|
195
211
|
conflicts: void 0,
|
|
196
|
-
replaceExisting: void 0
|
|
197
|
-
|
|
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
|
|
198
230
|
};
|
|
199
231
|
default:
|
|
200
232
|
return context;
|
|
@@ -216,6 +248,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
216
248
|
shouldTranslateReferences: !1,
|
|
217
249
|
maxReferenceDepth: 1,
|
|
218
250
|
references: [],
|
|
251
|
+
selectedReferenceIds: /* @__PURE__ */ new Set(),
|
|
219
252
|
// Restore jobs from active translation if available
|
|
220
253
|
translationJobs: activeTranslation?.translationJobs ?? /* @__PURE__ */ new Map(),
|
|
221
254
|
translationMode: config?.openai?.defaultMode || "sanity-only",
|
|
@@ -223,7 +256,10 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
223
256
|
releaseConfig: {
|
|
224
257
|
mode: "new",
|
|
225
258
|
newReleaseTitle: defaultReleaseTitle
|
|
226
|
-
}
|
|
259
|
+
},
|
|
260
|
+
// Reconciliation state for existing releases
|
|
261
|
+
reconcilingStatus: "idle",
|
|
262
|
+
reconciliationProgress: void 0
|
|
227
263
|
};
|
|
228
264
|
activeTranslation && devLog.info("Reconnecting to active translation", {
|
|
229
265
|
documentId: document._id,
|
|
@@ -231,7 +267,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
231
267
|
jobCount: activeTranslation.translationJobs.size,
|
|
232
268
|
component: "TranslationContextProvider"
|
|
233
269
|
});
|
|
234
|
-
const [context, dispatch] = useReducer(translationReducer, initialContext);
|
|
270
|
+
const [context, dispatch] = useReducer(translationReducer, initialContext), lastProcessedRefsRef = useRef("");
|
|
235
271
|
return useEffect(() => {
|
|
236
272
|
const handleActiveTranslationUpdate = (state) => {
|
|
237
273
|
dispatch({
|
|
@@ -271,7 +307,28 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
271
307
|
});
|
|
272
308
|
}
|
|
273
309
|
})();
|
|
274
|
-
}, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]),
|
|
310
|
+
}, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), useEffect(() => {
|
|
311
|
+
const currentRefsKey = context.references.map((ref) => ref._id).sort().join(",");
|
|
312
|
+
if (currentRefsKey !== lastProcessedRefsRef.current) {
|
|
313
|
+
if (context.references.length > 0) {
|
|
314
|
+
const currentReferenceIds = new Set(context.references.map((ref) => ref._id)), previousReferenceIds = new Set(
|
|
315
|
+
lastProcessedRefsRef.current ? lastProcessedRefsRef.current.split(",") : []
|
|
316
|
+
), hasNewReferences = Array.from(currentReferenceIds).some((id) => !previousReferenceIds.has(id)), currentSelection = context.selectedReferenceIds;
|
|
317
|
+
if (currentSelection.size === 0 || hasNewReferences) {
|
|
318
|
+
const newSelection = new Set(currentSelection);
|
|
319
|
+
currentReferenceIds.forEach((id) => {
|
|
320
|
+
newSelection.add(id);
|
|
321
|
+
});
|
|
322
|
+
const currentIds = Array.from(currentSelection).sort(), newIds = Array.from(newSelection).sort();
|
|
323
|
+
(currentIds.length !== newIds.length || !currentIds.every((id, i) => id === newIds[i])) && dispatch({
|
|
324
|
+
type: "SET_SELECTED_REFERENCE_IDS",
|
|
325
|
+
payload: newSelection
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
lastProcessedRefsRef.current = currentRefsKey;
|
|
330
|
+
}
|
|
331
|
+
}, [context.references, context.selectedReferenceIds]), devLog.debug("Translation context state initialized", {
|
|
275
332
|
documentId: document._id,
|
|
276
333
|
overallStatus: context.overallStatus,
|
|
277
334
|
selectedLanguages: context.selectedTargetLanguages,
|
|
@@ -288,7 +345,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
288
345
|
return context;
|
|
289
346
|
}, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
290
347
|
(translation) => translation._key === targetLanguage
|
|
291
|
-
)?.value._ref || null,
|
|
348
|
+
)?.value._ref || null, TRANSLATION_METADATA_TYPE = "translation.metadata", getTranslationMetadata = async ({
|
|
292
349
|
client,
|
|
293
350
|
sourceDocument,
|
|
294
351
|
sourceLanguage
|
|
@@ -409,6 +466,115 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
409
466
|
}
|
|
410
467
|
return allConflicts;
|
|
411
468
|
};
|
|
469
|
+
function isNetworkError(error) {
|
|
470
|
+
if (!error) return !1;
|
|
471
|
+
if (error instanceof TypeError) {
|
|
472
|
+
const message = error.message.toLowerCase();
|
|
473
|
+
return message.includes("fetch") || message.includes("network") || message.includes("connection") || message.includes("failed to fetch") || message.includes("networkerror");
|
|
474
|
+
}
|
|
475
|
+
if (error && typeof error == "object" && "code" in error) {
|
|
476
|
+
const code = error.code;
|
|
477
|
+
return ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "ECONNREFUSED", "EHOSTUNREACH"].includes(code);
|
|
478
|
+
}
|
|
479
|
+
if (error && typeof error == "object" && "response" in error) {
|
|
480
|
+
const response = error.response;
|
|
481
|
+
if (response?.status)
|
|
482
|
+
return response.status >= 500 || response.status === 429;
|
|
483
|
+
}
|
|
484
|
+
if (error instanceof Error) {
|
|
485
|
+
const message = error.message;
|
|
486
|
+
if (/\b(50[0-9]|429)\b/.test(message))
|
|
487
|
+
return !0;
|
|
488
|
+
}
|
|
489
|
+
return !1;
|
|
490
|
+
}
|
|
491
|
+
function isSanityMutationError(error) {
|
|
492
|
+
if (!error) return !1;
|
|
493
|
+
if (error instanceof Error) {
|
|
494
|
+
const message = error.message.toLowerCase();
|
|
495
|
+
return [
|
|
496
|
+
"validation",
|
|
497
|
+
"schema",
|
|
498
|
+
"invalid document",
|
|
499
|
+
"invalid field",
|
|
500
|
+
"type mismatch",
|
|
501
|
+
"reference",
|
|
502
|
+
"patch",
|
|
503
|
+
"mutation",
|
|
504
|
+
"document structure",
|
|
505
|
+
"required field",
|
|
506
|
+
"field type",
|
|
507
|
+
"sanity"
|
|
508
|
+
].some((pattern) => message.includes(pattern));
|
|
509
|
+
}
|
|
510
|
+
if (error && typeof error == "object") {
|
|
511
|
+
const errorObj = error;
|
|
512
|
+
if (("statusCode" in errorObj || "response" in errorObj || "details" in errorObj) && "statusCode" in errorObj) {
|
|
513
|
+
const statusCode = errorObj.statusCode;
|
|
514
|
+
if (statusCode >= 400 && statusCode < 500 && statusCode !== 429)
|
|
515
|
+
return !0;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return !1;
|
|
519
|
+
}
|
|
520
|
+
const DEFAULT_RETRY_OPTIONS = {
|
|
521
|
+
maxAttempts: 2,
|
|
522
|
+
initialDelayMs: 0,
|
|
523
|
+
maxDelayMs: 5e3,
|
|
524
|
+
backoffMultiplier: 2,
|
|
525
|
+
retryableErrors: isNetworkError,
|
|
526
|
+
logRetries: !0
|
|
527
|
+
};
|
|
528
|
+
function calculateDelay(attempt, options) {
|
|
529
|
+
const delay = options.initialDelayMs * options.backoffMultiplier ** attempt;
|
|
530
|
+
return Math.min(delay, options.maxDelayMs);
|
|
531
|
+
}
|
|
532
|
+
function sleep(ms) {
|
|
533
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
534
|
+
}
|
|
535
|
+
async function retryWithBackoff(operation, options = {}) {
|
|
536
|
+
const opts = {
|
|
537
|
+
...DEFAULT_RETRY_OPTIONS,
|
|
538
|
+
...options,
|
|
539
|
+
retryableErrors: options.retryableErrors || DEFAULT_RETRY_OPTIONS.retryableErrors
|
|
540
|
+
};
|
|
541
|
+
let lastError;
|
|
542
|
+
const maxAttempts = opts.maxAttempts + 1;
|
|
543
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++)
|
|
544
|
+
try {
|
|
545
|
+
const result = await operation();
|
|
546
|
+
return attempt > 1 && opts.logRetries && devLog.info(`Operation succeeded after ${attempt} attempts`, {
|
|
547
|
+
...opts.context,
|
|
548
|
+
attempt,
|
|
549
|
+
totalAttempts: attempt
|
|
550
|
+
}), result;
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (lastError = error, !opts.retryableErrors(error))
|
|
553
|
+
throw opts.logRetries && devLog.warn("Error is not retryable, aborting retries", {
|
|
554
|
+
...opts.context,
|
|
555
|
+
attempt,
|
|
556
|
+
error: error instanceof Error ? error.message : String(error)
|
|
557
|
+
}), error;
|
|
558
|
+
if (attempt >= maxAttempts)
|
|
559
|
+
throw opts.logRetries && devLog.error(
|
|
560
|
+
`Operation failed after ${maxAttempts} attempts`,
|
|
561
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
562
|
+
{
|
|
563
|
+
...opts.context,
|
|
564
|
+
totalAttempts: maxAttempts
|
|
565
|
+
}
|
|
566
|
+
), error;
|
|
567
|
+
const delay = calculateDelay(attempt - 1, opts);
|
|
568
|
+
opts.logRetries && devLog.warn(`Operation failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {
|
|
569
|
+
...opts.context,
|
|
570
|
+
attempt,
|
|
571
|
+
totalAttempts: maxAttempts,
|
|
572
|
+
delayMs: delay,
|
|
573
|
+
error: error instanceof Error ? error.message : String(error)
|
|
574
|
+
}), delay > 0 && await sleep(delay);
|
|
575
|
+
}
|
|
576
|
+
throw lastError;
|
|
577
|
+
}
|
|
412
578
|
class Semaphore {
|
|
413
579
|
permits;
|
|
414
580
|
queue = [];
|
|
@@ -759,12 +925,105 @@ const fetchTranslationPrompts = async ({
|
|
|
759
925
|
}
|
|
760
926
|
}), hasChanges = !0);
|
|
761
927
|
}
|
|
762
|
-
hasChanges && (await
|
|
928
|
+
hasChanges && (await retryWithBackoff(
|
|
929
|
+
async () => {
|
|
930
|
+
await patch.commit();
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
maxAttempts: 2,
|
|
934
|
+
initialDelayMs: 0,
|
|
935
|
+
retryableErrors: isNetworkError,
|
|
936
|
+
context: {
|
|
937
|
+
operation: "localizeReferences",
|
|
938
|
+
documentId,
|
|
939
|
+
referenceCount: refs.length
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
), devLog.info("Updated references in batch", {
|
|
763
943
|
documentId,
|
|
764
944
|
count: refs.length,
|
|
765
945
|
translatedCount: translationMap.size,
|
|
766
946
|
component: "updateReferences"
|
|
767
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
|
+
}
|
|
768
1027
|
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages, mainSourceDocumentId) => {
|
|
769
1028
|
for (const sourceDocument of sourceDocuments)
|
|
770
1029
|
for (const lang of targetLanguages) {
|
|
@@ -780,6 +1039,57 @@ const fetchTranslationPrompts = async ({
|
|
|
780
1039
|
payload
|
|
781
1040
|
}), mainSourceDocumentId && updateActiveTranslationJob(mainSourceDocumentId, payload);
|
|
782
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";
|
|
783
1093
|
}, updateTranslationMetadata = async ({
|
|
784
1094
|
client,
|
|
785
1095
|
targetLanguage,
|
|
@@ -813,11 +1123,71 @@ const fetchTranslationPrompts = async ({
|
|
|
813
1123
|
targetRev: newDocument._rev,
|
|
814
1124
|
targetSnapshot: JSON.stringify(newDocument)
|
|
815
1125
|
}, translationPatch = client.patch(translationMetadata._id).setIfMissing({ translations: [] });
|
|
816
|
-
isNewTranslation ? await
|
|
1126
|
+
isNewTranslation ? await retryWithBackoff(
|
|
1127
|
+
async () => {
|
|
1128
|
+
await translationPatch.insert("after", "translations[-1]", [refToNewTranslation]).commit();
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
maxAttempts: 2,
|
|
1132
|
+
initialDelayMs: 0,
|
|
1133
|
+
retryableErrors: isNetworkError,
|
|
1134
|
+
context: {
|
|
1135
|
+
operation: "updateTranslationMetadata.translations",
|
|
1136
|
+
sourceDocumentId: sourceDocument._id,
|
|
1137
|
+
targetLanguage,
|
|
1138
|
+
isNewTranslation: !0
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
) : await retryWithBackoff(
|
|
1142
|
+
async () => {
|
|
1143
|
+
await translationPatch.insert("replace", `translations[_key == "${targetLanguage}"]`, [refToNewTranslation]).commit();
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
maxAttempts: 2,
|
|
1147
|
+
initialDelayMs: 0,
|
|
1148
|
+
retryableErrors: isNetworkError,
|
|
1149
|
+
context: {
|
|
1150
|
+
operation: "updateTranslationMetadata.translations",
|
|
1151
|
+
sourceDocumentId: sourceDocument._id,
|
|
1152
|
+
targetLanguage,
|
|
1153
|
+
isNewTranslation: !1
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
);
|
|
817
1157
|
const historyPatch = client.patch(translationMetadata._id).setIfMissing({ translationHistory: [] });
|
|
818
1158
|
translationMetadata.translationHistory?.some(
|
|
819
1159
|
(history) => history.language === targetLanguage
|
|
820
|
-
) ? await
|
|
1160
|
+
) ? await retryWithBackoff(
|
|
1161
|
+
async () => {
|
|
1162
|
+
await historyPatch.insert("replace", `translationHistory[_key == "${targetLanguage}"]`, [newTranslationHistory]).commit();
|
|
1163
|
+
},
|
|
1164
|
+
{
|
|
1165
|
+
maxAttempts: 2,
|
|
1166
|
+
initialDelayMs: 0,
|
|
1167
|
+
retryableErrors: isNetworkError,
|
|
1168
|
+
context: {
|
|
1169
|
+
operation: "updateTranslationMetadata.history",
|
|
1170
|
+
sourceDocumentId: sourceDocument._id,
|
|
1171
|
+
targetLanguage,
|
|
1172
|
+
isNewHistory: !1
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
) : await retryWithBackoff(
|
|
1176
|
+
async () => {
|
|
1177
|
+
await historyPatch.insert("after", "translationHistory[-1]", [newTranslationHistory]).commit();
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
maxAttempts: 2,
|
|
1181
|
+
initialDelayMs: 0,
|
|
1182
|
+
retryableErrors: isNetworkError,
|
|
1183
|
+
context: {
|
|
1184
|
+
operation: "updateTranslationMetadata.history",
|
|
1185
|
+
sourceDocumentId: sourceDocument._id,
|
|
1186
|
+
targetLanguage,
|
|
1187
|
+
isNewHistory: !0
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
);
|
|
821
1191
|
} catch (error) {
|
|
822
1192
|
throw devLog.error("Error updating translation metadata", error instanceof Error ? error : new Error(String(error)), {
|
|
823
1193
|
sourceDocumentId: sourceDocument._id,
|
|
@@ -1068,7 +1438,9 @@ function restoreTransforms({
|
|
|
1068
1438
|
schemaType,
|
|
1069
1439
|
document: translatedDocument
|
|
1070
1440
|
};
|
|
1071
|
-
devLog.debug("Restoring transform for field", { fieldPath, schemaType })
|
|
1441
|
+
devLog.debug("Restoring transform for field", { fieldPath, schemaType });
|
|
1442
|
+
const restoredValue = transform.fromTranslatable(value, originalValue, context);
|
|
1443
|
+
restoredObj[key] = restoredValue;
|
|
1072
1444
|
} else
|
|
1073
1445
|
restoredObj[key] = restoreTransforms({
|
|
1074
1446
|
translatedDocument: value,
|
|
@@ -1161,14 +1533,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1161
1533
|
}), devLog.debug("OpenAI GPT-5 Full Prompt", {
|
|
1162
1534
|
messages: request.messages
|
|
1163
1535
|
});
|
|
1164
|
-
const response = await
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1536
|
+
const response = await retryWithBackoff(
|
|
1537
|
+
async () => {
|
|
1538
|
+
const fetchResponse = await fetch("https://api.openai.com/v1/responses", {
|
|
1539
|
+
method: "POST",
|
|
1540
|
+
headers: {
|
|
1541
|
+
"Content-Type": "application/json",
|
|
1542
|
+
Authorization: `Bearer ${apiKey}`
|
|
1543
|
+
},
|
|
1544
|
+
body: JSON.stringify(requestBody)
|
|
1545
|
+
});
|
|
1546
|
+
if (!fetchResponse.ok) {
|
|
1547
|
+
const errorMessage = `GPT-5 API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
|
|
1548
|
+
throw errorObj.response = { status: fetchResponse.status }, errorObj;
|
|
1549
|
+
}
|
|
1550
|
+
return fetchResponse;
|
|
1169
1551
|
},
|
|
1170
|
-
|
|
1171
|
-
|
|
1552
|
+
{
|
|
1553
|
+
maxAttempts: 2,
|
|
1554
|
+
initialDelayMs: 0,
|
|
1555
|
+
retryableErrors: isNetworkError,
|
|
1556
|
+
context: { model: requestBody.model, endpoint: "responses" }
|
|
1557
|
+
}
|
|
1558
|
+
);
|
|
1172
1559
|
if (!response.ok) {
|
|
1173
1560
|
const error = await response.json();
|
|
1174
1561
|
throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
|
|
@@ -1207,14 +1594,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1207
1594
|
}), devLog.debug("OpenAI Chat Completions Full Prompt", {
|
|
1208
1595
|
messages: request.messages
|
|
1209
1596
|
});
|
|
1210
|
-
const response = await
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1597
|
+
const response = await retryWithBackoff(
|
|
1598
|
+
async () => {
|
|
1599
|
+
const fetchResponse = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1600
|
+
method: "POST",
|
|
1601
|
+
headers: {
|
|
1602
|
+
"Content-Type": "application/json",
|
|
1603
|
+
Authorization: `Bearer ${apiKey}`
|
|
1604
|
+
},
|
|
1605
|
+
body: JSON.stringify(requestBody)
|
|
1606
|
+
});
|
|
1607
|
+
if (!fetchResponse.ok) {
|
|
1608
|
+
const errorMessage = `OpenAI API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
|
|
1609
|
+
throw errorObj.response = { status: fetchResponse.status }, errorObj;
|
|
1610
|
+
}
|
|
1611
|
+
return fetchResponse;
|
|
1215
1612
|
},
|
|
1216
|
-
|
|
1217
|
-
|
|
1613
|
+
{
|
|
1614
|
+
maxAttempts: 2,
|
|
1615
|
+
initialDelayMs: 0,
|
|
1616
|
+
retryableErrors: isNetworkError,
|
|
1617
|
+
context: { model, endpoint: "chat/completions" }
|
|
1618
|
+
}
|
|
1619
|
+
);
|
|
1218
1620
|
if (!response.ok) {
|
|
1219
1621
|
const error = await response.json();
|
|
1220
1622
|
throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
|
|
@@ -1276,16 +1678,63 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1276
1678
|
}, modelPricing = pricing[model] || pricing["gpt-5"], cachedInputRate = model === "gpt-5.1" ? 0.125 / 1e6 : modelPricing.input;
|
|
1277
1679
|
return Math.max(0, tokens.prompt - tokens.cached) * modelPricing.input + tokens.cached * cachedInputRate + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
|
|
1278
1680
|
}, deserializeDocument = (originalDoc, reviewedText) => {
|
|
1279
|
-
|
|
1681
|
+
devLog.debug("Raw GPT-5 response received", {
|
|
1682
|
+
length: reviewedText.length,
|
|
1683
|
+
preview: reviewedText.substring(0, 1e3),
|
|
1684
|
+
hasReasoning: reviewedText.includes("<reasoning>"),
|
|
1685
|
+
hasCodeBlocks: reviewedText.includes("```")
|
|
1686
|
+
});
|
|
1687
|
+
const cleanedText = preprocessResponse(reviewedText);
|
|
1688
|
+
cleanedText.length !== reviewedText.length && devLog.debug("Response after preprocessing", {
|
|
1689
|
+
originalLength: reviewedText.length,
|
|
1690
|
+
cleanedLength: cleanedText.length,
|
|
1691
|
+
preview: cleanedText.substring(0, 500)
|
|
1692
|
+
});
|
|
1693
|
+
const parsed = parseReviewFormat(cleanedText, originalDoc);
|
|
1694
|
+
devLog.debug("Parsed response structure", {
|
|
1695
|
+
rootNodeCount: parsed.length,
|
|
1696
|
+
rootNodeNames: parsed.map((n) => n.type === "field" && n.name ? n.name : "unknown")
|
|
1697
|
+
});
|
|
1698
|
+
const result = cloneDeep(originalDoc);
|
|
1280
1699
|
return applyTextChanges(result, parsed), result;
|
|
1281
|
-
},
|
|
1700
|
+
}, preprocessResponse = (text) => {
|
|
1701
|
+
const originalLength = text.length;
|
|
1702
|
+
let cleaned = text;
|
|
1703
|
+
const beforeReasoning = cleaned.length;
|
|
1704
|
+
cleaned = cleaned.replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, "");
|
|
1705
|
+
const reasoningRemoved = beforeReasoning !== cleaned.length, beforeCodeBlocks = cleaned.length;
|
|
1706
|
+
cleaned = cleaned.replace(/^```[a-z]*\s*$/gim, ""), cleaned = cleaned.replace(/^```\s*$/gim, "");
|
|
1707
|
+
const codeBlocksRemoved = beforeCodeBlocks !== cleaned.length, finalLength = cleaned.trim().length;
|
|
1708
|
+
return (reasoningRemoved || codeBlocksRemoved) && devLog.debug("Preprocessed GPT-5 response", {
|
|
1709
|
+
originalLength,
|
|
1710
|
+
finalLength,
|
|
1711
|
+
reasoningRemoved,
|
|
1712
|
+
codeBlocksRemoved,
|
|
1713
|
+
preview: cleaned.trim().substring(0, 200)
|
|
1714
|
+
}), cleaned.trim();
|
|
1715
|
+
}, parseReviewFormat = (text, originalDoc) => {
|
|
1282
1716
|
const root = [], stack = [], lines = text.split(`
|
|
1283
1717
|
`);
|
|
1718
|
+
devLog.debug("Parsing response text", {
|
|
1719
|
+
lineCount: lines.length,
|
|
1720
|
+
firstLines: lines.slice(0, 10).map((l, idx) => ({ line: idx, content: l.substring(0, 100) })),
|
|
1721
|
+
hasFieldTags: text.includes("<field"),
|
|
1722
|
+
hasBlockTags: text.includes("<block"),
|
|
1723
|
+
hasSpanTags: text.includes("<span"),
|
|
1724
|
+
textLength: text.length,
|
|
1725
|
+
textPreview: text.substring(0, 500)
|
|
1726
|
+
});
|
|
1727
|
+
let processedLineCount = 0, skippedEmptyLineCount = 0, matchedPatternCount = 0;
|
|
1284
1728
|
for (let i = 0; i < lines.length; i++) {
|
|
1285
1729
|
const trimmed = lines[i].trim();
|
|
1286
|
-
if (!trimmed)
|
|
1730
|
+
if (!trimmed) {
|
|
1731
|
+
skippedEmptyLineCount++;
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
processedLineCount++;
|
|
1287
1735
|
const compactStringMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
1288
1736
|
if (compactStringMatch && !trimmed.includes("<")) {
|
|
1737
|
+
matchedPatternCount++;
|
|
1289
1738
|
const [, fieldName, text2] = compactStringMatch, node = {
|
|
1290
1739
|
type: "field",
|
|
1291
1740
|
name: fieldName,
|
|
@@ -1302,6 +1751,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1302
1751
|
}
|
|
1303
1752
|
const compactStartMatch = trimmed.match(/^---\s+(\w+)\s+---$/);
|
|
1304
1753
|
if (compactStartMatch) {
|
|
1754
|
+
matchedPatternCount++;
|
|
1305
1755
|
const fieldName = compactStartMatch[1], contentLines = [];
|
|
1306
1756
|
for (i++; i < lines.length; ) {
|
|
1307
1757
|
const currentLine = lines[i];
|
|
@@ -1311,8 +1761,16 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1311
1761
|
}
|
|
1312
1762
|
const content = contentLines.join(`
|
|
1313
1763
|
`);
|
|
1314
|
-
let contentType
|
|
1315
|
-
|
|
1764
|
+
let contentType;
|
|
1765
|
+
try {
|
|
1766
|
+
const fieldValue = get(originalDoc, [fieldName]);
|
|
1767
|
+
if (fieldValue && typeof fieldValue == "object" && fieldValue !== null) {
|
|
1768
|
+
const fieldType = fieldValue._type;
|
|
1769
|
+
typeof fieldType == "string" && fieldType.startsWith("translatable.") && (contentType = fieldType);
|
|
1770
|
+
}
|
|
1771
|
+
} catch {
|
|
1772
|
+
}
|
|
1773
|
+
contentType || (content.includes("<block") || content.includes("<span") ? contentType = "translatable.richtext" : contentType = "translatable.vtt.captions");
|
|
1316
1774
|
const node = {
|
|
1317
1775
|
type: "field",
|
|
1318
1776
|
name: fieldName,
|
|
@@ -1329,6 +1787,12 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1329
1787
|
}
|
|
1330
1788
|
const openMatch = trimmed.match(/^<(field|block|span)([^>]*)>(.*)$/);
|
|
1331
1789
|
if (openMatch) {
|
|
1790
|
+
matchedPatternCount++, devLog.debug("Matched opening tag", {
|
|
1791
|
+
line: i,
|
|
1792
|
+
tagName: openMatch[1],
|
|
1793
|
+
attributes: openMatch[2],
|
|
1794
|
+
contentPreview: openMatch[3]?.substring(0, 50)
|
|
1795
|
+
});
|
|
1332
1796
|
const [, tagName, attrsStr, content] = openMatch, attributes = parseAttributes(attrsStr), node = {
|
|
1333
1797
|
type: tagName,
|
|
1334
1798
|
name: attributes.name,
|
|
@@ -1336,25 +1800,27 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1336
1800
|
attributes,
|
|
1337
1801
|
children: []
|
|
1338
1802
|
}, closingTag = `</${tagName}>`;
|
|
1339
|
-
if (content.
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
} else
|
|
1345
|
-
root.push(node);
|
|
1346
|
-
} else if (content.includes(closingTag))
|
|
1347
|
-
if (node.text = unescapeXml(content.replace(closingTag, "")), stack.length > 0) {
|
|
1803
|
+
if (content.includes(closingTag)) {
|
|
1804
|
+
const closeIdx = content.indexOf(closingTag);
|
|
1805
|
+
node.text = unescapeXml(content.substring(0, closeIdx));
|
|
1806
|
+
const remainder = content.substring(closeIdx + closingTag.length).trim();
|
|
1807
|
+
if (stack.length > 0) {
|
|
1348
1808
|
const parent = stack[stack.length - 1];
|
|
1349
1809
|
parent && parent.children.push(node);
|
|
1350
1810
|
} else
|
|
1351
1811
|
root.push(node);
|
|
1352
|
-
|
|
1812
|
+
remainder && (lines[i] = remainder, i--);
|
|
1813
|
+
} else if (attributes.type && typeof attributes.type == "string" && attributes.type.startsWith("translatable.")) {
|
|
1353
1814
|
const contentLines = [];
|
|
1354
1815
|
for (i++; i < lines.length; ) {
|
|
1355
1816
|
const currentLine = lines[i];
|
|
1356
|
-
if (currentLine.
|
|
1817
|
+
if (currentLine.includes("</field>")) {
|
|
1818
|
+
const idx = currentLine.indexOf("</field>"), before = currentLine.substring(0, idx).trimEnd();
|
|
1819
|
+
before.trim() && contentLines.push(before);
|
|
1820
|
+
const after = currentLine.substring(idx + 8).trim();
|
|
1821
|
+
after && (lines[i] = after, i--);
|
|
1357
1822
|
break;
|
|
1823
|
+
}
|
|
1358
1824
|
contentLines.push(currentLine), i++;
|
|
1359
1825
|
}
|
|
1360
1826
|
if (node.text = contentLines.join(`
|
|
@@ -1363,18 +1829,36 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1363
1829
|
parent && parent.children.push(node);
|
|
1364
1830
|
} else
|
|
1365
1831
|
root.push(node);
|
|
1366
|
-
} else if (attributes.type === "string" && content && content.includes(closingTag))
|
|
1367
|
-
|
|
1832
|
+
} else if (attributes.type === "string" && content && !content.includes(closingTag)) {
|
|
1833
|
+
const contentLines = [content];
|
|
1834
|
+
for (i++; i < lines.length; ) {
|
|
1835
|
+
const currentLine = lines[i];
|
|
1836
|
+
if (currentLine.includes(closingTag)) {
|
|
1837
|
+
const closingIndex = currentLine.indexOf(closingTag), remainingContent = currentLine.substring(0, closingIndex).trimEnd();
|
|
1838
|
+
remainingContent && contentLines.push(remainingContent);
|
|
1839
|
+
const after = currentLine.substring(closingIndex + closingTag.length).trim();
|
|
1840
|
+
after && (lines[i] = after, i--);
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1843
|
+
contentLines.push(currentLine), i++;
|
|
1844
|
+
}
|
|
1845
|
+
if (node.text = unescapeXml(contentLines.join(`
|
|
1846
|
+
`)), stack.length > 0) {
|
|
1368
1847
|
const parent = stack[stack.length - 1];
|
|
1369
1848
|
parent && parent.children.push(node);
|
|
1370
1849
|
} else
|
|
1371
1850
|
root.push(node);
|
|
1372
|
-
else if (attributes.type === "string" && !content) {
|
|
1851
|
+
} else if (attributes.type === "string" && !content) {
|
|
1373
1852
|
const contentLines = [];
|
|
1374
1853
|
for (i++; i < lines.length; ) {
|
|
1375
1854
|
const currentLine = lines[i];
|
|
1376
|
-
if (currentLine.
|
|
1855
|
+
if (currentLine.includes("</field>")) {
|
|
1856
|
+
const idx = currentLine.indexOf("</field>"), before = currentLine.substring(0, idx).trimEnd();
|
|
1857
|
+
before.trim() && contentLines.push(before);
|
|
1858
|
+
const after = currentLine.substring(idx + 8).trim();
|
|
1859
|
+
after && (lines[i] = after, i--);
|
|
1377
1860
|
break;
|
|
1861
|
+
}
|
|
1378
1862
|
contentLines.push(currentLine), i++;
|
|
1379
1863
|
}
|
|
1380
1864
|
if (node.text = contentLines.join(`
|
|
@@ -1387,17 +1871,130 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1387
1871
|
stack.push(node);
|
|
1388
1872
|
continue;
|
|
1389
1873
|
}
|
|
1390
|
-
if (trimmed.match(/^<\/(field|block|span)
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1874
|
+
if (trimmed.match(/^<\/(field|block|span)>/)) {
|
|
1875
|
+
let afterClosingTag = trimmed;
|
|
1876
|
+
for (; ; ) {
|
|
1877
|
+
const match = afterClosingTag.match(/^<\/(field|block|span)>/);
|
|
1878
|
+
if (!match) break;
|
|
1879
|
+
if (stack.length === 0) {
|
|
1880
|
+
devLog.warn("Closing tag found but stack is empty", {
|
|
1881
|
+
line: i,
|
|
1882
|
+
tagName: match[1],
|
|
1883
|
+
trimmed: afterClosingTag
|
|
1884
|
+
}), afterClosingTag = afterClosingTag.substring(match[0].length).trim(), afterClosingTag = afterClosingTag.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
matchedPatternCount++, devLog.debug("Matched closing tag", {
|
|
1888
|
+
line: i,
|
|
1889
|
+
tagName: match[1],
|
|
1890
|
+
trimmed: afterClosingTag,
|
|
1891
|
+
stackLength: stack.length,
|
|
1892
|
+
stackTopName: stack[stack.length - 1]?.name
|
|
1893
|
+
});
|
|
1894
|
+
const node = stack.pop();
|
|
1895
|
+
if (node)
|
|
1896
|
+
if (stack.length > 0) {
|
|
1897
|
+
const parent = stack[stack.length - 1];
|
|
1898
|
+
parent && parent.children.push(node);
|
|
1899
|
+
} else
|
|
1900
|
+
root.push(node);
|
|
1901
|
+
afterClosingTag = afterClosingTag.substring(match[0].length).trim(), afterClosingTag = afterClosingTag.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
1902
|
+
}
|
|
1903
|
+
if (afterClosingTag) {
|
|
1904
|
+
const remainingOpenMatch = afterClosingTag.match(/^<(field|block|span)([^>]*)>(.*)$/);
|
|
1905
|
+
if (remainingOpenMatch) {
|
|
1906
|
+
const [, tagName, attrsStr, content] = remainingOpenMatch, attributes = parseAttributes(attrsStr), remainingNode = {
|
|
1907
|
+
type: tagName,
|
|
1908
|
+
name: attributes.name,
|
|
1909
|
+
text: content ? unescapeXml(content) : void 0,
|
|
1910
|
+
attributes,
|
|
1911
|
+
children: []
|
|
1912
|
+
}, remainingClosingTag = `</${tagName}>`;
|
|
1913
|
+
if (content.includes(remainingClosingTag)) {
|
|
1914
|
+
const closeIdx = content.indexOf(remainingClosingTag);
|
|
1915
|
+
remainingNode.text = unescapeXml(content.substring(0, closeIdx));
|
|
1916
|
+
const remainder = content.substring(closeIdx + remainingClosingTag.length).trim();
|
|
1917
|
+
if (stack.length > 0) {
|
|
1918
|
+
const parent = stack[stack.length - 1];
|
|
1919
|
+
parent && parent.children.push(remainingNode);
|
|
1920
|
+
} else
|
|
1921
|
+
root.push(remainingNode);
|
|
1922
|
+
remainder && (lines[i] = remainder, i--);
|
|
1923
|
+
} else if (attributes.type === "string" && content)
|
|
1924
|
+
if (remainingNode.text = unescapeXml(content), stack.length > 0) {
|
|
1925
|
+
const parent = stack[stack.length - 1];
|
|
1926
|
+
parent && parent.children.push(remainingNode);
|
|
1927
|
+
} else
|
|
1928
|
+
root.push(remainingNode);
|
|
1929
|
+
else if (attributes.type === "string" && !content) {
|
|
1930
|
+
const contentLines = [];
|
|
1931
|
+
for (i++; i < lines.length; ) {
|
|
1932
|
+
const currentLine = lines[i];
|
|
1933
|
+
if (currentLine.includes(`</${tagName}>`)) {
|
|
1934
|
+
const idx = currentLine.indexOf(`</${tagName}>`), before = currentLine.substring(0, idx).trimEnd();
|
|
1935
|
+
before.trim() && contentLines.push(before);
|
|
1936
|
+
const after = currentLine.substring(idx + `</${tagName}>`.length).trim();
|
|
1937
|
+
after && (lines[i] = after, i--);
|
|
1938
|
+
break;
|
|
1939
|
+
}
|
|
1940
|
+
contentLines.push(currentLine), i++;
|
|
1941
|
+
}
|
|
1942
|
+
if (remainingNode.text = contentLines.join(`
|
|
1943
|
+
`).trim(), stack.length > 0) {
|
|
1944
|
+
const parent = stack[stack.length - 1];
|
|
1945
|
+
parent && parent.children.push(remainingNode);
|
|
1946
|
+
} else
|
|
1947
|
+
root.push(remainingNode);
|
|
1948
|
+
} else
|
|
1949
|
+
stack.push(remainingNode);
|
|
1396
1950
|
} else
|
|
1397
|
-
|
|
1951
|
+
lines[i] = afterClosingTag, i--;
|
|
1952
|
+
}
|
|
1953
|
+
continue;
|
|
1954
|
+
}
|
|
1955
|
+
if (trimmed.startsWith("</")) {
|
|
1956
|
+
const attemptedCloseMatch = trimmed.match(/^<\/(field|block|span)>/);
|
|
1957
|
+
attemptedCloseMatch ? stack.length === 0 && devLog.warn("Closing tag matched but stack is empty (unmatched closing tag)", {
|
|
1958
|
+
line: i,
|
|
1959
|
+
tagName: attemptedCloseMatch[1],
|
|
1960
|
+
trimmed
|
|
1961
|
+
}) : devLog.warn("Line starts with </ but did not match closing tag pattern", {
|
|
1962
|
+
line: i,
|
|
1963
|
+
trimmed,
|
|
1964
|
+
stackLength: stack.length,
|
|
1965
|
+
stackTopName: stack[stack.length - 1]?.name,
|
|
1966
|
+
stackTopType: stack[stack.length - 1]?.type
|
|
1967
|
+
});
|
|
1398
1968
|
}
|
|
1969
|
+
processedLineCount <= 20 && devLog.debug("Line did not match any pattern", {
|
|
1970
|
+
line: i,
|
|
1971
|
+
trimmed,
|
|
1972
|
+
startsWithLessThan: trimmed.startsWith("<"),
|
|
1973
|
+
startsWithField: trimmed.startsWith("<field"),
|
|
1974
|
+
startsWithBacktick: trimmed.startsWith("`"),
|
|
1975
|
+
startsWithClosingTag: trimmed.startsWith("</")
|
|
1976
|
+
});
|
|
1399
1977
|
}
|
|
1400
|
-
|
|
1978
|
+
if (stack.length > 0)
|
|
1979
|
+
for (devLog.warn("Nodes remaining in stack after parsing complete", {
|
|
1980
|
+
stackLength: stack.length,
|
|
1981
|
+
remainingNodes: stack.map((n) => ({
|
|
1982
|
+
type: n.type,
|
|
1983
|
+
name: n.name,
|
|
1984
|
+
attributes: n.attributes
|
|
1985
|
+
}))
|
|
1986
|
+
}); stack.length > 0; ) {
|
|
1987
|
+
const node = stack.pop();
|
|
1988
|
+
node && root.push(node);
|
|
1989
|
+
}
|
|
1990
|
+
return devLog.debug("Parse complete", {
|
|
1991
|
+
totalLines: lines.length,
|
|
1992
|
+
processedLines: processedLineCount,
|
|
1993
|
+
skippedEmptyLines: skippedEmptyLineCount,
|
|
1994
|
+
rootNodeCount: root.length,
|
|
1995
|
+
stackLength: stack.length,
|
|
1996
|
+
matchedPatterns: matchedPatternCount
|
|
1997
|
+
}), root;
|
|
1401
1998
|
}, parseAttributes = (attrStr) => {
|
|
1402
1999
|
const attrs = {}, attrRegex = /(\w+)="([^"]*)"/g;
|
|
1403
2000
|
let match = attrRegex.exec(attrStr);
|
|
@@ -1405,20 +2002,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1405
2002
|
attrs[match[1]] = unescapeXml(match[2]), match = attrRegex.exec(attrStr);
|
|
1406
2003
|
return attrs;
|
|
1407
2004
|
}, unescapeXml = (str) => str.replace(/'/g, "'").replace(/"/g, '"').replace(/>/g, ">").replace(/</g, "<").replace(/&/g, "&"), applyTextChanges = (doc, parsedNodes) => {
|
|
2005
|
+
devLog.debug("Applying text changes from parsed nodes", {
|
|
2006
|
+
nodeCount: parsedNodes.length,
|
|
2007
|
+
nodeNames: parsedNodes.map((n) => n.type === "field" && n.name ? n.name : "unknown")
|
|
2008
|
+
});
|
|
1408
2009
|
for (const node of parsedNodes)
|
|
1409
2010
|
if (node.type === "field" && node.name) {
|
|
1410
|
-
const path = [node.name];
|
|
2011
|
+
const path = [node.name], beforeValue = get(doc, path);
|
|
1411
2012
|
applyFieldChanges(doc, path, node);
|
|
2013
|
+
const afterValue = get(doc, path);
|
|
2014
|
+
beforeValue !== afterValue && devLog.debug("Field updated during deserialization", {
|
|
2015
|
+
path: path.join("."),
|
|
2016
|
+
before: typeof beforeValue == "string" ? beforeValue.substring(0, 50) : beforeValue,
|
|
2017
|
+
after: typeof afterValue == "string" ? afterValue.substring(0, 50) : afterValue
|
|
2018
|
+
});
|
|
1412
2019
|
}
|
|
1413
2020
|
}, 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") {
|
|
2021
|
+
const nodeType = node.attributes.type;
|
|
2022
|
+
if (typeof nodeType == "string" && nodeType.startsWith("translatable.")) {
|
|
1420
2023
|
const fieldValue = get(doc, path);
|
|
1421
|
-
fieldValue && typeof fieldValue == "object" && fieldValue
|
|
2024
|
+
if (fieldValue && typeof fieldValue == "object" && fieldValue !== null) {
|
|
2025
|
+
const fieldType = fieldValue._type;
|
|
2026
|
+
typeof fieldType == "string" && fieldType === nodeType && node.text && set(doc, [...path, "translationText"], node.text);
|
|
2027
|
+
}
|
|
1422
2028
|
return;
|
|
1423
2029
|
}
|
|
1424
2030
|
if (node.attributes.type === "array") {
|
|
@@ -1428,8 +2034,28 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1428
2034
|
return;
|
|
1429
2035
|
}
|
|
1430
2036
|
if (node.attributes.type === "object") {
|
|
1431
|
-
|
|
1432
|
-
|
|
2037
|
+
const currentValue = get(doc, path);
|
|
2038
|
+
if (currentValue && typeof currentValue == "object" && !Array.isArray(currentValue)) {
|
|
2039
|
+
const currentValueObj = currentValue;
|
|
2040
|
+
for (const child of node.children)
|
|
2041
|
+
if (child.name) {
|
|
2042
|
+
const childValue = currentValueObj[child.name];
|
|
2043
|
+
if (/^\d+$/.test(child.name) && childValue === void 0) {
|
|
2044
|
+
devLog.warn("Skipping numeric child name in object field - field does not exist (likely parsing error)", {
|
|
2045
|
+
path: path.join("."),
|
|
2046
|
+
childName: child.name,
|
|
2047
|
+
availableKeys: Object.keys(currentValueObj),
|
|
2048
|
+
childType: child.attributes.type
|
|
2049
|
+
});
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
applyFieldChanges(doc, [...path, child.name], child);
|
|
2053
|
+
}
|
|
2054
|
+
} else
|
|
2055
|
+
devLog.warn("Object field type but value is not an object", {
|
|
2056
|
+
path: path.join("."),
|
|
2057
|
+
actualType: Array.isArray(currentValue) ? "array" : typeof currentValue
|
|
2058
|
+
});
|
|
1433
2059
|
return;
|
|
1434
2060
|
}
|
|
1435
2061
|
if (node.children.some((c) => c.type === "block")) {
|
|
@@ -1439,10 +2065,17 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1439
2065
|
});
|
|
1440
2066
|
return;
|
|
1441
2067
|
}
|
|
1442
|
-
node.text !== void 0 && !node.attributes.readonly
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
2068
|
+
if (node.text !== void 0 && !node.attributes.readonly) {
|
|
2069
|
+
const existingValue = get(doc, path);
|
|
2070
|
+
existingValue !== void 0 && typeof existingValue == "string" ? set(doc, path, node.text) : existingValue !== void 0 ? devLog.debug("Skipping field - existing value is not a string (would corrupt structure)", {
|
|
2071
|
+
path: path.join("."),
|
|
2072
|
+
existingType: typeof existingValue,
|
|
2073
|
+
attemptedValue: node.text?.substring(0, 50)
|
|
2074
|
+
}) : devLog.debug("Skipping field not present in original document", {
|
|
2075
|
+
path: path.join("."),
|
|
2076
|
+
attemptedValue: node.text
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
1446
2079
|
}, applyBlockChanges = (block, blockNode) => {
|
|
1447
2080
|
if (blockNode.attributes._key && block._key !== blockNode.attributes._key) {
|
|
1448
2081
|
devLog.warn("Block key mismatch, skipping", {
|
|
@@ -1473,7 +2106,11 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1473
2106
|
});
|
|
1474
2107
|
return;
|
|
1475
2108
|
}
|
|
1476
|
-
fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (fieldName in block ? block[fieldName] = fieldNode.text : devLog.debug("Skipping block field not
|
|
2109
|
+
fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (fieldName in block && typeof block[fieldName] == "string" ? block[fieldName] = fieldNode.text : fieldName in block ? devLog.debug("Skipping block field - existing value is not a string (would corrupt structure)", {
|
|
2110
|
+
fieldName,
|
|
2111
|
+
existingType: typeof block[fieldName],
|
|
2112
|
+
attemptedValue: fieldNode.text?.substring(0, 50)
|
|
2113
|
+
}) : devLog.debug("Skipping block field not present in original", {
|
|
1477
2114
|
fieldName,
|
|
1478
2115
|
attemptedValue: fieldNode.text
|
|
1479
2116
|
}));
|
|
@@ -1506,12 +2143,9 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1506
2143
|
return;
|
|
1507
2144
|
}
|
|
1508
2145
|
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} ---`);
|
|
2146
|
+
const valueObj = value, valueType = valueObj._type, translationText = valueObj.translationText;
|
|
2147
|
+
if (typeof valueType == "string" && valueType.startsWith("translatable.") && typeof translationText == "string") {
|
|
2148
|
+
lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
|
|
1515
2149
|
return;
|
|
1516
2150
|
}
|
|
1517
2151
|
lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
|
|
@@ -1647,15 +2281,19 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1647
2281
|
languageFieldName,
|
|
1648
2282
|
translationPrompts,
|
|
1649
2283
|
targetLanguage,
|
|
1650
|
-
openaiConfig
|
|
2284
|
+
openaiConfig,
|
|
2285
|
+
previousError: _previousError,
|
|
2286
|
+
errorContext,
|
|
2287
|
+
telemetryConfig
|
|
1651
2288
|
}) => {
|
|
1652
|
-
const sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
2289
|
+
const startTime = Date.now(), sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
1653
2290
|
sourceId: sourceDocument._id,
|
|
1654
2291
|
sourceLanguage
|
|
1655
2292
|
};
|
|
2293
|
+
let serializedDocument, openaiResponseContent;
|
|
1656
2294
|
try {
|
|
1657
2295
|
const sourceText = serializeDocument(sourceDocument);
|
|
1658
|
-
devLog.info("Serialized document for OpenAI", {
|
|
2296
|
+
serializedDocument = sourceText, devLog.info("Serialized document for OpenAI", {
|
|
1659
2297
|
...logContext,
|
|
1660
2298
|
characterCount: sourceText.length,
|
|
1661
2299
|
estimatedTokens: Math.ceil(sourceText.length / 4),
|
|
@@ -1665,7 +2303,8 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1665
2303
|
sourceText,
|
|
1666
2304
|
sourceLanguage,
|
|
1667
2305
|
targetLanguage,
|
|
1668
|
-
translationPrompts
|
|
2306
|
+
translationPrompts,
|
|
2307
|
+
errorContext
|
|
1669
2308
|
}), response = await callGPT5(openaiConfig.apiKey, {
|
|
1670
2309
|
messages: [
|
|
1671
2310
|
{
|
|
@@ -1677,7 +2316,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1677
2316
|
model: openaiConfig.model,
|
|
1678
2317
|
reasoningEffort: openaiConfig.reasoningEffort
|
|
1679
2318
|
});
|
|
1680
|
-
devLog.info("OpenAI translation response received", {
|
|
2319
|
+
openaiResponseContent = response.content, devLog.info("OpenAI translation response received", {
|
|
1681
2320
|
...logContext,
|
|
1682
2321
|
tokensUsed: response.tokensUsed,
|
|
1683
2322
|
cost: response.cost,
|
|
@@ -1715,22 +2354,55 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1715
2354
|
})), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
|
|
1716
2355
|
tokensUsed: response.tokensUsed.total,
|
|
1717
2356
|
estimatedCost: response.cost
|
|
1718
|
-
}),
|
|
2357
|
+
}), {
|
|
2358
|
+
document: result,
|
|
2359
|
+
tokensUsed: response.tokensUsed.total,
|
|
2360
|
+
cost: response.cost
|
|
2361
|
+
};
|
|
1719
2362
|
} catch (error) {
|
|
1720
|
-
|
|
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;
|
|
1721
2386
|
}
|
|
1722
2387
|
}, buildTranslationPrompt$1 = ({
|
|
1723
2388
|
sourceText,
|
|
1724
2389
|
sourceLanguage,
|
|
1725
2390
|
targetLanguage,
|
|
1726
|
-
translationPrompts
|
|
2391
|
+
translationPrompts,
|
|
2392
|
+
errorContext
|
|
1727
2393
|
}) => {
|
|
1728
2394
|
const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
|
|
1729
2395
|
return `# TRANSLATION TASK
|
|
1730
2396
|
|
|
1731
2397
|
Translate this ${sourceLanguage} document into ${targetLanguage}.
|
|
1732
2398
|
|
|
1733
|
-
|
|
2399
|
+
${errorContext ? `## \u26A0\uFE0F IMPORTANT: Previous Attempt Error
|
|
2400
|
+
|
|
2401
|
+
${errorContext}
|
|
2402
|
+
|
|
2403
|
+
Please carefully review the error above and ensure your translation fixes the issue.
|
|
2404
|
+
|
|
2405
|
+
` : ""}## Guidelines
|
|
1734
2406
|
|
|
1735
2407
|
${globalPrompt ? `### General Guidelines
|
|
1736
2408
|
${globalPrompt}
|
|
@@ -1904,21 +2576,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
1904
2576
|
languageFieldName,
|
|
1905
2577
|
targetLanguage
|
|
1906
2578
|
});
|
|
1907
|
-
return await
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2579
|
+
return await retryWithBackoff(
|
|
2580
|
+
async () => await client.agent.action.translate({
|
|
2581
|
+
schemaId,
|
|
2582
|
+
toLanguage,
|
|
2583
|
+
fromLanguage,
|
|
2584
|
+
documentId: sourceDocument._id,
|
|
2585
|
+
noWrite: !0,
|
|
2586
|
+
// Do not write to the document, just return the translated content
|
|
2587
|
+
languageFieldPath: [languageFieldName],
|
|
2588
|
+
styleGuide,
|
|
2589
|
+
protectedPhrases: translationPrompts.protectedPhrases,
|
|
2590
|
+
conditionalPaths: {
|
|
2591
|
+
defaultHidden: !1,
|
|
2592
|
+
defaultReadOnly: !1
|
|
2593
|
+
}
|
|
2594
|
+
}),
|
|
2595
|
+
{
|
|
2596
|
+
maxAttempts: 2,
|
|
2597
|
+
initialDelayMs: 0,
|
|
2598
|
+
retryableErrors: isNetworkError,
|
|
2599
|
+
context: {
|
|
2600
|
+
operation: "agent.action.translate",
|
|
2601
|
+
documentId: sourceDocument._id,
|
|
2602
|
+
targetLanguage,
|
|
2603
|
+
schemaId
|
|
2604
|
+
}
|
|
1920
2605
|
}
|
|
1921
|
-
|
|
2606
|
+
);
|
|
1922
2607
|
}, handleTranslation = async ({
|
|
1923
2608
|
dispatch,
|
|
1924
2609
|
client,
|
|
@@ -1933,9 +2618,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
1933
2618
|
releaseId,
|
|
1934
2619
|
excludeFields = [],
|
|
1935
2620
|
replaceExisting = !1,
|
|
1936
|
-
mainSourceDocumentId
|
|
2621
|
+
mainSourceDocumentId,
|
|
2622
|
+
telemetryConfig
|
|
1937
2623
|
}) => {
|
|
1938
|
-
const safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
|
|
2624
|
+
const startTime = Date.now(), sourceLanguage = sourceDocument[languageFieldName], safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
|
|
1939
2625
|
excludeFields.length !== safeExcludeFields.length && devLog.warn("Language field was in excludeFields - removing it to prevent translation errors", {
|
|
1940
2626
|
documentId: sourceDocument._id,
|
|
1941
2627
|
languageFieldName,
|
|
@@ -1974,43 +2660,79 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
1974
2660
|
documentId: sourceDocument._id,
|
|
1975
2661
|
documentType: sourceDocument._type
|
|
1976
2662
|
}), 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
|
-
|
|
2663
|
+
let translatedDoc, translationMetrics = {}, previousError = null;
|
|
2664
|
+
const performTranslation = async (errorContext) => {
|
|
2665
|
+
switch (translationMode) {
|
|
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
|
+
}
|
|
2700
|
+
case "openai-only": {
|
|
2701
|
+
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2702
|
+
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2703
|
+
setJobStatus(
|
|
2704
|
+
dispatch,
|
|
2705
|
+
sourceDocument._id,
|
|
2706
|
+
targetLanguage,
|
|
2707
|
+
"translating_with_openai",
|
|
2708
|
+
void 0,
|
|
2709
|
+
mainSourceDocumentId
|
|
2710
|
+
);
|
|
2711
|
+
const result = await getOpenAITranslation({
|
|
2712
|
+
dispatch,
|
|
2713
|
+
sourceDocument: documentToTranslate,
|
|
2714
|
+
targetLanguage,
|
|
2715
|
+
languageFieldName,
|
|
2716
|
+
translationPrompts,
|
|
2717
|
+
openaiConfig,
|
|
2718
|
+
previousError: errorContext && previousError ? previousError : void 0,
|
|
2719
|
+
errorContext,
|
|
2720
|
+
telemetryConfig
|
|
2721
|
+
});
|
|
2722
|
+
return result ? {
|
|
2723
|
+
document: result.document,
|
|
2724
|
+
tokensUsed: result.tokensUsed,
|
|
2725
|
+
cost: result.cost
|
|
2726
|
+
} : { document: null };
|
|
2727
|
+
}
|
|
2728
|
+
default:
|
|
2729
|
+
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2009
2730
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2731
|
+
}, translationResult = await performTranslation();
|
|
2732
|
+
if (translatedDoc = translationResult.document, translationMetrics = {
|
|
2733
|
+
tokensUsed: translationResult.tokensUsed,
|
|
2734
|
+
cost: translationResult.cost
|
|
2735
|
+
}, !translatedDoc)
|
|
2014
2736
|
return null;
|
|
2015
2737
|
let transformedResult;
|
|
2016
2738
|
Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
|
|
@@ -2041,15 +2763,88 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2041
2763
|
...finalDoc,
|
|
2042
2764
|
_id: targetDocumentVersionId
|
|
2043
2765
|
};
|
|
2044
|
-
|
|
2766
|
+
replaceExisting && await client.getDocument(targetDocumentVersionId) && (devLog.info("Replacing existing version in release", {
|
|
2045
2767
|
documentId: sourceDocument._id,
|
|
2046
2768
|
targetDocumentVersionId,
|
|
2047
2769
|
releaseId
|
|
2048
|
-
}), await client.delete(targetDocumentVersionId))
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2770
|
+
}), await client.delete(targetDocumentVersionId));
|
|
2771
|
+
try {
|
|
2772
|
+
await retryWithBackoff(
|
|
2773
|
+
async () => {
|
|
2774
|
+
await client.action({
|
|
2775
|
+
actionType: "sanity.action.document.version.create",
|
|
2776
|
+
publishedId: targetDocumentPublishedId,
|
|
2777
|
+
document: newDoc
|
|
2778
|
+
});
|
|
2779
|
+
},
|
|
2780
|
+
{
|
|
2781
|
+
maxAttempts: 2,
|
|
2782
|
+
initialDelayMs: 0,
|
|
2783
|
+
retryableErrors: isNetworkError,
|
|
2784
|
+
context: {
|
|
2785
|
+
documentId: sourceDocument._id,
|
|
2786
|
+
targetLanguage,
|
|
2787
|
+
operation: "version.create"
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
);
|
|
2791
|
+
} catch (error) {
|
|
2792
|
+
if (translationMode === "openai-only" && isSanityMutationError(error)) {
|
|
2793
|
+
devLog.warn("Mutation error after OpenAI translation, retrying full translation", {
|
|
2794
|
+
documentId: sourceDocument._id,
|
|
2795
|
+
targetLanguage,
|
|
2796
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2797
|
+
}), previousError = error instanceof Error ? error : new Error(String(error));
|
|
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)
|
|
2800
|
+
throw new Error("Translation retry failed: no document returned");
|
|
2801
|
+
translationMetrics = {
|
|
2802
|
+
tokensUsed: retryResult.tokensUsed,
|
|
2803
|
+
cost: retryResult.cost
|
|
2804
|
+
};
|
|
2805
|
+
let retryTransformedResult;
|
|
2806
|
+
Object.keys(originalStructure).length > 0 ? retryTransformedResult = restoreTransforms({
|
|
2807
|
+
translatedDocument: translatedDoc,
|
|
2808
|
+
originalStructure,
|
|
2809
|
+
registry: globalTransformRegistry,
|
|
2810
|
+
schemaType: sourceDocument._type
|
|
2811
|
+
}) : retryTransformedResult = translatedDoc;
|
|
2812
|
+
const retryNewDoc = {
|
|
2813
|
+
...restoreExcludedFields(retryTransformedResult, excludedValuesToRestore),
|
|
2814
|
+
_id: targetDocumentVersionId
|
|
2815
|
+
};
|
|
2816
|
+
return await retryWithBackoff(
|
|
2817
|
+
async () => {
|
|
2818
|
+
await client.action({
|
|
2819
|
+
actionType: "sanity.action.document.version.create",
|
|
2820
|
+
publishedId: targetDocumentPublishedId,
|
|
2821
|
+
document: retryNewDoc
|
|
2822
|
+
});
|
|
2823
|
+
},
|
|
2824
|
+
{
|
|
2825
|
+
maxAttempts: 2,
|
|
2826
|
+
initialDelayMs: 0,
|
|
2827
|
+
retryableErrors: isNetworkError,
|
|
2828
|
+
context: {
|
|
2829
|
+
documentId: sourceDocument._id,
|
|
2830
|
+
targetLanguage,
|
|
2831
|
+
operation: "version.create",
|
|
2832
|
+
isRetry: !0
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
), {
|
|
2836
|
+
document: retryNewDoc,
|
|
2837
|
+
tokensUsed: translationMetrics.tokensUsed,
|
|
2838
|
+
cost: translationMetrics.cost
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
throw error;
|
|
2842
|
+
}
|
|
2843
|
+
return {
|
|
2844
|
+
document: newDoc,
|
|
2845
|
+
tokensUsed: translationMetrics.tokensUsed,
|
|
2846
|
+
cost: translationMetrics.cost
|
|
2847
|
+
};
|
|
2053
2848
|
}, translateDocument = async ({
|
|
2054
2849
|
dispatch,
|
|
2055
2850
|
client,
|
|
@@ -2064,9 +2859,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2064
2859
|
releaseId,
|
|
2065
2860
|
excludeFields,
|
|
2066
2861
|
replaceExisting,
|
|
2067
|
-
mainSourceDocumentId
|
|
2862
|
+
mainSourceDocumentId,
|
|
2863
|
+
telemetryConfig
|
|
2068
2864
|
}) => {
|
|
2069
|
-
const logContext = {
|
|
2865
|
+
const jobStartTime = Date.now(), logContext = {
|
|
2070
2866
|
documentId: sourceDocument._id,
|
|
2071
2867
|
targetLanguage,
|
|
2072
2868
|
schemaId,
|
|
@@ -2078,7 +2874,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2078
2874
|
client,
|
|
2079
2875
|
sourceDocument,
|
|
2080
2876
|
sourceLanguage
|
|
2081
|
-
}), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage),
|
|
2877
|
+
}), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), translationResult = await handleTranslation({
|
|
2082
2878
|
dispatch,
|
|
2083
2879
|
client,
|
|
2084
2880
|
schemaId,
|
|
@@ -2092,8 +2888,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2092
2888
|
releaseId,
|
|
2093
2889
|
excludeFields,
|
|
2094
2890
|
replaceExisting,
|
|
2095
|
-
mainSourceDocumentId
|
|
2096
|
-
|
|
2891
|
+
mainSourceDocumentId,
|
|
2892
|
+
telemetryConfig
|
|
2893
|
+
}), newDocument = translationResult?.document;
|
|
2097
2894
|
if (!newDocument) {
|
|
2098
2895
|
devLog.error("Translation failed: no document returned", new Error("Translation returned null"), logContext), setJobStatus(
|
|
2099
2896
|
dispatch,
|
|
@@ -2130,12 +2927,27 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2130
2927
|
targetLanguage,
|
|
2131
2928
|
"completed",
|
|
2132
2929
|
{
|
|
2133
|
-
targetId: newDocument._id
|
|
2930
|
+
targetId: newDocument._id,
|
|
2931
|
+
tokensUsed: translationResult?.tokensUsed,
|
|
2932
|
+
estimatedCost: translationResult?.cost
|
|
2134
2933
|
},
|
|
2135
2934
|
mainSourceDocumentId
|
|
2136
2935
|
), devLog.info("Translation completed successfully", {
|
|
2137
2936
|
...logContext,
|
|
2138
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
|
+
}
|
|
2139
2951
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
2140
2952
|
} catch (error) {
|
|
2141
2953
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -2163,9 +2975,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2163
2975
|
translationMode,
|
|
2164
2976
|
openaiConfig,
|
|
2165
2977
|
excludeFields,
|
|
2166
|
-
releaseConfig
|
|
2978
|
+
releaseConfig,
|
|
2979
|
+
telemetryConfig
|
|
2167
2980
|
}) => {
|
|
2168
|
-
const logContext = {
|
|
2981
|
+
const batchStartTime = Date.now(), logContext = {
|
|
2169
2982
|
documentId: sourceDocument._id,
|
|
2170
2983
|
documentRev: sourceDocument._rev,
|
|
2171
2984
|
targetLanguages,
|
|
@@ -2179,7 +2992,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2179
2992
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
2180
2993
|
if (!sourceLanguage)
|
|
2181
2994
|
throw devLog.error("Source language not found"), new Error("Source language not found");
|
|
2182
|
-
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({
|
|
2183
3008
|
type: "SET_OVERALL_STATUS",
|
|
2184
3009
|
payload: "translating"
|
|
2185
3010
|
}), releaseConfig.mode === "existing" && releaseConfig.existingReleaseId)
|
|
@@ -2189,13 +3014,25 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2189
3014
|
});
|
|
2190
3015
|
else {
|
|
2191
3016
|
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
|
-
|
|
3017
|
+
releaseId = (await retryWithBackoff(
|
|
3018
|
+
async () => await client.releases.create({
|
|
3019
|
+
metadata: {
|
|
3020
|
+
title: releaseTitle,
|
|
3021
|
+
description: releaseDescription,
|
|
3022
|
+
releaseType: "undecided"
|
|
3023
|
+
}
|
|
3024
|
+
}),
|
|
3025
|
+
{
|
|
3026
|
+
maxAttempts: 2,
|
|
3027
|
+
initialDelayMs: 0,
|
|
3028
|
+
retryableErrors: isNetworkError,
|
|
3029
|
+
context: {
|
|
3030
|
+
operation: "releases.create",
|
|
3031
|
+
releaseTitle,
|
|
3032
|
+
...logContext
|
|
3033
|
+
}
|
|
2197
3034
|
}
|
|
2198
|
-
|
|
3035
|
+
)).releaseId, createdNewRelease = !0, devLog.info("Content Release created", {
|
|
2199
3036
|
...logContext,
|
|
2200
3037
|
releaseId,
|
|
2201
3038
|
releaseTitle
|
|
@@ -2217,8 +3054,28 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2217
3054
|
sourceDocument
|
|
2218
3055
|
});
|
|
2219
3056
|
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages, sourceDocument._id);
|
|
2220
|
-
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(
|
|
2221
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
|
+
}
|
|
2222
3079
|
try {
|
|
2223
3080
|
await translationSemaphore.withPermit(async () => {
|
|
2224
3081
|
const document = await client.withConfig({
|
|
@@ -2240,7 +3097,8 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2240
3097
|
excludeFields,
|
|
2241
3098
|
replaceExisting: releaseConfig.replaceExisting,
|
|
2242
3099
|
// Pass main source document ID for active translations tracking
|
|
2243
|
-
mainSourceDocumentId: sourceDocument._id
|
|
3100
|
+
mainSourceDocumentId: sourceDocument._id,
|
|
3101
|
+
telemetryConfig
|
|
2244
3102
|
});
|
|
2245
3103
|
versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
|
|
2246
3104
|
}
|
|
@@ -2257,13 +3115,28 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2257
3115
|
);
|
|
2258
3116
|
}
|
|
2259
3117
|
})
|
|
2260
|
-
), 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;
|
|
2261
3119
|
devLog.info("Translation tasks completed", {
|
|
2262
3120
|
...logContext,
|
|
2263
3121
|
total: results.length,
|
|
2264
3122
|
succeeded,
|
|
2265
3123
|
failed,
|
|
3124
|
+
skipped: skippedCount,
|
|
2266
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
|
+
}
|
|
2267
3140
|
}), dispatch({
|
|
2268
3141
|
type: "SET_OVERALL_STATUS",
|
|
2269
3142
|
payload: "linking_references"
|
|
@@ -2276,7 +3149,15 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2276
3149
|
languageFieldName
|
|
2277
3150
|
});
|
|
2278
3151
|
})
|
|
2279
|
-
)
|
|
3152
|
+
), releaseConfig.mode === "existing" && releaseId && await updateBackwardReferences({
|
|
3153
|
+
client,
|
|
3154
|
+
releaseId,
|
|
3155
|
+
sourceDocumentIds,
|
|
3156
|
+
targetLanguages,
|
|
3157
|
+
sourceLanguage,
|
|
3158
|
+
languageFieldName,
|
|
3159
|
+
excludeDocumentIds: versionedTargetDocumentIds
|
|
3160
|
+
});
|
|
2280
3161
|
const hasAnyFailures = failed > 0;
|
|
2281
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", {
|
|
2282
3163
|
...logContext,
|
|
@@ -2303,8 +3184,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2303
3184
|
}
|
|
2304
3185
|
throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id), error;
|
|
2305
3186
|
}
|
|
2306
|
-
}, ReleaseConflictWarning = ({
|
|
2307
|
-
|
|
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]);
|
|
2308
3195
|
return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2309
3196
|
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
2310
3197
|
/* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
|
|
@@ -2328,13 +3215,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2328
3215
|
" \u2014 Go back and select a different release, or create a new one."
|
|
2329
3216
|
] }),
|
|
2330
3217
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2331
|
-
/* @__PURE__ */ jsx("strong", { children: "Replace" }),
|
|
3218
|
+
/* @__PURE__ */ jsx("strong", { children: "Replace All" }),
|
|
2332
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)"
|
|
2333
3229
|
] })
|
|
2334
3230
|
] }),
|
|
2335
|
-
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "
|
|
3231
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "space-between", children: [
|
|
2336
3232
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
|
|
2337
|
-
/* @__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
|
+
] })
|
|
2338
3246
|
] })
|
|
2339
3247
|
] }) });
|
|
2340
3248
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
@@ -2344,15 +3252,16 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2344
3252
|
overallStatus,
|
|
2345
3253
|
dispatch,
|
|
2346
3254
|
references,
|
|
3255
|
+
selectedReferenceIds,
|
|
2347
3256
|
selectedTargetLanguages,
|
|
2348
3257
|
translationMode,
|
|
2349
3258
|
releaseConfig
|
|
2350
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1);
|
|
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]);
|
|
2351
3260
|
useEffect(() => {
|
|
2352
3261
|
releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
|
|
2353
3262
|
}, [releaseConfig.mode, conflictContext]);
|
|
2354
3263
|
const startTranslation = useCallback(
|
|
2355
|
-
(replaceExisting, overrideReleaseId) => {
|
|
3264
|
+
(replaceExisting, overrideReleaseId, skipConflicts) => {
|
|
2356
3265
|
setConflictContext(null), translateDocumentSet({
|
|
2357
3266
|
sourceDocument: document,
|
|
2358
3267
|
targetLanguages: selectedTargetLanguages,
|
|
@@ -2360,7 +3269,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2360
3269
|
dispatch,
|
|
2361
3270
|
languageFieldName: config.languageFieldName,
|
|
2362
3271
|
schemaId: config.schemaId,
|
|
2363
|
-
references,
|
|
3272
|
+
references: filteredReferences,
|
|
2364
3273
|
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
2365
3274
|
maxReferenceDepth: config.maxReferenceDepth,
|
|
2366
3275
|
translationMode,
|
|
@@ -2369,6 +3278,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2369
3278
|
releaseConfig: {
|
|
2370
3279
|
...releaseConfig,
|
|
2371
3280
|
replaceExisting,
|
|
3281
|
+
skipConflicts,
|
|
2372
3282
|
// Use the override release ID if provided (from conflict context),
|
|
2373
3283
|
// otherwise use the current releaseConfig value
|
|
2374
3284
|
existingReleaseId: overrideReleaseId ?? releaseConfig.existingReleaseId
|
|
@@ -2386,7 +3296,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2386
3296
|
config.maxReferenceDepth,
|
|
2387
3297
|
config.openai,
|
|
2388
3298
|
config.excludeFields,
|
|
2389
|
-
|
|
3299
|
+
filteredReferences,
|
|
2390
3300
|
translationMode,
|
|
2391
3301
|
releaseConfig
|
|
2392
3302
|
]
|
|
@@ -2404,7 +3314,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2404
3314
|
sourceDocument: document,
|
|
2405
3315
|
targetLanguages: selectedTargetLanguages,
|
|
2406
3316
|
languageFieldName: config.languageFieldName,
|
|
2407
|
-
references
|
|
3317
|
+
references: filteredReferences
|
|
2408
3318
|
});
|
|
2409
3319
|
if (setIsCheckingConflicts(!1), foundConflicts.length > 0) {
|
|
2410
3320
|
setConflictContext({
|
|
@@ -2415,10 +3325,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2415
3325
|
}
|
|
2416
3326
|
}
|
|
2417
3327
|
startTranslation(!1);
|
|
2418
|
-
}, [
|
|
3328
|
+
}, [
|
|
3329
|
+
releaseConfig,
|
|
3330
|
+
client,
|
|
3331
|
+
document,
|
|
3332
|
+
selectedTargetLanguages,
|
|
3333
|
+
config.languageFieldName,
|
|
3334
|
+
startTranslation,
|
|
3335
|
+
filteredReferences
|
|
3336
|
+
]), handleCancelConflict = useCallback(() => {
|
|
2419
3337
|
setConflictContext(null);
|
|
2420
3338
|
}, []), handleReplaceConflict = useCallback(() => {
|
|
2421
3339
|
conflictContext && startTranslation(!0, conflictContext.releaseId);
|
|
3340
|
+
}, [startTranslation, conflictContext]), handleSkipConflicts = useCallback(() => {
|
|
3341
|
+
conflictContext && startTranslation(!1, conflictContext.releaseId, conflictContext.conflicts);
|
|
2422
3342
|
}, [startTranslation, conflictContext]), handleSelectAll = useCallback(() => {
|
|
2423
3343
|
dispatch({
|
|
2424
3344
|
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
@@ -2434,8 +3354,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2434
3354
|
ReleaseConflictWarning,
|
|
2435
3355
|
{
|
|
2436
3356
|
conflicts: conflictContext.conflicts,
|
|
3357
|
+
totalTranslations,
|
|
2437
3358
|
onCancel: handleCancelConflict,
|
|
2438
|
-
onReplace: handleReplaceConflict
|
|
3359
|
+
onReplace: handleReplaceConflict,
|
|
3360
|
+
onSkipConflicts: handleSkipConflicts
|
|
2439
3361
|
}
|
|
2440
3362
|
) }) : /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
|
|
2441
3363
|
/* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
|
|
@@ -2517,8 +3439,183 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2517
3439
|
documentId: document._id
|
|
2518
3440
|
});
|
|
2519
3441
|
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)) }) });
|
|
3442
|
+
}, reconcileReleaseReferences = async ({
|
|
3443
|
+
client,
|
|
3444
|
+
releaseId,
|
|
3445
|
+
sourceLanguage,
|
|
3446
|
+
languageFieldName,
|
|
3447
|
+
onProgress
|
|
3448
|
+
}) => {
|
|
3449
|
+
devLog.info("Starting release reference reconciliation", {
|
|
3450
|
+
releaseId,
|
|
3451
|
+
sourceLanguage,
|
|
3452
|
+
component: "reconcileReleaseReferences"
|
|
3453
|
+
});
|
|
3454
|
+
try {
|
|
3455
|
+
const docsInRelease = await client.fetch(
|
|
3456
|
+
`*[_id match "versions." + $releaseId + ".*"] {
|
|
3457
|
+
_id,
|
|
3458
|
+
"language": @[$languageFieldName]
|
|
3459
|
+
}`,
|
|
3460
|
+
{ releaseId, languageFieldName }
|
|
3461
|
+
);
|
|
3462
|
+
if (docsInRelease.length === 0)
|
|
3463
|
+
return devLog.info("No documents found in release", {
|
|
3464
|
+
releaseId,
|
|
3465
|
+
component: "reconcileReleaseReferences"
|
|
3466
|
+
}), { total: 0, updated: 0, errors: 0 };
|
|
3467
|
+
devLog.info("Found documents in release", {
|
|
3468
|
+
releaseId,
|
|
3469
|
+
count: docsInRelease.length,
|
|
3470
|
+
component: "reconcileReleaseReferences"
|
|
3471
|
+
});
|
|
3472
|
+
let updated = 0, errors = 0;
|
|
3473
|
+
for (let i = 0; i < docsInRelease.length; i++) {
|
|
3474
|
+
const doc = docsInRelease[i];
|
|
3475
|
+
onProgress && onProgress(i + 1, docsInRelease.length);
|
|
3476
|
+
try {
|
|
3477
|
+
if (!doc.language) {
|
|
3478
|
+
devLog.warn("Document missing language field, skipping", {
|
|
3479
|
+
documentId: doc._id,
|
|
3480
|
+
component: "reconcileReleaseReferences"
|
|
3481
|
+
});
|
|
3482
|
+
continue;
|
|
3483
|
+
}
|
|
3484
|
+
await localizeReferences({
|
|
3485
|
+
client,
|
|
3486
|
+
documentId: doc._id,
|
|
3487
|
+
sourceLanguage,
|
|
3488
|
+
languageFieldName
|
|
3489
|
+
}), updated++;
|
|
3490
|
+
} catch (error) {
|
|
3491
|
+
errors++, devLog.error(
|
|
3492
|
+
"Failed to reconcile references for document",
|
|
3493
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
3494
|
+
{
|
|
3495
|
+
documentId: doc._id,
|
|
3496
|
+
component: "reconcileReleaseReferences"
|
|
3497
|
+
}
|
|
3498
|
+
);
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
const result = {
|
|
3502
|
+
total: docsInRelease.length,
|
|
3503
|
+
updated,
|
|
3504
|
+
errors
|
|
3505
|
+
};
|
|
3506
|
+
return devLog.info("Release reference reconciliation complete", {
|
|
3507
|
+
releaseId,
|
|
3508
|
+
...result,
|
|
3509
|
+
component: "reconcileReleaseReferences"
|
|
3510
|
+
}), result;
|
|
3511
|
+
} catch (error) {
|
|
3512
|
+
throw devLog.error("Failed to reconcile release references", error instanceof Error ? error : new Error(String(error)), {
|
|
3513
|
+
releaseId,
|
|
3514
|
+
component: "reconcileReleaseReferences"
|
|
3515
|
+
}), error;
|
|
3516
|
+
}
|
|
3517
|
+
}, ReconcileReferencesButton = ({ sourceLanguage }) => {
|
|
3518
|
+
const { releaseConfig, overallStatus, config } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [documentCount, setDocumentCount] = useState(null), [isLoadingCount, setIsLoadingCount] = useState(!1), [reconcilingStatus, setReconcilingStatus] = useState("idle"), [reconciliationProgress, setReconciliationProgress] = useState(
|
|
3519
|
+
void 0
|
|
3520
|
+
), releaseId = releaseConfig.existingReleaseId, languageFieldName = config?.languageFieldName ?? "language";
|
|
3521
|
+
useEffect(() => {
|
|
3522
|
+
if (!releaseId || releaseConfig.mode !== "existing") {
|
|
3523
|
+
setDocumentCount(null);
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3526
|
+
(async () => {
|
|
3527
|
+
setIsLoadingCount(!0);
|
|
3528
|
+
try {
|
|
3529
|
+
const count = await client.fetch('count(*[_id match "versions." + $releaseId + ".*"])', { releaseId });
|
|
3530
|
+
setDocumentCount(count), devLog.debug("Fetched release document count", {
|
|
3531
|
+
releaseId,
|
|
3532
|
+
count,
|
|
3533
|
+
component: "ReconcileReferencesButton"
|
|
3534
|
+
});
|
|
3535
|
+
} catch (error) {
|
|
3536
|
+
devLog.error(
|
|
3537
|
+
"Failed to fetch release document count",
|
|
3538
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
3539
|
+
{
|
|
3540
|
+
releaseId,
|
|
3541
|
+
component: "ReconcileReferencesButton"
|
|
3542
|
+
}
|
|
3543
|
+
), setDocumentCount(null);
|
|
3544
|
+
} finally {
|
|
3545
|
+
setIsLoadingCount(!1);
|
|
3546
|
+
}
|
|
3547
|
+
})();
|
|
3548
|
+
}, [releaseId, releaseConfig.mode, client]);
|
|
3549
|
+
const handleReconcile = useCallback(async () => {
|
|
3550
|
+
if (releaseId) {
|
|
3551
|
+
devLog.info("Starting reference reconciliation", {
|
|
3552
|
+
releaseId,
|
|
3553
|
+
sourceLanguage,
|
|
3554
|
+
component: "ReconcileReferencesButton"
|
|
3555
|
+
}), setReconcilingStatus("reconciling");
|
|
3556
|
+
try {
|
|
3557
|
+
const result = await reconcileReleaseReferences({
|
|
3558
|
+
client,
|
|
3559
|
+
releaseId,
|
|
3560
|
+
sourceLanguage,
|
|
3561
|
+
languageFieldName,
|
|
3562
|
+
onProgress: (current, total) => {
|
|
3563
|
+
setReconciliationProgress({ current, total });
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
devLog.info("Reference reconciliation completed", {
|
|
3567
|
+
releaseId,
|
|
3568
|
+
...result,
|
|
3569
|
+
component: "ReconcileReferencesButton"
|
|
3570
|
+
}), setReconcilingStatus("completed"), setTimeout(() => {
|
|
3571
|
+
setReconcilingStatus("idle"), setReconciliationProgress(void 0);
|
|
3572
|
+
}, 2e3);
|
|
3573
|
+
} catch (error) {
|
|
3574
|
+
devLog.error("Reference reconciliation failed", error instanceof Error ? error : new Error(String(error)), {
|
|
3575
|
+
releaseId,
|
|
3576
|
+
component: "ReconcileReferencesButton"
|
|
3577
|
+
}), setReconcilingStatus("error"), setReconciliationProgress(void 0);
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
}, [releaseId, sourceLanguage, languageFieldName, client]);
|
|
3581
|
+
if (releaseConfig.mode !== "existing" || !releaseId)
|
|
3582
|
+
return null;
|
|
3583
|
+
const isTranslating = overallStatus === "translating" || overallStatus === "linking_references", isReconciling = reconcilingStatus === "reconciling";
|
|
3584
|
+
return /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, tone: reconcilingStatus === "error" ? "critical" : "default", children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3585
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", gap: 3, children: [
|
|
3586
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3587
|
+
/* @__PURE__ */ jsx(Text, { size: 1, weight: "medium", children: "Reconcile References" }),
|
|
3588
|
+
/* @__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: [
|
|
3589
|
+
documentCount,
|
|
3590
|
+
" document",
|
|
3591
|
+
documentCount !== 1 ? "s" : "",
|
|
3592
|
+
" in release"
|
|
3593
|
+
] }) })
|
|
3594
|
+
] }),
|
|
3595
|
+
/* @__PURE__ */ jsx(
|
|
3596
|
+
Button,
|
|
3597
|
+
{
|
|
3598
|
+
icon: isReconciling ? Spinner : SyncIcon,
|
|
3599
|
+
text: reconcilingStatus === "completed" ? "Done!" : reconcilingStatus === "error" ? "Retry" : isReconciling ? "Reconciling..." : "Reconcile",
|
|
3600
|
+
tone: reconcilingStatus === "completed" ? "positive" : reconcilingStatus === "error" ? "critical" : "default",
|
|
3601
|
+
mode: "ghost",
|
|
3602
|
+
onClick: handleReconcile,
|
|
3603
|
+
disabled: (isTranslating || isReconciling || isLoadingCount || documentCount === 0) && reconcilingStatus !== "error"
|
|
3604
|
+
}
|
|
3605
|
+
)
|
|
3606
|
+
] }),
|
|
3607
|
+
isReconciling && reconciliationProgress && /* @__PURE__ */ jsx(Flex, { align: "center", gap: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3608
|
+
"Processing ",
|
|
3609
|
+
reconciliationProgress.current,
|
|
3610
|
+
" of ",
|
|
3611
|
+
reconciliationProgress.total,
|
|
3612
|
+
"..."
|
|
3613
|
+
] }) }),
|
|
3614
|
+
reconcilingStatus === "error" && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Reconciliation failed. Click Retry to try again." }),
|
|
3615
|
+
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." })
|
|
3616
|
+
] }) });
|
|
2520
3617
|
}, ReleaseSelectionPanel = () => {
|
|
2521
|
-
const { dispatch, releaseConfig } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [releases, setReleases] = useState([]), [isLoading, setIsLoading] = useState(!1), [error, setError] = useState(null), [searchQuery, setSearchQuery] = useState("");
|
|
3618
|
+
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);
|
|
2522
3619
|
useEffect(() => {
|
|
2523
3620
|
(async () => {
|
|
2524
3621
|
setIsLoading(!0), setError(null);
|
|
@@ -2631,12 +3728,33 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2631
3728
|
radius: 2,
|
|
2632
3729
|
tone: isSelected ? "primary" : "default",
|
|
2633
3730
|
style: { cursor: "pointer" },
|
|
2634
|
-
onClick: () =>
|
|
3731
|
+
onClick: () => {
|
|
3732
|
+
handleSelectRelease(releaseName), isSelected || setShowReconcileUI(!1);
|
|
3733
|
+
},
|
|
2635
3734
|
border: !0,
|
|
2636
3735
|
muted: isSelected,
|
|
2637
3736
|
children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
2638
|
-
/* @__PURE__ */
|
|
2639
|
-
|
|
3737
|
+
/* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", children: [
|
|
3738
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, children: [
|
|
3739
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: release.metadata.title }),
|
|
3740
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: formatDate(release._createdAt) })
|
|
3741
|
+
] }),
|
|
3742
|
+
isSelected && sourceLanguage && /* @__PURE__ */ jsx(
|
|
3743
|
+
Button,
|
|
3744
|
+
{
|
|
3745
|
+
icon: SyncIcon,
|
|
3746
|
+
text: showReconcileUI ? "Hide" : "Reconcile References",
|
|
3747
|
+
mode: "bleed",
|
|
3748
|
+
tone: "primary",
|
|
3749
|
+
fontSize: 0,
|
|
3750
|
+
padding: 2,
|
|
3751
|
+
onClick: (e) => {
|
|
3752
|
+
e.stopPropagation(), setShowReconcileUI(!showReconcileUI);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
)
|
|
3756
|
+
] }),
|
|
3757
|
+
isSelected && sourceLanguage && showReconcileUI && /* @__PURE__ */ jsx(ReconcileReferencesButton, { sourceLanguage })
|
|
2640
3758
|
] })
|
|
2641
3759
|
},
|
|
2642
3760
|
release._id
|
|
@@ -2789,8 +3907,90 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2789
3907
|
});
|
|
2790
3908
|
}
|
|
2791
3909
|
return result;
|
|
3910
|
+
}, ReferenceSelectionPanel = () => {
|
|
3911
|
+
const { references, selectedReferenceIds, dispatch } = useTranslationContext(), [typeFilter, setTypeFilter] = useState("all"), documentTypes = useMemo(() => {
|
|
3912
|
+
const types = new Set(references.map((ref) => ref._type));
|
|
3913
|
+
return Array.from(types).sort();
|
|
3914
|
+
}, [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) => {
|
|
3915
|
+
const newSelection = new Set(selectedReferenceIds);
|
|
3916
|
+
newSelection.has(referenceId) ? newSelection.delete(referenceId) : newSelection.add(referenceId), dispatch({
|
|
3917
|
+
type: "SET_SELECTED_REFERENCE_IDS",
|
|
3918
|
+
payload: newSelection
|
|
3919
|
+
});
|
|
3920
|
+
}, handleSelectAll = () => {
|
|
3921
|
+
const newSelection = new Set(selectedReferenceIds);
|
|
3922
|
+
filteredReferences.forEach((ref) => {
|
|
3923
|
+
newSelection.add(ref._id);
|
|
3924
|
+
}), dispatch({
|
|
3925
|
+
type: "SET_SELECTED_REFERENCE_IDS",
|
|
3926
|
+
payload: newSelection
|
|
3927
|
+
});
|
|
3928
|
+
}, handleSelectNone = () => {
|
|
3929
|
+
const newSelection = new Set(selectedReferenceIds);
|
|
3930
|
+
filteredReferences.forEach((ref) => {
|
|
3931
|
+
newSelection.delete(ref._id);
|
|
3932
|
+
}), dispatch({
|
|
3933
|
+
type: "SET_SELECTED_REFERENCE_IDS",
|
|
3934
|
+
payload: newSelection
|
|
3935
|
+
});
|
|
3936
|
+
};
|
|
3937
|
+
return references.length === 0 ? null : /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3938
|
+
/* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", gap: 2, children: [
|
|
3939
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
3940
|
+
/* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "Select References" }),
|
|
3941
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3942
|
+
"(",
|
|
3943
|
+
selectedCount,
|
|
3944
|
+
" of ",
|
|
3945
|
+
filteredReferences.length,
|
|
3946
|
+
" selected)"
|
|
3947
|
+
] })
|
|
3948
|
+
] }),
|
|
3949
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
3950
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 1, onClick: handleSelectAll, text: "Select All" }),
|
|
3951
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 1, onClick: handleSelectNone, text: "Select None" })
|
|
3952
|
+
] })
|
|
3953
|
+
] }),
|
|
3954
|
+
documentTypes.length > 1 && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
3955
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Filter by type:" }),
|
|
3956
|
+
/* @__PURE__ */ jsxs(
|
|
3957
|
+
Select,
|
|
3958
|
+
{
|
|
3959
|
+
value: typeFilter,
|
|
3960
|
+
onChange: (event) => setTypeFilter(event.target.value),
|
|
3961
|
+
style: { minWidth: "150px" },
|
|
3962
|
+
children: [
|
|
3963
|
+
/* @__PURE__ */ jsxs("option", { value: "all", children: [
|
|
3964
|
+
"All types (",
|
|
3965
|
+
references.length,
|
|
3966
|
+
")"
|
|
3967
|
+
] }),
|
|
3968
|
+
documentTypes.map((type) => {
|
|
3969
|
+
const count = references.filter((ref) => ref._type === type).length;
|
|
3970
|
+
return /* @__PURE__ */ jsxs("option", { value: type, children: [
|
|
3971
|
+
type,
|
|
3972
|
+
" (",
|
|
3973
|
+
count,
|
|
3974
|
+
")"
|
|
3975
|
+
] }, type);
|
|
3976
|
+
})
|
|
3977
|
+
]
|
|
3978
|
+
}
|
|
3979
|
+
)
|
|
3980
|
+
] }),
|
|
3981
|
+
/* @__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) => {
|
|
3982
|
+
const isSelected = selectedReferenceIds.has(reference._id), displayTitle = reference.title || reference._id;
|
|
3983
|
+
return /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
|
|
3984
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: isSelected, onChange: () => handleToggleReference(reference._id) }),
|
|
3985
|
+
/* @__PURE__ */ jsxs(Flex, { flex: 1, align: "center", gap: 2, children: [
|
|
3986
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: displayTitle }),
|
|
3987
|
+
/* @__PURE__ */ jsx(Badge, { tone: "primary", fontSize: 0, children: reference._type })
|
|
3988
|
+
] })
|
|
3989
|
+
] }) }, reference._id);
|
|
3990
|
+
}) }) })
|
|
3991
|
+
] }) });
|
|
2792
3992
|
}, ReferenceConfigPanel = () => {
|
|
2793
|
-
const { dispatch, document, shouldTranslateReferences, maxReferenceDepth, config, references } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [isLoadingReferences, setIsLoadingReferences] = useState(!1), maxDepth = config.maxReferenceDepth || 3;
|
|
3993
|
+
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;
|
|
2794
3994
|
useEffect(() => {
|
|
2795
3995
|
(async () => {
|
|
2796
3996
|
if (setIsLoadingReferences(!0), !shouldTranslateReferences) {
|
|
@@ -2860,7 +4060,24 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2860
4060
|
)
|
|
2861
4061
|
] }) : null
|
|
2862
4062
|
] }),
|
|
2863
|
-
shouldTranslateReferences ? /* @__PURE__ */
|
|
4063
|
+
shouldTranslateReferences ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4064
|
+
/* @__PURE__ */ jsxs(Flex, { padding: 2, justify: "space-between", align: "center", children: [
|
|
4065
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: isLoadingReferences ? "Loading references..." : references.length === 0 ? "No translatable references found." : (() => {
|
|
4066
|
+
const selectedCount = references.filter((ref) => selectedReferenceIds.has(ref._id)).length, totalCount = references.length;
|
|
4067
|
+
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.`;
|
|
4068
|
+
})() }),
|
|
4069
|
+
!isLoadingReferences && references.length > 0 && /* @__PURE__ */ jsx(
|
|
4070
|
+
Button,
|
|
4071
|
+
{
|
|
4072
|
+
mode: "ghost",
|
|
4073
|
+
size: 1,
|
|
4074
|
+
text: showSelectionPanel ? "Hide selection" : "Select specific references...",
|
|
4075
|
+
onClick: () => setShowSelectionPanel(!showSelectionPanel)
|
|
4076
|
+
}
|
|
4077
|
+
)
|
|
4078
|
+
] }),
|
|
4079
|
+
!isLoadingReferences && references.length > 0 && showSelectionPanel && /* @__PURE__ */ jsx(ReferenceSelectionPanel, {})
|
|
4080
|
+
] }) : null
|
|
2864
4081
|
] });
|
|
2865
4082
|
}, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2866
4083
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
@@ -3436,10 +4653,14 @@ function serializeVTTCaptionText(captions) {
|
|
|
3436
4653
|
firstCaption: captions.length > 0 ? captions[0] : null
|
|
3437
4654
|
}), !Array.isArray(captions) || captions.length === 0)
|
|
3438
4655
|
return "";
|
|
3439
|
-
const result = captions.map((caption, index) =>
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
4656
|
+
const result = captions.map((caption, index) => {
|
|
4657
|
+
devLog.debug(`Processing caption ${index}:`, {
|
|
4658
|
+
text: caption.text,
|
|
4659
|
+
hasText: typeof caption.text == "string"
|
|
4660
|
+
});
|
|
4661
|
+
const escapedText = (caption.text || "").replace(/\n/g, "\\n");
|
|
4662
|
+
return `[${index}] ${escapedText}`;
|
|
4663
|
+
}).join(`
|
|
3443
4664
|
`);
|
|
3444
4665
|
return devLog.debug("serializeVTTCaptionText - output:", {
|
|
3445
4666
|
resultLength: result.length,
|
|
@@ -3456,29 +4677,46 @@ function deserializeVTTCaptionText(translationText, originalMetadata) {
|
|
|
3456
4677
|
return [];
|
|
3457
4678
|
const result = [], lines = translationText.split(`
|
|
3458
4679
|
`), lineRegex = /^\[(\d+)\]\s*(.*)$/;
|
|
4680
|
+
let currentIndex = null, currentTextParts = [];
|
|
3459
4681
|
for (const line of lines) {
|
|
3460
4682
|
const match = line.match(lineRegex);
|
|
3461
|
-
if (
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
4683
|
+
if (match) {
|
|
4684
|
+
if (currentIndex !== null) {
|
|
4685
|
+
const text = currentTextParts.join(`
|
|
4686
|
+
`).replace(/\\n/g, `
|
|
4687
|
+
`), metadata = originalMetadata.find((m) => m._originalIndex === currentIndex);
|
|
4688
|
+
if (metadata) {
|
|
4689
|
+
const { _originalIndex, ...metadataWithoutIndex } = metadata;
|
|
4690
|
+
result.push({
|
|
4691
|
+
...metadataWithoutIndex,
|
|
4692
|
+
text
|
|
4693
|
+
});
|
|
4694
|
+
} else
|
|
4695
|
+
devLog.warn("Missing metadata for caption", { index: currentIndex }), result.push({
|
|
4696
|
+
_type: "vtt.cue",
|
|
4697
|
+
_key: `caption-${currentIndex}`,
|
|
4698
|
+
text,
|
|
4699
|
+
startTime: 0,
|
|
4700
|
+
endTime: 0
|
|
4701
|
+
});
|
|
4702
|
+
}
|
|
4703
|
+
currentIndex = Number.parseInt(match[1], 10), currentTextParts = [match[2]];
|
|
4704
|
+
} else currentIndex !== null ? currentTextParts.push(line) : devLog.warn("Failed to parse caption line", { line });
|
|
4705
|
+
}
|
|
4706
|
+
if (currentIndex !== null) {
|
|
4707
|
+
const text = currentTextParts.join(`
|
|
4708
|
+
`).replace(/\\n/g, `
|
|
4709
|
+
`), metadata = originalMetadata.find((m) => m._originalIndex === currentIndex);
|
|
3471
4710
|
if (metadata) {
|
|
3472
|
-
devLog.debug(`Found matching metadata for index ${index}`);
|
|
3473
4711
|
const { _originalIndex, ...metadataWithoutIndex } = metadata;
|
|
3474
4712
|
result.push({
|
|
3475
4713
|
...metadataWithoutIndex,
|
|
3476
4714
|
text
|
|
3477
4715
|
});
|
|
3478
4716
|
} else
|
|
3479
|
-
devLog.warn("Missing metadata for caption", { index }), result.push({
|
|
4717
|
+
devLog.warn("Missing metadata for caption", { index: currentIndex }), result.push({
|
|
3480
4718
|
_type: "vtt.cue",
|
|
3481
|
-
_key: `caption-${
|
|
4719
|
+
_key: `caption-${currentIndex}`,
|
|
3482
4720
|
text,
|
|
3483
4721
|
startTime: 0,
|
|
3484
4722
|
endTime: 0
|
|
@@ -3489,6 +4727,293 @@ function deserializeVTTCaptionText(translationText, originalMetadata) {
|
|
|
3489
4727
|
resultExample: result.length > 0 ? result[0] : null
|
|
3490
4728
|
}), result;
|
|
3491
4729
|
}
|
|
4730
|
+
const LOCALE_TO_ISO = {
|
|
4731
|
+
// English variants
|
|
4732
|
+
"en-us": "eng",
|
|
4733
|
+
"en-US": "eng",
|
|
4734
|
+
"en-gb": "eng",
|
|
4735
|
+
"en-GB": "eng",
|
|
4736
|
+
"en-ca": "eng",
|
|
4737
|
+
"en-CA": "eng",
|
|
4738
|
+
"en-au": "eng",
|
|
4739
|
+
"en-AU": "eng",
|
|
4740
|
+
// Spanish variants
|
|
4741
|
+
es: "spa",
|
|
4742
|
+
"es-es": "spa",
|
|
4743
|
+
"es-ES": "spa",
|
|
4744
|
+
"es-mx": "spa",
|
|
4745
|
+
"es-MX": "spa",
|
|
4746
|
+
// French variants
|
|
4747
|
+
fr: "fra",
|
|
4748
|
+
"fr-fr": "fra",
|
|
4749
|
+
"fr-FR": "fra",
|
|
4750
|
+
"fr-ca": "fra",
|
|
4751
|
+
"fr-CA": "fra",
|
|
4752
|
+
// German variants
|
|
4753
|
+
de: "ger",
|
|
4754
|
+
"de-de": "ger",
|
|
4755
|
+
"de-DE": "ger",
|
|
4756
|
+
"de-at": "ger",
|
|
4757
|
+
"de-AT": "ger",
|
|
4758
|
+
// Italian variants
|
|
4759
|
+
it: "ita",
|
|
4760
|
+
"it-it": "ita",
|
|
4761
|
+
"it-IT": "ita",
|
|
4762
|
+
// Portuguese variants
|
|
4763
|
+
pt: "por",
|
|
4764
|
+
"pt-br": "por",
|
|
4765
|
+
"pt-BR": "por",
|
|
4766
|
+
"pt-pt": "por",
|
|
4767
|
+
"pt-PT": "por",
|
|
4768
|
+
// Russian variants
|
|
4769
|
+
ru: "rus",
|
|
4770
|
+
"ru-ru": "rus",
|
|
4771
|
+
"ru-RU": "rus",
|
|
4772
|
+
// Japanese variants
|
|
4773
|
+
ja: "jpn",
|
|
4774
|
+
"ja-jp": "jpn",
|
|
4775
|
+
"ja-JP": "jpn",
|
|
4776
|
+
// Korean variants
|
|
4777
|
+
ko: "kor",
|
|
4778
|
+
"ko-kr": "kor",
|
|
4779
|
+
"ko-KR": "kor",
|
|
4780
|
+
// Chinese variants
|
|
4781
|
+
zh: "chi",
|
|
4782
|
+
"zh-cn": "chi",
|
|
4783
|
+
"zh-CN": "chi",
|
|
4784
|
+
"zh-tw": "chi",
|
|
4785
|
+
"zh-TW": "chi",
|
|
4786
|
+
"zh-hk": "chi",
|
|
4787
|
+
"zh-HK": "chi"
|
|
4788
|
+
}, normalizeToIsoLanguageCode = (code) => {
|
|
4789
|
+
const normalizedCode = code.toLowerCase();
|
|
4790
|
+
return LOCALE_TO_ISO[normalizedCode];
|
|
4791
|
+
}, wistiaCaptionTransform = {
|
|
4792
|
+
matcher: {
|
|
4793
|
+
fieldName: "wistiaCaptions"
|
|
4794
|
+
},
|
|
4795
|
+
toTranslatable: (wistiaCaptions, context) => {
|
|
4796
|
+
if (devLog.info("Wistia captions toTranslatable - input:", {
|
|
4797
|
+
fieldPath: context.fieldPath,
|
|
4798
|
+
hasCaptionContent: !!wistiaCaptions?.captionContent,
|
|
4799
|
+
captionContentLength: wistiaCaptions?.captionContent?.length ?? 0,
|
|
4800
|
+
isoLanguage: wistiaCaptions?.isoLanguage
|
|
4801
|
+
}), !wistiaCaptions || !wistiaCaptions.captionContent)
|
|
4802
|
+
return devLog.debug("Wistia caption field is missing or has no content, returning empty transform", {
|
|
4803
|
+
fieldPath: context.fieldPath,
|
|
4804
|
+
value: wistiaCaptions
|
|
4805
|
+
}), {
|
|
4806
|
+
_type: "translatable.wistia.captions",
|
|
4807
|
+
translationText: "",
|
|
4808
|
+
_originalMetadata: {
|
|
4809
|
+
captionFileName: wistiaCaptions?.captionFileName,
|
|
4810
|
+
isoLanguage: wistiaCaptions?.isoLanguage,
|
|
4811
|
+
captionUploadStatus: wistiaCaptions?.captionUploadStatus,
|
|
4812
|
+
captionUploadMessage: wistiaCaptions?.captionUploadMessage,
|
|
4813
|
+
_originalIndex: 0
|
|
4814
|
+
},
|
|
4815
|
+
_srtCues: []
|
|
4816
|
+
};
|
|
4817
|
+
devLog.info("Applying Wistia caption transform", {
|
|
4818
|
+
fieldPath: context.fieldPath,
|
|
4819
|
+
captionContentPreview: wistiaCaptions.captionContent.substring(0, 200)
|
|
4820
|
+
});
|
|
4821
|
+
const srtCues = parseSRTContent(wistiaCaptions.captionContent);
|
|
4822
|
+
devLog.debug("Parsed SRT cues:", {
|
|
4823
|
+
fieldPath: context.fieldPath,
|
|
4824
|
+
cueCount: srtCues.length,
|
|
4825
|
+
firstCue: srtCues.length > 0 ? srtCues[0] : null
|
|
4826
|
+
});
|
|
4827
|
+
const translationText = serializeWistiaCaptionText(srtCues), originalMetadata = {
|
|
4828
|
+
captionFileName: wistiaCaptions.captionFileName,
|
|
4829
|
+
isoLanguage: wistiaCaptions.isoLanguage,
|
|
4830
|
+
captionUploadStatus: wistiaCaptions.captionUploadStatus,
|
|
4831
|
+
captionUploadMessage: wistiaCaptions.captionUploadMessage,
|
|
4832
|
+
_originalIndex: 0
|
|
4833
|
+
}, srtCuesMetadata = srtCues.map((cue) => ({
|
|
4834
|
+
index: cue.index,
|
|
4835
|
+
startTime: cue.startTime,
|
|
4836
|
+
endTime: cue.endTime
|
|
4837
|
+
}));
|
|
4838
|
+
return devLog.info("Wistia captions serialized:", {
|
|
4839
|
+
fieldPath: context.fieldPath,
|
|
4840
|
+
translationTextLength: translationText.length,
|
|
4841
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
|
|
4842
|
+
cueCount: srtCues.length
|
|
4843
|
+
}), {
|
|
4844
|
+
_type: "translatable.wistia.captions",
|
|
4845
|
+
translationText,
|
|
4846
|
+
_originalMetadata: originalMetadata,
|
|
4847
|
+
_srtCues: srtCuesMetadata
|
|
4848
|
+
};
|
|
4849
|
+
},
|
|
4850
|
+
fromTranslatable: (translatedValue, originalValue, context) => {
|
|
4851
|
+
if (devLog.info("Wistia captions fromTranslatable - input:", {
|
|
4852
|
+
fieldPath: context.fieldPath,
|
|
4853
|
+
valueType: typeof translatedValue,
|
|
4854
|
+
isCorrectType: translatedValue._type === "translatable.wistia.captions",
|
|
4855
|
+
hasText: !!translatedValue.translationText,
|
|
4856
|
+
hasMetadata: !!translatedValue._originalMetadata,
|
|
4857
|
+
hasCues: Array.isArray(translatedValue._srtCues)
|
|
4858
|
+
}), !translatedValue || translatedValue._type !== "translatable.wistia.captions" || !translatedValue._originalMetadata || !Array.isArray(translatedValue._srtCues))
|
|
4859
|
+
return devLog.debug("Value is not a translatable Wistia caption object, returning original", {
|
|
4860
|
+
fieldPath: context.fieldPath,
|
|
4861
|
+
valueType: typeof translatedValue,
|
|
4862
|
+
translatedValue
|
|
4863
|
+
}), originalValue || {};
|
|
4864
|
+
devLog.info("Processing translated Wistia captions", {
|
|
4865
|
+
fieldPath: context.fieldPath,
|
|
4866
|
+
cueCount: translatedValue._srtCues.length
|
|
4867
|
+
});
|
|
4868
|
+
const { translationText, _originalMetadata, _srtCues } = translatedValue;
|
|
4869
|
+
devLog.info("Wistia captions translationText:", {
|
|
4870
|
+
fieldPath: context.fieldPath,
|
|
4871
|
+
translationTextLength: translationText.length,
|
|
4872
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
|
|
4873
|
+
});
|
|
4874
|
+
const reconstructedCaptionContent = deserializeWistiaCaptionText(translationText, _srtCues), documentLanguage = getDocumentLanguage(context.document);
|
|
4875
|
+
let updatedIsoLanguage = _originalMetadata.isoLanguage;
|
|
4876
|
+
if (documentLanguage) {
|
|
4877
|
+
const isoCode = normalizeToIsoLanguageCode(documentLanguage);
|
|
4878
|
+
isoCode ? (updatedIsoLanguage = isoCode, devLog.debug("Updated isoLanguage based on document language", {
|
|
4879
|
+
fieldPath: context.fieldPath,
|
|
4880
|
+
documentLanguage,
|
|
4881
|
+
isoCode
|
|
4882
|
+
})) : devLog.debug("Could not convert document language to ISO code, preserving original", {
|
|
4883
|
+
fieldPath: context.fieldPath,
|
|
4884
|
+
documentLanguage,
|
|
4885
|
+
originalIsoLanguage: _originalMetadata.isoLanguage
|
|
4886
|
+
});
|
|
4887
|
+
} else
|
|
4888
|
+
devLog.debug("Document language field not found, preserving original isoLanguage", {
|
|
4889
|
+
fieldPath: context.fieldPath,
|
|
4890
|
+
originalIsoLanguage: _originalMetadata.isoLanguage
|
|
4891
|
+
});
|
|
4892
|
+
const result = {
|
|
4893
|
+
captionContent: reconstructedCaptionContent,
|
|
4894
|
+
captionFileName: _originalMetadata.captionFileName,
|
|
4895
|
+
isoLanguage: updatedIsoLanguage,
|
|
4896
|
+
captionUploadStatus: _originalMetadata.captionUploadStatus,
|
|
4897
|
+
captionUploadMessage: _originalMetadata.captionUploadMessage
|
|
4898
|
+
};
|
|
4899
|
+
return devLog.info("Wistia captions final result:", {
|
|
4900
|
+
fieldPath: context.fieldPath,
|
|
4901
|
+
hasCaptionContent: !!result.captionContent,
|
|
4902
|
+
captionContentLength: result.captionContent?.length ?? 0,
|
|
4903
|
+
isoLanguage: result.isoLanguage
|
|
4904
|
+
}), result;
|
|
4905
|
+
}
|
|
4906
|
+
};
|
|
4907
|
+
function parseSRTContent(content) {
|
|
4908
|
+
if (devLog.debug("parseSRTContent - input:", {
|
|
4909
|
+
contentLength: content.length,
|
|
4910
|
+
contentPreview: content.substring(0, 100) + (content.length > 100 ? "..." : "")
|
|
4911
|
+
}), !content || content.trim() === "")
|
|
4912
|
+
return [];
|
|
4913
|
+
let normalizedContent = content;
|
|
4914
|
+
content.includes("\\n") && !content.includes(`
|
|
4915
|
+
`) && (normalizedContent = content.replace(/\\n/g, `
|
|
4916
|
+
`), devLog.debug("Unescaped newlines in SRT content", {
|
|
4917
|
+
originalLength: content.length,
|
|
4918
|
+
normalizedLength: normalizedContent.length
|
|
4919
|
+
}));
|
|
4920
|
+
const cues = [], cueBlocks = normalizedContent.split(/\n\s*\n/);
|
|
4921
|
+
for (const block of cueBlocks) {
|
|
4922
|
+
const lines = block.trim().split(`
|
|
4923
|
+
`);
|
|
4924
|
+
if (lines.length < 3)
|
|
4925
|
+
continue;
|
|
4926
|
+
const cueNumber = Number.parseInt(lines[0].trim(), 10);
|
|
4927
|
+
if (Number.isNaN(cueNumber)) {
|
|
4928
|
+
devLog.warn("Invalid SRT cue number", { line: lines[0] });
|
|
4929
|
+
continue;
|
|
4930
|
+
}
|
|
4931
|
+
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})$/);
|
|
4932
|
+
if (!timestampMatch) {
|
|
4933
|
+
devLog.warn("Invalid SRT timestamp format", { line: timestampLine });
|
|
4934
|
+
continue;
|
|
4935
|
+
}
|
|
4936
|
+
const startTime = timestampMatch[1].replace(".", ","), endTime = timestampMatch[2].replace(".", ","), text = lines.slice(2).join(`
|
|
4937
|
+
`).trim();
|
|
4938
|
+
text && cues.push({
|
|
4939
|
+
index: cueNumber,
|
|
4940
|
+
startTime,
|
|
4941
|
+
endTime,
|
|
4942
|
+
text
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4945
|
+
return devLog.debug("parseSRTContent - output:", {
|
|
4946
|
+
cueCount: cues.length,
|
|
4947
|
+
firstCue: cues.length > 0 ? cues[0] : null
|
|
4948
|
+
}), cues;
|
|
4949
|
+
}
|
|
4950
|
+
function serializeWistiaCaptionText(cues) {
|
|
4951
|
+
if (devLog.debug("serializeWistiaCaptionText - input:", {
|
|
4952
|
+
cuesCount: cues.length,
|
|
4953
|
+
firstCue: cues.length > 0 ? cues[0] : null
|
|
4954
|
+
}), !Array.isArray(cues) || cues.length === 0)
|
|
4955
|
+
return "";
|
|
4956
|
+
const result = cues.map((cue) => {
|
|
4957
|
+
devLog.debug(`Processing cue ${cue.index}:`, {
|
|
4958
|
+
text: cue.text,
|
|
4959
|
+
hasText: typeof cue.text == "string"
|
|
4960
|
+
});
|
|
4961
|
+
const escapedText = cue.text.replace(/\n/g, "\\n");
|
|
4962
|
+
return `[${cue.index}] ${escapedText}`;
|
|
4963
|
+
}).join(`
|
|
4964
|
+
`);
|
|
4965
|
+
return devLog.debug("serializeWistiaCaptionText - output:", {
|
|
4966
|
+
resultLength: result.length,
|
|
4967
|
+
resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
|
|
4968
|
+
}), result;
|
|
4969
|
+
}
|
|
4970
|
+
function deserializeWistiaCaptionText(translationText, srtCuesMetadata) {
|
|
4971
|
+
if (devLog.debug("deserializeWistiaCaptionText - input:", {
|
|
4972
|
+
translationTextLength: translationText.length,
|
|
4973
|
+
translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
|
|
4974
|
+
metadataCount: srtCuesMetadata.length,
|
|
4975
|
+
metadataExample: srtCuesMetadata.length > 0 ? srtCuesMetadata[0] : null
|
|
4976
|
+
}), !translationText || translationText.trim() === "")
|
|
4977
|
+
return "";
|
|
4978
|
+
const result = [], lines = translationText.split(`
|
|
4979
|
+
`), lineRegex = /^\[(\d+)\]\s*(.*)$/, metadataByIndex = /* @__PURE__ */ new Map();
|
|
4980
|
+
for (const metadata of srtCuesMetadata)
|
|
4981
|
+
metadataByIndex.set(metadata.index, { startTime: metadata.startTime, endTime: metadata.endTime });
|
|
4982
|
+
let currentIndex = null, currentTextParts = [];
|
|
4983
|
+
for (const line of lines) {
|
|
4984
|
+
const match = line.match(lineRegex);
|
|
4985
|
+
if (match) {
|
|
4986
|
+
if (currentIndex !== null) {
|
|
4987
|
+
const text = currentTextParts.join(`
|
|
4988
|
+
`).replace(/\\n/g, `
|
|
4989
|
+
`), metadata = metadataByIndex.get(currentIndex);
|
|
4990
|
+
metadata ? (result.push(`${currentIndex}`), result.push(`${metadata.startTime} --> ${metadata.endTime}`), result.push(text), result.push("")) : devLog.warn("Missing metadata for caption", { index: currentIndex });
|
|
4991
|
+
}
|
|
4992
|
+
currentIndex = Number.parseInt(match[1], 10), currentTextParts = [match[2]];
|
|
4993
|
+
} else currentIndex !== null ? currentTextParts.push(line) : devLog.warn("Failed to parse caption line", { line });
|
|
4994
|
+
}
|
|
4995
|
+
if (currentIndex !== null) {
|
|
4996
|
+
const text = currentTextParts.join(`
|
|
4997
|
+
`).replace(/\\n/g, `
|
|
4998
|
+
`), metadata = metadataByIndex.get(currentIndex);
|
|
4999
|
+
metadata ? (result.push(`${currentIndex}`), result.push(`${metadata.startTime} --> ${metadata.endTime}`), result.push(text), result.push("")) : devLog.warn("Missing metadata for caption", { index: currentIndex });
|
|
5000
|
+
}
|
|
5001
|
+
result.length > 0 && result[result.length - 1] === "" && result.pop();
|
|
5002
|
+
const reconstructed = result.join(`
|
|
5003
|
+
`);
|
|
5004
|
+
return devLog.debug("deserializeWistiaCaptionText - output:", {
|
|
5005
|
+
resultLength: reconstructed.length,
|
|
5006
|
+
resultPreview: reconstructed.substring(0, 200) + (reconstructed.length > 200 ? "..." : "")
|
|
5007
|
+
}), reconstructed;
|
|
5008
|
+
}
|
|
5009
|
+
function getDocumentLanguage(document) {
|
|
5010
|
+
if (!document || typeof document != "object")
|
|
5011
|
+
return;
|
|
5012
|
+
const languageFieldNames = ["language", "lang", "locale"];
|
|
5013
|
+
for (const fieldName of languageFieldNames)
|
|
5014
|
+
if (fieldName in document && typeof document[fieldName] == "string")
|
|
5015
|
+
return document[fieldName];
|
|
5016
|
+
}
|
|
3492
5017
|
const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
|
|
3493
5018
|
globalTransformRegistry.register(transform);
|
|
3494
5019
|
})), {
|
|
@@ -3508,6 +5033,7 @@ export {
|
|
|
3508
5033
|
restoreExcludedFields,
|
|
3509
5034
|
richTextTransform,
|
|
3510
5035
|
translationHistorySchema,
|
|
3511
|
-
vttCaptionTransform
|
|
5036
|
+
vttCaptionTransform,
|
|
5037
|
+
wistiaCaptionTransform
|
|
3512
5038
|
};
|
|
3513
5039
|
//# sourceMappingURL=index.js.map
|