@braze/sanity-plugin-ai-translation 0.2.26 → 0.2.32

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,16 +1,68 @@
1
- import { useClient, defineType, defineField, getPublishedId, pathToString, definePlugin } from "sanity";
1
+ import { useClient, getPublishedId, pathToString, defineType, defineField, definePlugin } from "sanity";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { ErrorOutlineIcon, CheckmarkCircleIcon, EmptyIcon, AddIcon, TrashIcon, WarningOutlineIcon, InfoOutlineIcon, ArrowLeftIcon, EditIcon, TranslateIcon, CheckmarkIcon, CircleIcon, ArrowRightIcon, PanelRightIcon } from "@sanity/icons";
4
- import { createContext, useContext, useEffect, useState, useReducer, useCallback, useMemo } from "react";
4
+ import { createContext, useReducer, useEffect, useContext, useState, useCallback, useMemo } from "react";
5
+ import defaults from "lodash.defaults";
5
6
  import { Card, Flex, Stack, Text, Spinner, Button, Box, Tooltip, Checkbox, Select, TextInput, TextArea, Heading, Inline, Badge, Grid, Radio, Label, Code } from "@sanity/ui";
6
7
  import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
7
- import defaults from "lodash.defaults";
8
8
  import { toPlainText } from "@portabletext/react";
9
9
  import { uuid } from "@sanity/uuid";
10
10
  import cloneDeep from "lodash.clonedeep";
11
11
  import get from "lodash.get";
12
12
  import set from "lodash.set";
13
- const TranslationContext = createContext({
13
+ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true" || process.env.SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true", devLog = {
14
+ info: (message, context) => {
15
+ isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
16
+ },
17
+ warn: (message, context) => {
18
+ isLoggingEnabled() && console.warn("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
19
+ },
20
+ error: (message, error, context) => {
21
+ isLoggingEnabled() && console.error("[AI Translation]", message, error, context ? JSON.stringify(context, null, 2) : "");
22
+ },
23
+ debug: (message, data, context) => {
24
+ isLoggingEnabled() && console.debug("[AI Translation]", message, data, context ? JSON.stringify(context, null, 2) : "");
25
+ },
26
+ group: (label, context) => {
27
+ isLoggingEnabled() && console.group(`[AI Translation] ${label}`, context ? JSON.stringify(context, null, 2) : "");
28
+ },
29
+ groupEnd: () => {
30
+ isLoggingEnabled() && console.groupEnd();
31
+ },
32
+ time: (label) => {
33
+ isLoggingEnabled() && console.time(`[AI Translation] ${label}`);
34
+ },
35
+ timeEnd: (label) => {
36
+ isLoggingEnabled() && console.timeEnd(`[AI Translation] ${label}`);
37
+ }
38
+ }, validateOpenAIApiKey = async (apiKey, model = "gpt-5") => {
39
+ try {
40
+ const response = await fetch("https://api.openai.com/v1/models", {
41
+ method: "GET",
42
+ headers: {
43
+ Authorization: `Bearer ${apiKey}`
44
+ }
45
+ });
46
+ if (!response.ok)
47
+ return {
48
+ valid: !1,
49
+ error: (await response.json()).error?.message || "Invalid API key"
50
+ };
51
+ const availableModels = (await response.json()).data.map((m) => m.id);
52
+ return availableModels.includes(model) ? {
53
+ valid: !0,
54
+ modelAvailable: availableModels
55
+ } : {
56
+ valid: !1,
57
+ error: `Model ${model} not available. Try: ${availableModels.slice(0, 5).join(", ")}`
58
+ };
59
+ } catch (error) {
60
+ return {
61
+ valid: !1,
62
+ error: error instanceof Error ? error.message : "Network error"
63
+ };
64
+ }
65
+ }, TranslationContext = createContext({
14
66
  document: null,
15
67
  config: null,
16
68
  overallStatus: "idle",
@@ -25,14 +77,126 @@ const TranslationContext = createContext({
25
77
  openaiValidated: !1,
26
78
  dispatch: () => {
27
79
  }
28
- }), useTranslationContext = () => {
80
+ }), translationReducer = (context, action) => {
81
+ const logContext = {
82
+ documentId: context.document?._id,
83
+ actionType: action.type,
84
+ component: "translationReducer"
85
+ };
86
+ switch (devLog.debug("Translation action dispatched", {
87
+ ...logContext,
88
+ payload: action.payload
89
+ }), action.type) {
90
+ case "SET_OVERALL_STATUS":
91
+ return { ...context, overallStatus: action.payload };
92
+ case "SET_TRANSLATION_METADATA":
93
+ return { ...context, translationMetadata: action.payload };
94
+ case "SET_SELECTED_TARGET_LANGUAGES":
95
+ return {
96
+ ...context,
97
+ selectedTargetLanguages: action.payload
98
+ };
99
+ case "SET_TRANSLATION_MODE":
100
+ return {
101
+ ...context,
102
+ translationMode: action.payload
103
+ };
104
+ case "SET_OPENAI_VALIDATED":
105
+ return {
106
+ ...context,
107
+ openaiValidated: action.payload
108
+ };
109
+ case "SET_SHOULD_TRANSLATE_REFERENCES":
110
+ return {
111
+ ...context,
112
+ shouldTranslateReferences: action.payload
113
+ };
114
+ case "SET_MAX_REFERENCE_DEPTH":
115
+ return {
116
+ ...context,
117
+ maxReferenceDepth: action.payload
118
+ };
119
+ case "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS":
120
+ return {
121
+ ...context,
122
+ shouldOverwriteFreshTranslations: action.payload
123
+ };
124
+ case "SET_REFERENCES":
125
+ return {
126
+ ...context,
127
+ references: action.payload
128
+ };
129
+ case "SET_TRANSLATION_JOB_STATUS": {
130
+ const jobKey = `${action.payload._id}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs), existingJob = newTranslationJobs.get(jobKey), mergedJob = defaults(action.payload, existingJob);
131
+ return newTranslationJobs.set(jobKey, mergedJob), {
132
+ ...context,
133
+ translationJobs: newTranslationJobs
134
+ };
135
+ }
136
+ default:
137
+ return context;
138
+ }
139
+ }, TranslationContextProvider = ({ document, config, children }) => {
140
+ devLog.debug("TranslationContextProvider initialized", {
141
+ documentId: document._id,
142
+ documentType: document._type,
143
+ supportedLanguages: config.supportedLanguages?.map((l) => l.id),
144
+ component: "TranslationContextProvider"
145
+ });
146
+ const initialContext = {
147
+ document,
148
+ config,
149
+ overallStatus: "idle",
150
+ translationMetadata: null,
151
+ selectedTargetLanguages: [],
152
+ shouldOverwriteFreshTranslations: !1,
153
+ shouldTranslateReferences: !1,
154
+ maxReferenceDepth: 1,
155
+ references: [],
156
+ translationJobs: /* @__PURE__ */ new Map(),
157
+ translationMode: config?.openai?.defaultMode || "sanity-only",
158
+ openaiValidated: !1
159
+ }, [context, dispatch] = useReducer(translationReducer, initialContext);
160
+ return useEffect(() => {
161
+ (async () => {
162
+ if (config?.openai?.enabled && config?.openai?.apiKey)
163
+ try {
164
+ const validation = await validateOpenAIApiKey(config.openai.apiKey, config.openai.model || "gpt-5");
165
+ dispatch({
166
+ type: "SET_OPENAI_VALIDATED",
167
+ payload: validation.valid
168
+ }), validation.valid ? devLog.info("OpenAI API key validated successfully", {
169
+ model: config.openai.model || "gpt-5",
170
+ component: "TranslationContextProvider"
171
+ }) : devLog.error("OpenAI API key validation failed", {
172
+ error: validation.error,
173
+ component: "TranslationContextProvider"
174
+ });
175
+ } catch (error) {
176
+ devLog.error("OpenAI validation error", error instanceof Error ? error : new Error(String(error)), {
177
+ component: "TranslationContextProvider"
178
+ }), dispatch({
179
+ type: "SET_OPENAI_VALIDATED",
180
+ payload: !1
181
+ });
182
+ }
183
+ })();
184
+ }, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), devLog.debug("Translation context state initialized", {
185
+ documentId: document._id,
186
+ overallStatus: context.overallStatus,
187
+ selectedLanguages: context.selectedTargetLanguages,
188
+ translationMode: context.translationMode,
189
+ openaiEnabled: config?.openai?.enabled,
190
+ component: "TranslationContextProvider"
191
+ }), /* @__PURE__ */ jsx(TranslationContext.Provider, { value: { ...context, dispatch }, children });
192
+ }, useTranslationContext = () => {
29
193
  const context = useContext(TranslationContext);
30
194
  if (!context.document || !context.config)
31
195
  throw new Error(
32
196
  "useTranslationContext must be used within a TranslationContextProvider with document and config props"
33
197
  );
34
198
  return context;
35
- }, TRANSLATION_METADATA_TYPE = "translation.metadata", undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
199
+ }, undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_TYPE = "translation.metadata", TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
36
200
  const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
37
201
  params: {
38
202
  documentId: undraftId(documentId),
@@ -59,31 +223,6 @@ const TranslationContext = createContext({
59
223
  loading,
60
224
  error
61
225
  };
62
- }, isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true" || process.env.SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true", devLog = {
63
- info: (message, context) => {
64
- isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
65
- },
66
- warn: (message, context) => {
67
- isLoggingEnabled() && console.warn("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
68
- },
69
- error: (message, error, context) => {
70
- isLoggingEnabled() && console.error("[AI Translation]", message, error, context ? JSON.stringify(context, null, 2) : "");
71
- },
72
- debug: (message, data, context) => {
73
- isLoggingEnabled() && console.debug("[AI Translation]", message, data, context ? JSON.stringify(context, null, 2) : "");
74
- },
75
- group: (label, context) => {
76
- isLoggingEnabled() && console.group(`[AI Translation] ${label}`, context ? JSON.stringify(context, null, 2) : "");
77
- },
78
- groupEnd: () => {
79
- isLoggingEnabled() && console.groupEnd();
80
- },
81
- time: (label) => {
82
- isLoggingEnabled() && console.time(`[AI Translation] ${label}`);
83
- },
84
- timeEnd: (label) => {
85
- isLoggingEnabled() && console.timeEnd(`[AI Translation] ${label}`);
86
- }
87
226
  };
88
227
  async function validateSuggestion(client, suggestion) {
89
228
  return suggestion.type === "glossaryTerm" ? validateGlossaryTerm(client, suggestion) : suggestion.type === "protectedPhrase" ? validateProtectedPhrase(client, suggestion) : suggestion.type === "globalPrompt" || suggestion.type === "localePrompt" ? validatePrompt(suggestion) : { valid: !0, status: "valid" };
@@ -411,7 +550,7 @@ function convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage)
411
550
  type: "glossaryTerm",
412
551
  description: `${term.sourceValue} \u2192 ${term.targetValue}`,
413
552
  approved: !0,
414
- // Use the provided source/target languages instead of relying on AI to provide them
553
+ // Use explicit source/target languages from history instead of AI-provided values
415
554
  sourceLocale: sourceLanguage,
416
555
  sourceValue: term.sourceValue,
417
556
  targetLocale: targetLanguage,
@@ -1069,358 +1208,45 @@ const ICON_SIZE = 64, progressIconMap = {
1069
1208
  )
1070
1209
  ] })
1071
1210
  ] });
1072
- }, validateOpenAIApiKey = async (apiKey, model = "gpt-5") => {
1073
- try {
1074
- const response = await fetch("https://api.openai.com/v1/models", {
1075
- method: "GET",
1076
- headers: {
1077
- Authorization: `Bearer ${apiKey}`
1078
- }
1079
- });
1080
- if (!response.ok)
1081
- return {
1082
- valid: !1,
1083
- error: (await response.json()).error?.message || "Invalid API key"
1084
- };
1085
- const availableModels = (await response.json()).data.map((m) => m.id);
1086
- return availableModels.includes(model) ? {
1087
- valid: !0,
1088
- modelAvailable: availableModels
1089
- } : {
1090
- valid: !1,
1091
- error: `Model ${model} not available. Try: ${availableModels.slice(0, 5).join(", ")}`
1092
- };
1093
- } catch (error) {
1094
- return {
1095
- valid: !1,
1096
- error: error instanceof Error ? error.message : "Network error"
1097
- };
1098
- }
1099
- }, translationReducer = (context, action) => {
1100
- const logContext = {
1101
- documentId: context.document?._id,
1102
- actionType: action.type,
1103
- component: "translationReducer"
1104
- };
1105
- switch (devLog.debug("Translation action dispatched", {
1106
- ...logContext,
1107
- payload: action.payload
1108
- }), action.type) {
1109
- case "SET_OVERALL_STATUS":
1110
- return { ...context, overallStatus: action.payload };
1111
- case "SET_TRANSLATION_METADATA":
1112
- return { ...context, translationMetadata: action.payload };
1113
- case "SET_SELECTED_TARGET_LANGUAGES":
1114
- return {
1115
- ...context,
1116
- selectedTargetLanguages: action.payload
1117
- };
1118
- case "SET_TRANSLATION_MODE":
1119
- return {
1120
- ...context,
1121
- translationMode: action.payload
1122
- };
1123
- case "SET_OPENAI_VALIDATED":
1124
- return {
1125
- ...context,
1126
- openaiValidated: action.payload
1127
- };
1128
- case "SET_SHOULD_TRANSLATE_REFERENCES":
1129
- return {
1130
- ...context,
1131
- shouldTranslateReferences: action.payload
1132
- };
1133
- case "SET_MAX_REFERENCE_DEPTH":
1134
- return {
1135
- ...context,
1136
- maxReferenceDepth: action.payload
1137
- };
1138
- case "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS":
1139
- return {
1140
- ...context,
1141
- shouldOverwriteFreshTranslations: action.payload
1142
- };
1143
- case "SET_REFERENCES":
1144
- return {
1145
- ...context,
1146
- references: action.payload
1147
- };
1148
- case "SET_TRANSLATION_JOB_STATUS": {
1149
- const jobKey = `${action.payload._id}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs), existingJob = newTranslationJobs.get(jobKey), mergedJob = defaults(action.payload, existingJob);
1150
- return newTranslationJobs.set(jobKey, mergedJob), {
1151
- ...context,
1152
- translationJobs: newTranslationJobs
1153
- };
1154
- }
1155
- default:
1156
- return context;
1157
- }
1158
- }, TranslationContextProvider = ({ document, config, children }) => {
1159
- devLog.debug("TranslationContextProvider initialized", {
1160
- documentId: document._id,
1161
- documentType: document._type,
1162
- supportedLanguages: config.supportedLanguages?.map((l) => l.id),
1163
- component: "TranslationContextProvider"
1164
- });
1165
- const initialContext = {
1166
- document,
1167
- config,
1168
- overallStatus: "idle",
1169
- translationMetadata: null,
1170
- selectedTargetLanguages: [],
1171
- shouldOverwriteFreshTranslations: !1,
1172
- shouldTranslateReferences: !1,
1173
- maxReferenceDepth: 1,
1174
- references: [],
1175
- translationJobs: /* @__PURE__ */ new Map(),
1176
- translationMode: config?.openai?.defaultMode || "sanity-only",
1177
- openaiValidated: !1
1178
- }, [context, dispatch] = useReducer(translationReducer, initialContext);
1179
- return useEffect(() => {
1180
- (async () => {
1181
- if (config?.openai?.enabled && config?.openai?.apiKey)
1182
- try {
1183
- const validation = await validateOpenAIApiKey(config.openai.apiKey, config.openai.model || "gpt-5");
1184
- dispatch({
1185
- type: "SET_OPENAI_VALIDATED",
1186
- payload: validation.valid
1187
- }), validation.valid ? devLog.info("OpenAI API key validated successfully", {
1188
- model: config.openai.model || "gpt-5",
1189
- component: "TranslationContextProvider"
1190
- }) : devLog.error("OpenAI API key validation failed", {
1191
- error: validation.error,
1192
- component: "TranslationContextProvider"
1193
- });
1194
- } catch (error) {
1195
- devLog.error("OpenAI validation error", error instanceof Error ? error : new Error(String(error)), {
1196
- component: "TranslationContextProvider"
1197
- }), dispatch({
1198
- type: "SET_OPENAI_VALIDATED",
1199
- payload: !1
1200
- });
1201
- }
1202
- })();
1203
- }, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), devLog.debug("Translation context state initialized", {
1204
- documentId: document._id,
1205
- overallStatus: context.overallStatus,
1206
- selectedLanguages: context.selectedTargetLanguages,
1207
- translationMode: context.translationMode,
1208
- openaiEnabled: config?.openai?.enabled,
1209
- component: "TranslationContextProvider"
1210
- }), /* @__PURE__ */ jsx(TranslationContext.Provider, { value: { ...context, dispatch }, children });
1211
- }, getGiveTranslationFeedbackAction = (config) => ({ draft, published }) => {
1212
- const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
1213
- return devLog.debug("ImproveTranslationPromptsAction initialized", {
1214
- documentId: document?._id,
1215
- documentType: document?._type
1216
- }), !document || !config.translatableSchemaTypes?.includes(document._type) ? (devLog.warn("Document not in translatable schema types", {
1217
- documentType: document?._type,
1218
- translatableTypes: config.translatableSchemaTypes
1219
- }), null) : config ? {
1220
- label: "Give Translation Feedback",
1221
- icon: TranslateIcon,
1222
- onHandle: () => {
1223
- devLog.info("Give Translation Feedback dialog opened", {
1224
- documentId: document._id,
1225
- documentType: document._type,
1226
- component: "GiveTranslationFeedbackAction"
1227
- }), setIsDialogOpen(!0);
1228
- },
1229
- dialog: isDialogOpen && {
1230
- type: "dialog",
1231
- onClose: () => setIsDialogOpen(!1),
1232
- header: "Give Translation Feedback",
1233
- content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(GiveFeedbackDialog, { setIsDialogOpen }) })
1234
- }
1235
- } : (devLog.error("Missing config in TranslateAction"), null);
1236
- }, translationGlossaryTerm = defineType({
1237
- name: "translationGlossaryTerm",
1238
- title: "Translation Glossary Term",
1239
- icon: TranslateIcon,
1240
- type: "document",
1241
- fields: [
1242
- defineField({
1243
- name: "translations",
1244
- title: "Translations",
1245
- type: "internationalizedArrayString"
1246
- })
1247
- ],
1248
- preview: {
1249
- select: {
1250
- translations: "translations"
1251
- },
1252
- prepare({ translations }) {
1253
- const enTranslation = translations?.find((t) => t.locale === "en-us"), firstTranslation = translations?.[0];
1254
- return {
1255
- title: enTranslation?.value || firstTranslation?.value || "No translation available"
1256
- };
1257
- }
1258
- }
1259
- }), translationHistory = defineField({
1260
- name: "translationHistory",
1261
- type: "array",
1262
- of: [
1263
- {
1264
- name: "targetLanguageHistory",
1265
- type: "object",
1266
- fields: [
1267
- defineField({
1268
- name: "language",
1269
- title: "Language",
1270
- description: "The language of the target document",
1271
- type: "string"
1272
- }),
1273
- defineField({
1274
- name: "sourceId",
1275
- title: "Source ID",
1276
- description: "The ID of the source document",
1277
- type: "string"
1278
- }),
1279
- defineField({
1280
- name: "sourceRev",
1281
- title: "Source Revision",
1282
- description: "The revision ID of the source document at the time of translation",
1283
- type: "string"
1284
- }),
1285
- defineField({
1286
- name: "sourceLanguage",
1287
- title: "Source Language",
1288
- description: "The language of the source document",
1289
- type: "string"
1290
- }),
1291
- defineField({
1292
- name: "sourceSnapshot",
1293
- title: "Source Snapshot",
1294
- description: "The snapshot of the source document at the time of translation",
1295
- type: "text"
1296
- }),
1297
- defineField({
1298
- name: "targetId",
1299
- title: "Target ID",
1300
- description: "The ID of the target document",
1301
- type: "string"
1302
- })
1303
- ]
1304
- }
1305
- ]
1306
- }), translationSettings = defineType({
1307
- name: "translationSettings",
1308
- title: "Translation Settings",
1309
- icon: TranslateIcon,
1310
- type: "document",
1311
- groups: [
1312
- {
1313
- name: "general",
1314
- title: "General",
1315
- default: !0
1316
- },
1317
- {
1318
- name: "history",
1319
- title: "History"
1320
- }
1321
- ],
1322
- fields: [
1323
- defineField({
1324
- name: "globalPrompt",
1325
- title: "Global Translation Prompt",
1326
- description: "A prompt that will given to the Sanity AI Agent for all translation tasks.",
1327
- type: "text",
1328
- rows: 5,
1329
- group: "general"
1330
- }),
1331
- defineField({
1332
- name: "localePrompts",
1333
- title: "Locale Prompts",
1334
- description: "Prompts that will be given to the Sanity AI Agent for each locale.",
1335
- type: "internationalizedArrayText",
1336
- group: "general"
1337
- }),
1338
- defineField({
1339
- name: "protectedPhrases",
1340
- title: "Protected Phrases",
1341
- description: "Phrases that will never be translated.",
1342
- type: "array",
1343
- of: [{ type: "string" }],
1344
- group: "general"
1345
- }),
1346
- defineField({
1347
- name: "changeHistory",
1348
- title: "Change History",
1349
- description: "History of changes made via user feedback.",
1350
- type: "array",
1351
- readOnly: !0,
1352
- group: "history",
1353
- of: [
1354
- {
1355
- type: "object",
1356
- fields: [
1357
- defineField({
1358
- name: "timestamp",
1359
- title: "Timestamp",
1360
- type: "datetime"
1361
- }),
1362
- defineField({
1363
- name: "changeType",
1364
- title: "Change Type",
1365
- type: "string",
1366
- options: {
1367
- list: [
1368
- { title: "Global Prompt", value: "globalPrompt" },
1369
- { title: "Locale Prompt", value: "localePrompt" },
1370
- { title: "Protected Phrase", value: "protectedPhrase" },
1371
- { title: "Glossary Term", value: "glossaryTerm" }
1372
- ]
1373
- }
1374
- }),
1375
- defineField({
1376
- name: "description",
1377
- title: "Description",
1378
- type: "text",
1379
- rows: 3
1380
- }),
1381
- defineField({
1382
- name: "locale",
1383
- title: "Locale",
1384
- type: "string",
1385
- description: "Target locale for locale-specific changes"
1386
- })
1387
- ],
1388
- preview: {
1389
- select: {
1390
- timestamp: "timestamp",
1391
- changeType: "changeType",
1392
- description: "description"
1393
- },
1394
- prepare({ timestamp, changeType, description }) {
1395
- return {
1396
- title: `${changeType} - ${new Date(timestamp).toLocaleDateString()}`,
1397
- subtitle: description
1398
- };
1399
- }
1400
- }
1401
- }
1402
- ]
1403
- })
1404
- ],
1405
- preview: {
1406
- prepare: () => ({
1407
- title: "Translation Settings"
1408
- })
1409
- }
1410
- });
1411
- class Semaphore {
1412
- permits;
1413
- queue = [];
1414
- constructor(permits) {
1415
- this.permits = permits;
1416
- }
1417
- /**
1418
- * Acquire a permit from the semaphore
1419
- * If no permits are available, the caller will wait until one becomes available
1420
- */
1421
- async acquire() {
1422
- return this.permits > 0 ? (this.permits--, Promise.resolve()) : new Promise((resolve) => {
1423
- this.queue.push(resolve);
1211
+ }, getGiveTranslationFeedbackAction = (config) => ({ draft, published }) => {
1212
+ const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
1213
+ return devLog.debug("ImproveTranslationPromptsAction initialized", {
1214
+ documentId: document?._id,
1215
+ documentType: document?._type
1216
+ }), !document || !config.translatableSchemaTypes?.includes(document._type) ? (devLog.warn("Document not in translatable schema types", {
1217
+ documentType: document?._type,
1218
+ translatableTypes: config.translatableSchemaTypes
1219
+ }), null) : config ? {
1220
+ label: "Give Translation Feedback",
1221
+ icon: TranslateIcon,
1222
+ onHandle: () => {
1223
+ devLog.info("Give Translation Feedback dialog opened", {
1224
+ documentId: document._id,
1225
+ documentType: document._type,
1226
+ component: "GiveTranslationFeedbackAction"
1227
+ }), setIsDialogOpen(!0);
1228
+ },
1229
+ dialog: isDialogOpen && {
1230
+ type: "dialog",
1231
+ onClose: () => setIsDialogOpen(!1),
1232
+ header: "Give Translation Feedback",
1233
+ content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(GiveFeedbackDialog, { setIsDialogOpen }) })
1234
+ }
1235
+ } : (devLog.error("Missing config in TranslateAction"), null);
1236
+ };
1237
+ class Semaphore {
1238
+ permits;
1239
+ queue = [];
1240
+ constructor(permits) {
1241
+ this.permits = permits;
1242
+ }
1243
+ /**
1244
+ * Acquire a permit from the semaphore
1245
+ * If no permits are available, the caller will wait until one becomes available
1246
+ */
1247
+ async acquire() {
1248
+ return this.permits > 0 ? (this.permits--, Promise.resolve()) : new Promise((resolve) => {
1249
+ this.queue.push(resolve);
1424
1250
  });
1425
1251
  }
1426
1252
  /**
@@ -1583,11 +1409,21 @@ const fetchTranslationPrompts = async ({
1583
1409
  const sourceDocumentString = documentToPlainText(sourceDocument), translationSettings2 = await client.fetch(
1584
1410
  `*[_id == "translation-settings"][0] {
1585
1411
  globalPrompt,
1586
- localePrompts[locale in $targetLanguages],
1412
+ localePrompts[_key in $targetLanguages],
1587
1413
  protectedPhrases
1588
1414
  }`,
1589
1415
  { targetLanguages }
1590
- ), translationGlossaryTerms = await client.fetch(
1416
+ );
1417
+ devLog.debug("Fetched translation settings", {
1418
+ hasSettings: !!translationSettings2,
1419
+ hasGlobalPrompt: !!translationSettings2?.globalPrompt,
1420
+ globalPromptLength: translationSettings2?.globalPrompt?.length,
1421
+ localePromptsCount: translationSettings2?.localePrompts?.length,
1422
+ localePrompts: translationSettings2?.localePrompts,
1423
+ protectedPhrasesCount: translationSettings2?.protectedPhrases?.length,
1424
+ targetLanguages
1425
+ });
1426
+ const translationGlossaryTerms = await client.fetch(
1591
1427
  `*[
1592
1428
  _type == "translationGlossaryTerm"
1593
1429
  && count(translations[_key in $targetLanguages]) > 0
@@ -1607,9 +1443,21 @@ const fetchTranslationPrompts = async ({
1607
1443
  )), glossaryTermsByLocale[targetLanguage] = relevantTranslations;
1608
1444
  }
1609
1445
  let filteredProtectedPhrases = translationSettings2?.protectedPhrases;
1610
- return sourceDocument && filteredProtectedPhrases && (filteredProtectedPhrases = filteredProtectedPhrases.filter(
1611
- (phrase) => protectedPhraseAppearsInDocument(phrase, sourceDocumentString)
1612
- )), {
1446
+ return sourceDocument && filteredProtectedPhrases && (devLog.debug("Filtering protected phrases", {
1447
+ totalPhrases: filteredProtectedPhrases.length,
1448
+ phrases: filteredProtectedPhrases,
1449
+ sourceDocumentStringLength: sourceDocumentString.length,
1450
+ sourceDocumentPreview: sourceDocumentString.substring(0, 500)
1451
+ }), filteredProtectedPhrases = filteredProtectedPhrases.filter((phrase) => {
1452
+ const appears = protectedPhraseAppearsInDocument(phrase, sourceDocumentString);
1453
+ return devLog.debug("Protected phrase match result", {
1454
+ phrase,
1455
+ appears
1456
+ }), appears;
1457
+ }), devLog.debug("Filtered protected phrases result", {
1458
+ remainingCount: filteredProtectedPhrases.length,
1459
+ remainingPhrases: filteredProtectedPhrases
1460
+ })), {
1613
1461
  globalPrompt: translationSettings2?.globalPrompt,
1614
1462
  protectedPhrases: filteredProtectedPhrases,
1615
1463
  localeInstructions: Object.fromEntries(
@@ -1757,7 +1605,109 @@ const fetchTranslationPrompts = async ({
1757
1605
  payload
1758
1606
  });
1759
1607
  }
1760
- }, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
1608
+ }, DEFAULT_OPTIONS = {
1609
+ maxRetries: 3,
1610
+ initialDelayMs: 1e3,
1611
+ maxDelayMs: 3e4,
1612
+ backoffMultiplier: 2,
1613
+ jitter: !0
1614
+ };
1615
+ function isRetryableError(error) {
1616
+ if (error instanceof Error) {
1617
+ const message = error.message.toLowerCase(), name = error.name.toLowerCase(), nonRetryablePatterns = [
1618
+ "schema not found",
1619
+ "invalid schema",
1620
+ "not found",
1621
+ "unauthorized",
1622
+ "forbidden",
1623
+ "invalid api key",
1624
+ "missing api key",
1625
+ "validation failed",
1626
+ "invalid document",
1627
+ "source language not found",
1628
+ "invalid configuration",
1629
+ "type mismatch"
1630
+ ];
1631
+ for (const pattern of nonRetryablePatterns)
1632
+ if (message.includes(pattern) || name.includes(pattern))
1633
+ return !1;
1634
+ const retryablePatterns = [
1635
+ "failed to fetch",
1636
+ "network",
1637
+ "timeout",
1638
+ "econnreset",
1639
+ "econnrefused",
1640
+ "enotfound",
1641
+ "socket",
1642
+ "rate limit",
1643
+ "too many requests",
1644
+ "429",
1645
+ "500",
1646
+ "502",
1647
+ "503",
1648
+ "504",
1649
+ "internal server error",
1650
+ "bad gateway",
1651
+ "service unavailable",
1652
+ "gateway timeout",
1653
+ "temporarily unavailable",
1654
+ "try again"
1655
+ ];
1656
+ for (const pattern of retryablePatterns)
1657
+ if (message.includes(pattern) || name.includes(pattern))
1658
+ return !0;
1659
+ }
1660
+ if (typeof error == "object" && error !== null) {
1661
+ const statusCode = error.statusCode || error.status;
1662
+ if (statusCode) {
1663
+ if (statusCode === 429 || statusCode >= 500 && statusCode < 600)
1664
+ return !0;
1665
+ if (statusCode >= 400 && statusCode < 500)
1666
+ return !1;
1667
+ }
1668
+ }
1669
+ return !0;
1670
+ }
1671
+ function calculateBackoffDelay(attempt, options) {
1672
+ const { initialDelayMs, maxDelayMs, backoffMultiplier, jitter } = options;
1673
+ let delay = initialDelayMs * backoffMultiplier ** attempt;
1674
+ if (delay = Math.min(delay, maxDelayMs), jitter) {
1675
+ const jitterFactor = 0.75 + Math.random() * 0.5;
1676
+ delay = Math.round(delay * jitterFactor);
1677
+ }
1678
+ return delay;
1679
+ }
1680
+ function sleep(ms) {
1681
+ return new Promise((resolve) => setTimeout(resolve, ms));
1682
+ }
1683
+ async function withRetry(fn, options = {}) {
1684
+ const opts = { ...DEFAULT_OPTIONS, ...options }, shouldRetry = opts.isRetryable ?? isRetryableError;
1685
+ let lastError, attempts = 0;
1686
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
1687
+ attempts = attempt + 1;
1688
+ try {
1689
+ return { success: !0, result: await fn(), attempts };
1690
+ } catch (error) {
1691
+ if (lastError = error, attempt >= opts.maxRetries) {
1692
+ devLog.debug("Max retries exhausted", { attempt: attempts, error });
1693
+ break;
1694
+ }
1695
+ if (!shouldRetry(error)) {
1696
+ devLog.debug("Error is not retryable", { attempt: attempts, error });
1697
+ break;
1698
+ }
1699
+ const delay = calculateBackoffDelay(attempt, opts);
1700
+ devLog.debug("Scheduling retry", {
1701
+ attempt: attempts,
1702
+ nextAttempt: attempt + 2,
1703
+ delayMs: delay,
1704
+ error: error instanceof Error ? error.message : String(error)
1705
+ }), opts.onRetry && opts.onRetry(attempt + 1, error, delay), await sleep(delay);
1706
+ }
1707
+ }
1708
+ return { success: !1, error: lastError, attempts };
1709
+ }
1710
+ const getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
1761
1711
  (translation) => translation._key === targetLanguage
1762
1712
  )?.value._ref || null, getTranslationMetadata = async ({
1763
1713
  client,
@@ -1848,16 +1798,189 @@ const fetchTranslationPrompts = async ({
1848
1798
  status
1849
1799
  })
1850
1800
  });
1851
- }, callGPT5 = async (apiKey, request) => (request.model || "gpt-5") === "gpt-5" ? await callGPT5ResponsesAPI(apiKey, request) : await callChatCompletionsAPI(apiKey, request), callGPT5ResponsesAPI = async (apiKey, request) => {
1801
+ };
1802
+ function applyTransforms({
1803
+ document,
1804
+ registry,
1805
+ schemaType,
1806
+ parentPath = ""
1807
+ }) {
1808
+ if (!document)
1809
+ return { transformedDocument: document, originalStructure: {} };
1810
+ if (typeof document != "object")
1811
+ return { transformedDocument: document, originalStructure: {} };
1812
+ if (Array.isArray(document)) {
1813
+ const originalStructure2 = {};
1814
+ return { transformedDocument: document.map((item, index) => {
1815
+ const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`, { transformedDocument: transformedItem, originalStructure: itemOriginalStructure } = applyTransforms({
1816
+ document: item,
1817
+ registry,
1818
+ schemaType,
1819
+ parentPath: itemPath
1820
+ });
1821
+ return Object.assign(originalStructure2, itemOriginalStructure), transformedItem;
1822
+ }), originalStructure: originalStructure2 };
1823
+ }
1824
+ const transformedObj = {}, originalStructure = {};
1825
+ for (const [key, value] of Object.entries(document)) {
1826
+ const fieldPath = parentPath ? `${parentPath}.${key}` : key;
1827
+ if (key.startsWith("_") && key !== "_type") {
1828
+ transformedObj[key] = value;
1829
+ continue;
1830
+ }
1831
+ const transform = registry.findTransform(schemaType, fieldPath, value, document);
1832
+ if (transform) {
1833
+ devLog.debug("Applying transform", { fieldPath, schemaType });
1834
+ const context = {
1835
+ fieldPath,
1836
+ schemaType,
1837
+ document
1838
+ }, transformedValue = transform.toTranslatable(value, context);
1839
+ transformedObj[key] = transformedValue, originalStructure[fieldPath] = {
1840
+ value,
1841
+ transform
1842
+ };
1843
+ } else {
1844
+ const { transformedDocument: transformedValue, originalStructure: valueOriginalStructure } = applyTransforms({
1845
+ document: value,
1846
+ registry,
1847
+ schemaType,
1848
+ parentPath: fieldPath
1849
+ });
1850
+ Object.assign(originalStructure, valueOriginalStructure), transformedObj[key] = transformedValue;
1851
+ }
1852
+ }
1853
+ return { transformedDocument: transformedObj, originalStructure };
1854
+ }
1855
+ function restoreTransforms({
1856
+ translatedDocument,
1857
+ originalStructure,
1858
+ registry,
1859
+ schemaType,
1860
+ parentPath = ""
1861
+ }) {
1862
+ if (translatedDocument == null || typeof translatedDocument != "object")
1863
+ return translatedDocument;
1864
+ if (Array.isArray(translatedDocument))
1865
+ return translatedDocument.map((item, index) => {
1866
+ const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`;
1867
+ return restoreTransforms({
1868
+ translatedDocument: item,
1869
+ originalStructure,
1870
+ registry,
1871
+ schemaType,
1872
+ parentPath: itemPath
1873
+ });
1874
+ });
1875
+ const originalData = originalStructure[parentPath];
1876
+ if (originalData) {
1877
+ const { value: originalValue, transform } = originalData, context = {
1878
+ fieldPath: parentPath,
1879
+ schemaType,
1880
+ document: translatedDocument
1881
+ };
1882
+ return devLog.debug("Restoring transform", { fieldPath: parentPath, schemaType }), transform.fromTranslatable(translatedDocument, originalValue, context);
1883
+ }
1884
+ const restoredObj = {}, translatedObj = translatedDocument;
1885
+ for (const [key, value] of Object.entries(translatedObj)) {
1886
+ const fieldPath = parentPath ? `${parentPath}.${key}` : key, fieldOriginalData = originalStructure[fieldPath];
1887
+ if (fieldOriginalData) {
1888
+ const { value: originalValue, transform } = fieldOriginalData, context = {
1889
+ fieldPath,
1890
+ schemaType,
1891
+ document: translatedDocument
1892
+ };
1893
+ devLog.debug("Restoring transform for field", { fieldPath, schemaType }), restoredObj[key] = transform.fromTranslatable(value, originalValue, context);
1894
+ } else
1895
+ restoredObj[key] = restoreTransforms({
1896
+ translatedDocument: value,
1897
+ originalStructure,
1898
+ registry,
1899
+ schemaType,
1900
+ parentPath: fieldPath
1901
+ });
1902
+ }
1903
+ return restoredObj;
1904
+ }
1905
+ class TransformRegistry {
1906
+ transforms = [];
1907
+ /**
1908
+ * Register a transform with the registry.
1909
+ * @param transform - Field transform to register
1910
+ */
1911
+ register(transform) {
1912
+ devLog.debug("Registering field transform", {
1913
+ matcher: transform.matcher
1914
+ }), this.transforms.push(transform);
1915
+ }
1916
+ /**
1917
+ * Find a matching transform for the given field.
1918
+ * If multiple transforms match, returns the first one (order matters!).
1919
+ * @param schemaType - Document schema type
1920
+ * @param fieldPath - Path to the field
1921
+ * @param value - Optional field value for value-based matching
1922
+ * @param document - Optional full document for context
1923
+ * @returns Matching transform or undefined if none found
1924
+ */
1925
+ findTransform(schemaType, fieldPath, value, document) {
1926
+ const matches = [];
1927
+ for (const transform of this.transforms) {
1928
+ const { matcher } = transform;
1929
+ if (!(matcher.schemaType && matcher.schemaType !== schemaType)) {
1930
+ if (matcher.fieldPath) {
1931
+ if (typeof matcher.fieldPath == "string") {
1932
+ if (matcher.fieldPath !== fieldPath)
1933
+ continue;
1934
+ } else if (matcher.fieldPath instanceof RegExp && !matcher.fieldPath.test(fieldPath))
1935
+ continue;
1936
+ }
1937
+ if (matcher.fieldName) {
1938
+ const segments = fieldPath.split(".");
1939
+ if (segments[segments.length - 1]?.split("[")[0] !== matcher.fieldName)
1940
+ continue;
1941
+ }
1942
+ if (matcher.valueMatcher) {
1943
+ if (value === void 0)
1944
+ continue;
1945
+ const context = {
1946
+ fieldPath,
1947
+ schemaType,
1948
+ document: document || {}
1949
+ };
1950
+ if (!matcher.valueMatcher(value, context))
1951
+ continue;
1952
+ }
1953
+ matches.push(transform);
1954
+ }
1955
+ }
1956
+ return matches.length > 1 && devLog.warn("Multiple transforms match the same field - using first match", {
1957
+ schemaType,
1958
+ fieldPath,
1959
+ matchCount: matches.length,
1960
+ firstMatcher: matches[0].matcher
1961
+ }), matches[0];
1962
+ }
1963
+ }
1964
+ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKey, request) => (request.model || "gpt-5") === "gpt-5" ? await callGPT5ResponsesAPI(apiKey, request) : await callChatCompletionsAPI(apiKey, request), callGPT5ResponsesAPI = async (apiKey, request) => {
1852
1965
  const model = request.model || "gpt-5", requestBody = {
1853
- model,
1966
+ model: "gpt-5.1",
1854
1967
  input: request.messages
1855
1968
  // Uses 'input' instead of 'messages'
1856
1969
  };
1857
1970
  request.reasoningEffort && (requestBody.reasoning = {
1858
1971
  effort: request.reasoningEffort,
1859
1972
  summary: "auto"
1860
- // Request reasoning summaries
1973
+ // Enable automatic reasoning summaries
1974
+ }), devLog.debug("OpenAI GPT-5 Request", {
1975
+ model: requestBody.model,
1976
+ reasoningEffort: request.reasoningEffort,
1977
+ messages: request.messages.map((m) => ({
1978
+ role: m.role,
1979
+ contentLength: m.content.length,
1980
+ contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
1981
+ }))
1982
+ }), devLog.debug("OpenAI GPT-5 Full Prompt", {
1983
+ messages: request.messages
1861
1984
  });
1862
1985
  const response = await fetch("https://api.openai.com/v1/responses", {
1863
1986
  method: "POST",
@@ -1892,7 +2015,19 @@ const fetchTranslationPrompts = async ({
1892
2015
  model,
1893
2016
  messages: request.messages,
1894
2017
  temperature: request.temperature ?? 0.3
1895
- }, response = await fetch("https://api.openai.com/v1/chat/completions", {
2018
+ };
2019
+ devLog.debug("OpenAI Chat Completions Request", {
2020
+ model,
2021
+ temperature: request.temperature ?? 0.3,
2022
+ messages: request.messages.map((m) => ({
2023
+ role: m.role,
2024
+ contentLength: m.content.length,
2025
+ contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
2026
+ }))
2027
+ }), devLog.debug("OpenAI Chat Completions Full Prompt", {
2028
+ messages: request.messages
2029
+ });
2030
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
1896
2031
  method: "POST",
1897
2032
  headers: {
1898
2033
  "Content-Type": "application/json",
@@ -1945,9 +2080,52 @@ const fetchTranslationPrompts = async ({
1945
2080
  }, parseReviewFormat = (text) => {
1946
2081
  const root = [], stack = [], lines = text.split(`
1947
2082
  `);
1948
- for (const line of lines) {
1949
- const trimmed = line.trim();
2083
+ for (let i = 0; i < lines.length; i++) {
2084
+ const trimmed = lines[i].trim();
1950
2085
  if (!trimmed) continue;
2086
+ const compactStringMatch = trimmed.match(/^(\w+):\s*(.*)$/);
2087
+ if (compactStringMatch && !trimmed.includes("<")) {
2088
+ const [, fieldName, text2] = compactStringMatch, node = {
2089
+ type: "field",
2090
+ name: fieldName,
2091
+ text: unescapeXml(text2),
2092
+ attributes: {},
2093
+ children: []
2094
+ };
2095
+ if (stack.length > 0) {
2096
+ const parent = stack[stack.length - 1];
2097
+ parent && parent.children.push(node);
2098
+ } else
2099
+ root.push(node);
2100
+ continue;
2101
+ }
2102
+ const compactStartMatch = trimmed.match(/^---\s+(\w+)\s+---$/);
2103
+ if (compactStartMatch) {
2104
+ const fieldName = compactStartMatch[1], contentLines = [];
2105
+ for (i++; i < lines.length; ) {
2106
+ const currentLine = lines[i];
2107
+ if (currentLine.trim().match(new RegExp(`^---\\s+/${fieldName}\\s+---$`)))
2108
+ break;
2109
+ contentLines.push(currentLine), i++;
2110
+ }
2111
+ const content = contentLines.join(`
2112
+ `);
2113
+ let contentType = "translatable.vtt.captions";
2114
+ (content.includes("<block") || content.includes("<span")) && (contentType = "translatable.richtext");
2115
+ const node = {
2116
+ type: "field",
2117
+ name: fieldName,
2118
+ text: content,
2119
+ attributes: { type: contentType },
2120
+ children: []
2121
+ };
2122
+ if (stack.length > 0) {
2123
+ const parent = stack[stack.length - 1];
2124
+ parent && parent.children.push(node);
2125
+ } else
2126
+ root.push(node);
2127
+ continue;
2128
+ }
1951
2129
  const openMatch = trimmed.match(/^<(field|block|span)([^>]*)>(.*)$/);
1952
2130
  if (openMatch) {
1953
2131
  const [, tagName, attrsStr, content] = openMatch, attributes = parseAttributes(attrsStr), node = {
@@ -1970,7 +2148,21 @@ const fetchTranslationPrompts = async ({
1970
2148
  parent && parent.children.push(node);
1971
2149
  } else
1972
2150
  root.push(node);
1973
- else
2151
+ else if (attributes.type === "translatable.vtt.captions" || attributes.type === "translatable.richtext") {
2152
+ const contentLines = [];
2153
+ for (i++; i < lines.length; ) {
2154
+ const currentLine = lines[i];
2155
+ if (currentLine.trim().match(/^<\/field>$/))
2156
+ break;
2157
+ contentLines.push(currentLine), i++;
2158
+ }
2159
+ if (node.text = contentLines.join(`
2160
+ `), stack.length > 0) {
2161
+ const parent = stack[stack.length - 1];
2162
+ parent && parent.children.push(node);
2163
+ } else
2164
+ root.push(node);
2165
+ } else
1974
2166
  stack.push(node);
1975
2167
  continue;
1976
2168
  }
@@ -1998,6 +2190,16 @@ const fetchTranslationPrompts = async ({
1998
2190
  applyFieldChanges(doc, path, node);
1999
2191
  }
2000
2192
  }, applyFieldChanges = (doc, path, node) => {
2193
+ if (node.attributes.type === "translatable.vtt.captions") {
2194
+ const fieldValue = get(doc, path);
2195
+ fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.vtt.captions" && node.text && set(doc, [...path, "translationText"], node.text);
2196
+ return;
2197
+ }
2198
+ if (node.attributes.type === "translatable.richtext") {
2199
+ const fieldValue = get(doc, path);
2200
+ fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.richtext" && node.text && set(doc, [...path, "translationText"], node.text);
2201
+ return;
2202
+ }
2001
2203
  if (node.attributes.type === "array") {
2002
2204
  node.children.forEach((child, idx) => {
2003
2205
  applyFieldChanges(doc, [...path, idx], child);
@@ -2064,7 +2266,7 @@ const fetchTranslationPrompts = async ({
2064
2266
  if (path.length === 0) {
2065
2267
  if (isPlainObject(value))
2066
2268
  for (const [key, val] of Object.entries(value))
2067
- !key.startsWith("_") && key !== "language" && serialize(val, [key], indent);
2269
+ !key.startsWith("_") && key !== "language" && serialize(val, [key], 0);
2068
2270
  return;
2069
2271
  }
2070
2272
  const fieldName = path[path.length - 1];
@@ -2077,6 +2279,14 @@ const fetchTranslationPrompts = async ({
2077
2279
  return;
2078
2280
  }
2079
2281
  if (isPlainObject(value)) {
2282
+ if (value._type === "translatable.vtt.captions" && typeof value.translationText == "string") {
2283
+ lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
2284
+ return;
2285
+ }
2286
+ if (value._type === "translatable.richtext" && typeof value.translationText == "string") {
2287
+ lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
2288
+ return;
2289
+ }
2080
2290
  lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
2081
2291
  for (const [key, val] of Object.entries(value))
2082
2292
  key.startsWith("_") || serialize(val, [...path, key], indent + 1);
@@ -2084,7 +2294,7 @@ const fetchTranslationPrompts = async ({
2084
2294
  return;
2085
2295
  }
2086
2296
  if (typeof value == "string") {
2087
- value.length > 0 && lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
2297
+ value.length > 0 && lines.push(`${fieldName}: ${escapeXml(value)}`);
2088
2298
  return;
2089
2299
  }
2090
2300
  (typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
@@ -2217,7 +2427,14 @@ const fetchTranslationPrompts = async ({
2217
2427
  sourceLanguage
2218
2428
  };
2219
2429
  try {
2220
- const sourceText = serializeDocument(sourceDocument), prompt = buildTranslationPrompt$1({
2430
+ const sourceText = serializeDocument(sourceDocument);
2431
+ devLog.info("Serialized document for OpenAI", {
2432
+ ...logContext,
2433
+ characterCount: sourceText.length,
2434
+ estimatedTokens: Math.ceil(sourceText.length / 4),
2435
+ preview: sourceText.substring(0, 500)
2436
+ });
2437
+ const prompt = buildTranslationPrompt$1({
2221
2438
  sourceText,
2222
2439
  sourceLanguage,
2223
2440
  targetLanguage,
@@ -2232,7 +2449,15 @@ const fetchTranslationPrompts = async ({
2232
2449
  ],
2233
2450
  model: openaiConfig.model,
2234
2451
  reasoningEffort: openaiConfig.reasoningEffort
2235
- }), result = deserializeDocument(sourceDocument, response.content);
2452
+ });
2453
+ devLog.info("OpenAI translation response received", {
2454
+ ...logContext,
2455
+ tokensUsed: response.tokensUsed,
2456
+ cost: response.cost,
2457
+ responseLength: response.content.length,
2458
+ responsePreview: response.content.substring(0, 500)
2459
+ });
2460
+ const result = deserializeDocument(sourceDocument, response.content);
2236
2461
  result[languageFieldName] = targetLanguage;
2237
2462
  const validation = validateStructureIntegrity(sourceDocument, result);
2238
2463
  if (!validation.valid) {
@@ -2424,11 +2649,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2424
2649
  openaiConfig,
2425
2650
  releaseId
2426
2651
  }) => {
2652
+ devLog.debug("Applying field transforms before translation", {
2653
+ documentId: sourceDocument._id,
2654
+ documentType: sourceDocument._type
2655
+ });
2656
+ const { transformedDocument, originalStructure } = applyTransforms({
2657
+ document: sourceDocument,
2658
+ registry: globalTransformRegistry,
2659
+ schemaType: sourceDocument._type
2660
+ });
2427
2661
  let translatedDoc;
2428
2662
  switch (translationMode) {
2429
2663
  case "sanity-only": {
2430
2664
  setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
2431
- sourceDocument,
2665
+ sourceDocument: transformedDocument,
2432
2666
  targetLanguage,
2433
2667
  languageFieldName,
2434
2668
  schemaId,
@@ -2442,7 +2676,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2442
2676
  throw new Error("OpenAI is not configured but openai-only mode was selected");
2443
2677
  setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), translatedDoc = await getOpenAITranslation({
2444
2678
  dispatch,
2445
- sourceDocument,
2679
+ sourceDocument: transformedDocument,
2446
2680
  targetLanguage,
2447
2681
  languageFieldName,
2448
2682
  translationPrompts,
@@ -2457,8 +2691,17 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2457
2691
  }
2458
2692
  if (!translatedDoc)
2459
2693
  return null;
2460
- const targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
2461
- ...translatedDoc,
2694
+ devLog.debug("Restoring field transforms after translation", {
2695
+ documentId: sourceDocument._id,
2696
+ documentType: sourceDocument._type
2697
+ });
2698
+ const finalDoc = restoreTransforms({
2699
+ translatedDocument: translatedDoc,
2700
+ originalStructure,
2701
+ registry: globalTransformRegistry,
2702
+ schemaType: sourceDocument._type
2703
+ }), targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
2704
+ ...finalDoc,
2462
2705
  _id: targetDocumentVersionId
2463
2706
  };
2464
2707
  return await client.action({
@@ -2502,23 +2745,50 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2502
2745
  });
2503
2746
  return;
2504
2747
  }
2505
- const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
2506
- dispatch,
2507
- client,
2508
- schemaId,
2509
- sourceDocument,
2510
- targetLanguage,
2511
- existingTranslationId,
2512
- languageFieldName,
2513
- translationPrompts,
2514
- translationMode,
2515
- openaiConfig,
2516
- releaseId
2517
- });
2518
- if (!newDocument) {
2519
- setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2748
+ const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), translationResult = await withRetry(
2749
+ () => handleTranslation({
2750
+ dispatch,
2751
+ client,
2752
+ schemaId,
2753
+ sourceDocument,
2754
+ targetLanguage,
2755
+ existingTranslationId,
2756
+ languageFieldName,
2757
+ translationPrompts,
2758
+ translationMode,
2759
+ openaiConfig,
2760
+ releaseId
2761
+ }),
2762
+ {
2763
+ maxRetries: 3,
2764
+ initialDelayMs: 1e3,
2765
+ isRetryable: isRetryableError,
2766
+ onRetry: (attempt, error, delayMs) => {
2767
+ devLog.warn("Translation failed, scheduling retry", {
2768
+ ...logContext,
2769
+ attempt,
2770
+ delayMs,
2771
+ error: error instanceof Error ? error.message : String(error)
2772
+ }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "retrying", {
2773
+ retryCount: attempt,
2774
+ errorMessage: error instanceof Error ? error.message : String(error)
2775
+ });
2776
+ }
2777
+ }
2778
+ );
2779
+ if (!translationResult.success || !translationResult.result) {
2780
+ const errorMessage = translationResult.error instanceof Error ? translationResult.error.message : String(translationResult.error || "Translation failed");
2781
+ devLog.error("Translation failed after retries", translationResult.error, {
2782
+ ...logContext,
2783
+ attempts: translationResult.attempts
2784
+ }), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
2785
+ errorMessage,
2786
+ retryCount: translationResult.attempts - 1
2787
+ // -1 because attempts includes initial try
2788
+ }), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2520
2789
  return;
2521
2790
  }
2791
+ const newDocument = translationResult.result;
2522
2792
  return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
2523
2793
  targetId: newDocument._id
2524
2794
  }), await updateTranslationMetadata({
@@ -2588,7 +2858,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2588
2858
  {
2589
2859
  _id: sourceDocument._id,
2590
2860
  _type: sourceDocument._type,
2591
- // TODO: Handle this properly
2861
+ // TODO: Extract document title more robustly across schema types
2592
2862
  title: sourceDocument.name || sourceDocument.title
2593
2863
  },
2594
2864
  ...references
@@ -3098,12 +3368,17 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3098
3368
  job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3099
3369
  "Error: ",
3100
3370
  job.errorMessage
3371
+ ] }) }) : null,
3372
+ job.status === "retrying" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3373
+ "Retrying after: ",
3374
+ job.errorMessage
3101
3375
  ] }) }) : null
3102
3376
  ] })
3103
3377
  ] }),
3104
3378
  /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
3105
3379
  job.status === "skipped" ? /* @__PURE__ */ jsx(SkippedBadge, {}) : null,
3106
- job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {}) : null,
3380
+ job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, { retryCount: job.retryCount }) : null,
3381
+ job.status === "retrying" ? /* @__PURE__ */ jsx(RetryingBadge, { retryCount: job.retryCount }) : null,
3107
3382
  job.targetId ? /* @__PURE__ */ jsx(
3108
3383
  TranslationOpenerButton,
3109
3384
  {
@@ -3183,12 +3458,25 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3183
3458
  content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "There was already a fresh translation for this document and language." }) }),
3184
3459
  children: /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: "Skipped" })
3185
3460
  }
3186
- ), ErrorBadge = () => /* @__PURE__ */ jsx(
3461
+ ), ErrorBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
3187
3462
  Tooltip,
3188
3463
  {
3189
3464
  portal: !0,
3190
- content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "This translation failed. Check the error message for details." }) }),
3191
- children: /* @__PURE__ */ jsx(Badge, { tone: "critical", children: "Failed" })
3465
+ content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: retryCount && retryCount > 0 ? `This translation failed after ${retryCount} retry attempt${retryCount > 1 ? "s" : ""}.` : "This translation failed. Check the error message for details." }) }),
3466
+ children: /* @__PURE__ */ jsxs(Badge, { tone: "critical", children: [
3467
+ "Failed",
3468
+ retryCount && retryCount > 0 ? ` (${retryCount} retries)` : ""
3469
+ ] })
3470
+ }
3471
+ ), RetryingBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
3472
+ Tooltip,
3473
+ {
3474
+ portal: !0,
3475
+ content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Retrying translation after a temporary failure." }) }),
3476
+ children: /* @__PURE__ */ jsxs(Badge, { tone: "caution", children: [
3477
+ "Retry ",
3478
+ retryCount || 1
3479
+ ] })
3192
3480
  }
3193
3481
  ), getStatusLabel = (status) => ({
3194
3482
  idle: "Idle",
@@ -3198,6 +3486,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3198
3486
  translating_with_openai: "Translating with GPT-5...",
3199
3487
  reviewing_with_openai: "Reviewing translation with GPT-5...",
3200
3488
  updating_metadata: "Saving metadata...",
3489
+ retrying: "Retrying...",
3201
3490
  error: "Error",
3202
3491
  skipped: "Skipped",
3203
3492
  completed: "Completed"
@@ -3236,7 +3525,427 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3236
3525
  content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
3237
3526
  }
3238
3527
  } : (devLog.error("Missing config in TranslateAction"), null);
3239
- }, aiTranslationPlugin = definePlugin((config) => ({
3528
+ }, translationGlossaryTerm = defineType({
3529
+ name: "translationGlossaryTerm",
3530
+ title: "Translation Glossary Term",
3531
+ icon: TranslateIcon,
3532
+ type: "document",
3533
+ fields: [
3534
+ defineField({
3535
+ name: "translations",
3536
+ title: "Translations",
3537
+ type: "internationalizedArrayString"
3538
+ })
3539
+ ],
3540
+ preview: {
3541
+ select: {
3542
+ translations: "translations"
3543
+ },
3544
+ prepare({ translations }) {
3545
+ const enTranslation = translations?.find((t) => t.locale === "en-us"), firstTranslation = translations?.[0];
3546
+ return {
3547
+ title: enTranslation?.value || firstTranslation?.value || "No translation available"
3548
+ };
3549
+ }
3550
+ }
3551
+ }), translationHistory = defineField({
3552
+ name: "translationHistory",
3553
+ type: "array",
3554
+ of: [
3555
+ {
3556
+ name: "targetLanguageHistory",
3557
+ type: "object",
3558
+ fields: [
3559
+ defineField({
3560
+ name: "language",
3561
+ title: "Language",
3562
+ description: "The language of the target document",
3563
+ type: "string"
3564
+ }),
3565
+ defineField({
3566
+ name: "sourceId",
3567
+ title: "Source ID",
3568
+ description: "The ID of the source document",
3569
+ type: "string"
3570
+ }),
3571
+ defineField({
3572
+ name: "sourceRev",
3573
+ title: "Source Revision",
3574
+ description: "The revision ID of the source document at the time of translation",
3575
+ type: "string"
3576
+ }),
3577
+ defineField({
3578
+ name: "sourceLanguage",
3579
+ title: "Source Language",
3580
+ description: "The language of the source document",
3581
+ type: "string"
3582
+ }),
3583
+ defineField({
3584
+ name: "sourceSnapshot",
3585
+ title: "Source Snapshot",
3586
+ description: "The snapshot of the source document at the time of translation",
3587
+ type: "text"
3588
+ }),
3589
+ defineField({
3590
+ name: "targetId",
3591
+ title: "Target ID",
3592
+ description: "The ID of the target document",
3593
+ type: "string"
3594
+ })
3595
+ ]
3596
+ }
3597
+ ]
3598
+ }), translationSettings = defineType({
3599
+ name: "translationSettings",
3600
+ title: "Translation Settings",
3601
+ icon: TranslateIcon,
3602
+ type: "document",
3603
+ groups: [
3604
+ {
3605
+ name: "general",
3606
+ title: "General",
3607
+ default: !0
3608
+ },
3609
+ {
3610
+ name: "history",
3611
+ title: "History"
3612
+ }
3613
+ ],
3614
+ fields: [
3615
+ defineField({
3616
+ name: "globalPrompt",
3617
+ title: "Global Translation Prompt",
3618
+ description: "A prompt that will given to the Sanity AI Agent for all translation tasks.",
3619
+ type: "text",
3620
+ rows: 5,
3621
+ group: "general"
3622
+ }),
3623
+ defineField({
3624
+ name: "localePrompts",
3625
+ title: "Locale Prompts",
3626
+ description: "Prompts that will be given to the Sanity AI Agent for each locale.",
3627
+ type: "internationalizedArrayText",
3628
+ group: "general"
3629
+ }),
3630
+ defineField({
3631
+ name: "protectedPhrases",
3632
+ title: "Protected Phrases",
3633
+ description: "Phrases that will never be translated.",
3634
+ type: "array",
3635
+ of: [{ type: "string" }],
3636
+ group: "general"
3637
+ }),
3638
+ defineField({
3639
+ name: "changeHistory",
3640
+ title: "Change History",
3641
+ description: "History of changes made via user feedback.",
3642
+ type: "array",
3643
+ readOnly: !0,
3644
+ group: "history",
3645
+ of: [
3646
+ {
3647
+ type: "object",
3648
+ fields: [
3649
+ defineField({
3650
+ name: "timestamp",
3651
+ title: "Timestamp",
3652
+ type: "datetime"
3653
+ }),
3654
+ defineField({
3655
+ name: "changeType",
3656
+ title: "Change Type",
3657
+ type: "string",
3658
+ options: {
3659
+ list: [
3660
+ { title: "Global Prompt", value: "globalPrompt" },
3661
+ { title: "Locale Prompt", value: "localePrompt" },
3662
+ { title: "Protected Phrase", value: "protectedPhrase" },
3663
+ { title: "Glossary Term", value: "glossaryTerm" }
3664
+ ]
3665
+ }
3666
+ }),
3667
+ defineField({
3668
+ name: "description",
3669
+ title: "Description",
3670
+ type: "text",
3671
+ rows: 3
3672
+ }),
3673
+ defineField({
3674
+ name: "locale",
3675
+ title: "Locale",
3676
+ type: "string",
3677
+ description: "Target locale for locale-specific changes"
3678
+ })
3679
+ ],
3680
+ preview: {
3681
+ select: {
3682
+ timestamp: "timestamp",
3683
+ changeType: "changeType",
3684
+ description: "description"
3685
+ },
3686
+ prepare({ timestamp, changeType, description }) {
3687
+ return {
3688
+ title: `${changeType} - ${new Date(timestamp).toLocaleDateString()}`,
3689
+ subtitle: description
3690
+ };
3691
+ }
3692
+ }
3693
+ }
3694
+ ]
3695
+ })
3696
+ ],
3697
+ preview: {
3698
+ prepare: () => ({
3699
+ title: "Translation Settings"
3700
+ })
3701
+ }
3702
+ });
3703
+ function isPortableText(value) {
3704
+ return Array.isArray(value) && value.some((item) => item?._type === "block");
3705
+ }
3706
+ const richTextTransform = {
3707
+ // Matcher combines field name pattern with value-based detection
3708
+ // fieldPath pattern acts as a pre-filter for performance
3709
+ // valueMatcher definitively checks if the value is actually Portable Text
3710
+ matcher: {
3711
+ // Pre-filter: Match all fields to enable detection based solely on value
3712
+ fieldPath: /.*/,
3713
+ // Definitive check: Verify the value is actually Portable Text
3714
+ valueMatcher: (value) => Array.isArray(value) && isPortableText(value)
3715
+ },
3716
+ toTranslatable: (value, context) => {
3717
+ if (!Array.isArray(value) || !isPortableText(value))
3718
+ return {
3719
+ _type: "translatable.richtext",
3720
+ translationText: JSON.stringify(value),
3721
+ _originalValue: value
3722
+ };
3723
+ devLog.info("Applying rich text transform with XML serialization", {
3724
+ fieldPath: context.fieldPath,
3725
+ blockCount: value.length
3726
+ });
3727
+ try {
3728
+ const tempDoc = {
3729
+ _id: "temp",
3730
+ _type: "richTextWrapper",
3731
+ _createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3732
+ _updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3733
+ _rev: "temp",
3734
+ content: value
3735
+ }, serialized = serializeDocument(tempDoc);
3736
+ devLog.debug("Serialized rich text document", {
3737
+ fieldPath: context.fieldPath,
3738
+ serializedLength: serialized.length,
3739
+ preview: serialized.substring(0, 200)
3740
+ });
3741
+ const contentRegex = /<field name="content"[^>]*>([\s\S]*)<\/field>/s, match = serialized.match(contentRegex);
3742
+ if (match?.[1]) {
3743
+ const result = {
3744
+ _type: "translatable.richtext",
3745
+ translationText: match[1].trim(),
3746
+ _originalValue: value
3747
+ };
3748
+ return devLog.debug("Successfully extracted and wrapped rich text", {
3749
+ fieldPath: context.fieldPath,
3750
+ translationTextLength: result.translationText.length
3751
+ }), result;
3752
+ }
3753
+ return devLog.warn("Failed to serialize rich text content - regex did not match", {
3754
+ fieldPath: context.fieldPath,
3755
+ serializedPreview: serialized.substring(0, 500)
3756
+ }), {
3757
+ _type: "translatable.richtext",
3758
+ translationText: JSON.stringify(value),
3759
+ _originalValue: value
3760
+ };
3761
+ } catch (error) {
3762
+ return devLog.error("Error serializing rich text", error, {
3763
+ fieldPath: context.fieldPath
3764
+ }), {
3765
+ _type: "translatable.richtext",
3766
+ translationText: JSON.stringify(value),
3767
+ _originalValue: value
3768
+ };
3769
+ }
3770
+ },
3771
+ fromTranslatable: (translatedValue, originalValue, context) => {
3772
+ if (!translatedValue || translatedValue._type !== "translatable.richtext" || !translatedValue.translationText)
3773
+ return devLog.debug("Value is not a translatable rich text object, returning original", {
3774
+ fieldPath: context.fieldPath,
3775
+ hasCorrectType: translatedValue._type === "translatable.richtext"
3776
+ }), originalValue;
3777
+ devLog.info("Deserializing rich text content", {
3778
+ fieldPath: context.fieldPath,
3779
+ translationTextLength: translatedValue.translationText.length
3780
+ });
3781
+ try {
3782
+ const { translationText, _originalValue } = translatedValue, wrappedSerialized = `<field name="content">
3783
+ ${translationText}
3784
+ </field>`;
3785
+ devLog.debug("Prepared wrapped serialized content", {
3786
+ fieldPath: context.fieldPath,
3787
+ wrappedLength: wrappedSerialized.length,
3788
+ preview: wrappedSerialized.substring(0, 300)
3789
+ });
3790
+ const tempOriginalDoc = {
3791
+ _id: "temp",
3792
+ _type: "richTextWrapper",
3793
+ _createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3794
+ _updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3795
+ _rev: "temp",
3796
+ content: originalValue
3797
+ }, result = deserializeDocument(tempOriginalDoc, wrappedSerialized);
3798
+ return devLog.debug("Deserialization result", {
3799
+ fieldPath: context.fieldPath,
3800
+ hasContent: !!result.content,
3801
+ contentType: Array.isArray(result.content) ? "array" : typeof result.content,
3802
+ contentLength: Array.isArray(result.content) ? result.content.length : void 0
3803
+ }), result.content ? result.content : (devLog.warn("Deserialization returned no content, returning original", {
3804
+ fieldPath: context.fieldPath,
3805
+ resultKeys: Object.keys(result)
3806
+ }), originalValue);
3807
+ } catch (error) {
3808
+ return devLog.error("Failed to deserialize rich text", error, {
3809
+ fieldPath: context.fieldPath
3810
+ }), originalValue;
3811
+ }
3812
+ }
3813
+ }, vttCaptionTransform = {
3814
+ matcher: {
3815
+ fieldName: "captions",
3816
+ schemaType: "video"
3817
+ },
3818
+ toTranslatable: (captionsArray, context) => {
3819
+ if (devLog.info("VTT captions toTranslatable - input:", {
3820
+ fieldPath: context.fieldPath,
3821
+ isArray: Array.isArray(captionsArray),
3822
+ count: Array.isArray(captionsArray) ? captionsArray.length : 0,
3823
+ firstCaption: Array.isArray(captionsArray) && captionsArray.length > 0 ? captionsArray[0] : null
3824
+ }), !Array.isArray(captionsArray))
3825
+ return devLog.debug("VTT caption field is not an array, returning empty transform", {
3826
+ fieldPath: context.fieldPath,
3827
+ value: captionsArray
3828
+ }), {
3829
+ _type: "translatable.vtt.captions",
3830
+ translationText: "",
3831
+ _originalMetadata: []
3832
+ };
3833
+ devLog.info("Applying VTT caption hybrid transform", {
3834
+ fieldPath: context.fieldPath,
3835
+ count: captionsArray.length
3836
+ });
3837
+ const translationText = serializeVTTCaptionText(captionsArray), originalMetadata = captionsArray.map((caption, index) => {
3838
+ const { text: _text, ...metadata } = caption;
3839
+ return {
3840
+ ...metadata,
3841
+ _originalIndex: index
3842
+ };
3843
+ });
3844
+ return devLog.info("VTT captions serialized:", {
3845
+ fieldPath: context.fieldPath,
3846
+ translationTextLength: translationText.length,
3847
+ translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
3848
+ metadataCount: originalMetadata.length,
3849
+ metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
3850
+ }), {
3851
+ _type: "translatable.vtt.captions",
3852
+ translationText,
3853
+ _originalMetadata: originalMetadata
3854
+ };
3855
+ },
3856
+ fromTranslatable: (translatedValue, _originalValue, context) => {
3857
+ if (devLog.info("VTT captions fromTranslatable - input:", {
3858
+ fieldPath: context.fieldPath,
3859
+ valueType: typeof translatedValue,
3860
+ isCorrectType: translatedValue._type === "translatable.vtt.captions",
3861
+ hasText: !!translatedValue.translationText,
3862
+ hasMetadata: Array.isArray(translatedValue._originalMetadata)
3863
+ }), !translatedValue || translatedValue._type !== "translatable.vtt.captions" || !translatedValue.translationText || !Array.isArray(translatedValue._originalMetadata))
3864
+ return devLog.debug("Value is not a translatable VTT caption object, skipping transform", {
3865
+ fieldPath: context.fieldPath,
3866
+ valueType: typeof translatedValue,
3867
+ translatedValue
3868
+ }), [];
3869
+ devLog.info("Processing translated VTT captions", {
3870
+ fieldPath: context.fieldPath,
3871
+ metadataCount: translatedValue._originalMetadata.length
3872
+ });
3873
+ const { translationText, _originalMetadata } = translatedValue;
3874
+ devLog.info("VTT captions translationText:", {
3875
+ fieldPath: context.fieldPath,
3876
+ translationTextLength: translationText.length,
3877
+ translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
3878
+ });
3879
+ const result = deserializeVTTCaptionText(translationText, _originalMetadata);
3880
+ return devLog.info("VTT captions final result:", {
3881
+ fieldPath: context.fieldPath,
3882
+ resultCount: result.length,
3883
+ resultExample: result.length > 0 ? result[0] : null
3884
+ }), result;
3885
+ }
3886
+ };
3887
+ function serializeVTTCaptionText(captions) {
3888
+ if (devLog.debug("serializeVTTCaptionText - input:", {
3889
+ captionsCount: captions.length,
3890
+ firstCaption: captions.length > 0 ? captions[0] : null
3891
+ }), !Array.isArray(captions) || captions.length === 0)
3892
+ return "";
3893
+ const result = captions.map((caption, index) => (devLog.debug(`Processing caption ${index}:`, {
3894
+ text: caption.text,
3895
+ hasText: typeof caption.text == "string"
3896
+ }), `[${index}] ${caption.text || ""}`)).join(`
3897
+ `);
3898
+ return devLog.debug("serializeVTTCaptionText - output:", {
3899
+ resultLength: result.length,
3900
+ resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
3901
+ }), result;
3902
+ }
3903
+ function deserializeVTTCaptionText(translationText, originalMetadata) {
3904
+ if (devLog.debug("deserializeVTTCaptionText - input:", {
3905
+ translationTextLength: translationText.length,
3906
+ translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
3907
+ metadataCount: originalMetadata.length,
3908
+ metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
3909
+ }), !translationText || translationText.trim() === "")
3910
+ return [];
3911
+ const result = [], lines = translationText.split(`
3912
+ `), lineRegex = /^\[(\d+)\]\s*(.*)$/;
3913
+ for (const line of lines) {
3914
+ const match = line.match(lineRegex);
3915
+ if (!match) {
3916
+ devLog.warn("Failed to parse caption line", { line });
3917
+ continue;
3918
+ }
3919
+ const index = Number.parseInt(match[1], 10), text = match[2];
3920
+ devLog.debug("Found caption:", {
3921
+ index,
3922
+ text
3923
+ });
3924
+ const metadata = originalMetadata.find((m) => m._originalIndex === index);
3925
+ if (metadata) {
3926
+ devLog.debug(`Found matching metadata for index ${index}`);
3927
+ const { _originalIndex, ...metadataWithoutIndex } = metadata;
3928
+ result.push({
3929
+ ...metadataWithoutIndex,
3930
+ text
3931
+ });
3932
+ } else
3933
+ devLog.warn("Missing metadata for caption", { index }), result.push({
3934
+ _type: "vtt.cue",
3935
+ _key: `caption-${index}`,
3936
+ text,
3937
+ startTime: 0,
3938
+ endTime: 0
3939
+ });
3940
+ }
3941
+ return result.length < originalMetadata.length && devLog.warn(`Missing captions in translation: found ${result.length} out of ${originalMetadata.length}`), devLog.debug("deserializeVTTCaptionText - output:", {
3942
+ resultCount: result.length,
3943
+ resultExample: result.length > 0 ? result[0] : null
3944
+ }), result;
3945
+ }
3946
+ const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
3947
+ globalTransformRegistry.register(transform);
3948
+ })), {
3240
3949
  name: "sanity-plugin-ai-translation",
3241
3950
  document: {
3242
3951
  actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
@@ -3247,6 +3956,8 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3247
3956
  })), translationHistorySchema = translationHistory;
3248
3957
  export {
3249
3958
  aiTranslationPlugin,
3250
- translationHistorySchema
3959
+ richTextTransform,
3960
+ translationHistorySchema,
3961
+ vttCaptionTransform
3251
3962
  };
3252
3963
  //# sourceMappingURL=index.js.map