@braze/sanity-plugin-ai-translation 0.2.5 → 0.2.6

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