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

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(
@@ -1848,16 +1696,189 @@ const fetchTranslationPrompts = async ({
1848
1696
  status
1849
1697
  })
1850
1698
  });
1851
- }, callGPT5 = async (apiKey, request) => (request.model || "gpt-5") === "gpt-5" ? await callGPT5ResponsesAPI(apiKey, request) : await callChatCompletionsAPI(apiKey, request), callGPT5ResponsesAPI = async (apiKey, request) => {
1699
+ };
1700
+ function applyTransforms({
1701
+ document,
1702
+ registry,
1703
+ schemaType,
1704
+ parentPath = ""
1705
+ }) {
1706
+ if (!document)
1707
+ return { transformedDocument: document, originalStructure: {} };
1708
+ if (typeof document != "object")
1709
+ return { transformedDocument: document, originalStructure: {} };
1710
+ if (Array.isArray(document)) {
1711
+ const originalStructure2 = {};
1712
+ return { transformedDocument: document.map((item, index) => {
1713
+ const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`, { transformedDocument: transformedItem, originalStructure: itemOriginalStructure } = applyTransforms({
1714
+ document: item,
1715
+ registry,
1716
+ schemaType,
1717
+ parentPath: itemPath
1718
+ });
1719
+ return Object.assign(originalStructure2, itemOriginalStructure), transformedItem;
1720
+ }), originalStructure: originalStructure2 };
1721
+ }
1722
+ const transformedObj = {}, originalStructure = {};
1723
+ for (const [key, value] of Object.entries(document)) {
1724
+ const fieldPath = parentPath ? `${parentPath}.${key}` : key;
1725
+ if (key.startsWith("_") && key !== "_type") {
1726
+ transformedObj[key] = value;
1727
+ continue;
1728
+ }
1729
+ const transform = registry.findTransform(schemaType, fieldPath, value, document);
1730
+ if (transform) {
1731
+ devLog.debug("Applying transform", { fieldPath, schemaType });
1732
+ const context = {
1733
+ fieldPath,
1734
+ schemaType,
1735
+ document
1736
+ }, transformedValue = transform.toTranslatable(value, context);
1737
+ transformedObj[key] = transformedValue, originalStructure[fieldPath] = {
1738
+ value,
1739
+ transform
1740
+ };
1741
+ } else {
1742
+ const { transformedDocument: transformedValue, originalStructure: valueOriginalStructure } = applyTransforms({
1743
+ document: value,
1744
+ registry,
1745
+ schemaType,
1746
+ parentPath: fieldPath
1747
+ });
1748
+ Object.assign(originalStructure, valueOriginalStructure), transformedObj[key] = transformedValue;
1749
+ }
1750
+ }
1751
+ return { transformedDocument: transformedObj, originalStructure };
1752
+ }
1753
+ function restoreTransforms({
1754
+ translatedDocument,
1755
+ originalStructure,
1756
+ registry,
1757
+ schemaType,
1758
+ parentPath = ""
1759
+ }) {
1760
+ if (translatedDocument == null || typeof translatedDocument != "object")
1761
+ return translatedDocument;
1762
+ if (Array.isArray(translatedDocument))
1763
+ return translatedDocument.map((item, index) => {
1764
+ const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`;
1765
+ return restoreTransforms({
1766
+ translatedDocument: item,
1767
+ originalStructure,
1768
+ registry,
1769
+ schemaType,
1770
+ parentPath: itemPath
1771
+ });
1772
+ });
1773
+ const originalData = originalStructure[parentPath];
1774
+ if (originalData) {
1775
+ const { value: originalValue, transform } = originalData, context = {
1776
+ fieldPath: parentPath,
1777
+ schemaType,
1778
+ document: translatedDocument
1779
+ };
1780
+ return devLog.debug("Restoring transform", { fieldPath: parentPath, schemaType }), transform.fromTranslatable(translatedDocument, originalValue, context);
1781
+ }
1782
+ const restoredObj = {}, translatedObj = translatedDocument;
1783
+ for (const [key, value] of Object.entries(translatedObj)) {
1784
+ const fieldPath = parentPath ? `${parentPath}.${key}` : key, fieldOriginalData = originalStructure[fieldPath];
1785
+ if (fieldOriginalData) {
1786
+ const { value: originalValue, transform } = fieldOriginalData, context = {
1787
+ fieldPath,
1788
+ schemaType,
1789
+ document: translatedDocument
1790
+ };
1791
+ devLog.debug("Restoring transform for field", { fieldPath, schemaType }), restoredObj[key] = transform.fromTranslatable(value, originalValue, context);
1792
+ } else
1793
+ restoredObj[key] = restoreTransforms({
1794
+ translatedDocument: value,
1795
+ originalStructure,
1796
+ registry,
1797
+ schemaType,
1798
+ parentPath: fieldPath
1799
+ });
1800
+ }
1801
+ return restoredObj;
1802
+ }
1803
+ class TransformRegistry {
1804
+ transforms = [];
1805
+ /**
1806
+ * Register a transform with the registry.
1807
+ * @param transform - Field transform to register
1808
+ */
1809
+ register(transform) {
1810
+ devLog.debug("Registering field transform", {
1811
+ matcher: transform.matcher
1812
+ }), this.transforms.push(transform);
1813
+ }
1814
+ /**
1815
+ * Find a matching transform for the given field.
1816
+ * If multiple transforms match, returns the first one (order matters!).
1817
+ * @param schemaType - Document schema type
1818
+ * @param fieldPath - Path to the field
1819
+ * @param value - Optional field value for value-based matching
1820
+ * @param document - Optional full document for context
1821
+ * @returns Matching transform or undefined if none found
1822
+ */
1823
+ findTransform(schemaType, fieldPath, value, document) {
1824
+ const matches = [];
1825
+ for (const transform of this.transforms) {
1826
+ const { matcher } = transform;
1827
+ if (!(matcher.schemaType && matcher.schemaType !== schemaType)) {
1828
+ if (matcher.fieldPath) {
1829
+ if (typeof matcher.fieldPath == "string") {
1830
+ if (matcher.fieldPath !== fieldPath)
1831
+ continue;
1832
+ } else if (matcher.fieldPath instanceof RegExp && !matcher.fieldPath.test(fieldPath))
1833
+ continue;
1834
+ }
1835
+ if (matcher.fieldName) {
1836
+ const segments = fieldPath.split(".");
1837
+ if (segments[segments.length - 1]?.split("[")[0] !== matcher.fieldName)
1838
+ continue;
1839
+ }
1840
+ if (matcher.valueMatcher) {
1841
+ if (value === void 0)
1842
+ continue;
1843
+ const context = {
1844
+ fieldPath,
1845
+ schemaType,
1846
+ document: document || {}
1847
+ };
1848
+ if (!matcher.valueMatcher(value, context))
1849
+ continue;
1850
+ }
1851
+ matches.push(transform);
1852
+ }
1853
+ }
1854
+ return matches.length > 1 && devLog.warn("Multiple transforms match the same field - using first match", {
1855
+ schemaType,
1856
+ fieldPath,
1857
+ matchCount: matches.length,
1858
+ firstMatcher: matches[0].matcher
1859
+ }), matches[0];
1860
+ }
1861
+ }
1862
+ 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
1863
  const model = request.model || "gpt-5", requestBody = {
1853
- model,
1864
+ model: "gpt-5.1",
1854
1865
  input: request.messages
1855
1866
  // Uses 'input' instead of 'messages'
1856
1867
  };
1857
1868
  request.reasoningEffort && (requestBody.reasoning = {
1858
1869
  effort: request.reasoningEffort,
1859
1870
  summary: "auto"
1860
- // Request reasoning summaries
1871
+ // Enable automatic reasoning summaries
1872
+ }), devLog.debug("OpenAI GPT-5 Request", {
1873
+ model: requestBody.model,
1874
+ reasoningEffort: request.reasoningEffort,
1875
+ messages: request.messages.map((m) => ({
1876
+ role: m.role,
1877
+ contentLength: m.content.length,
1878
+ contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
1879
+ }))
1880
+ }), devLog.debug("OpenAI GPT-5 Full Prompt", {
1881
+ messages: request.messages
1861
1882
  });
1862
1883
  const response = await fetch("https://api.openai.com/v1/responses", {
1863
1884
  method: "POST",
@@ -1892,7 +1913,19 @@ const fetchTranslationPrompts = async ({
1892
1913
  model,
1893
1914
  messages: request.messages,
1894
1915
  temperature: request.temperature ?? 0.3
1895
- }, response = await fetch("https://api.openai.com/v1/chat/completions", {
1916
+ };
1917
+ devLog.debug("OpenAI Chat Completions Request", {
1918
+ model,
1919
+ temperature: request.temperature ?? 0.3,
1920
+ messages: request.messages.map((m) => ({
1921
+ role: m.role,
1922
+ contentLength: m.content.length,
1923
+ contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
1924
+ }))
1925
+ }), devLog.debug("OpenAI Chat Completions Full Prompt", {
1926
+ messages: request.messages
1927
+ });
1928
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
1896
1929
  method: "POST",
1897
1930
  headers: {
1898
1931
  "Content-Type": "application/json",
@@ -1945,9 +1978,52 @@ const fetchTranslationPrompts = async ({
1945
1978
  }, parseReviewFormat = (text) => {
1946
1979
  const root = [], stack = [], lines = text.split(`
1947
1980
  `);
1948
- for (const line of lines) {
1949
- const trimmed = line.trim();
1981
+ for (let i = 0; i < lines.length; i++) {
1982
+ const trimmed = lines[i].trim();
1950
1983
  if (!trimmed) continue;
1984
+ const compactStringMatch = trimmed.match(/^(\w+):\s*(.*)$/);
1985
+ if (compactStringMatch && !trimmed.includes("<")) {
1986
+ const [, fieldName, text2] = compactStringMatch, node = {
1987
+ type: "field",
1988
+ name: fieldName,
1989
+ text: unescapeXml(text2),
1990
+ attributes: {},
1991
+ children: []
1992
+ };
1993
+ if (stack.length > 0) {
1994
+ const parent = stack[stack.length - 1];
1995
+ parent && parent.children.push(node);
1996
+ } else
1997
+ root.push(node);
1998
+ continue;
1999
+ }
2000
+ const compactStartMatch = trimmed.match(/^---\s+(\w+)\s+---$/);
2001
+ if (compactStartMatch) {
2002
+ const fieldName = compactStartMatch[1], contentLines = [];
2003
+ for (i++; i < lines.length; ) {
2004
+ const currentLine = lines[i];
2005
+ if (currentLine.trim().match(new RegExp(`^---\\s+/${fieldName}\\s+---$`)))
2006
+ break;
2007
+ contentLines.push(currentLine), i++;
2008
+ }
2009
+ const content = contentLines.join(`
2010
+ `);
2011
+ let contentType = "translatable.vtt.captions";
2012
+ (content.includes("<block") || content.includes("<span")) && (contentType = "translatable.richtext");
2013
+ const node = {
2014
+ type: "field",
2015
+ name: fieldName,
2016
+ text: content,
2017
+ attributes: { type: contentType },
2018
+ children: []
2019
+ };
2020
+ if (stack.length > 0) {
2021
+ const parent = stack[stack.length - 1];
2022
+ parent && parent.children.push(node);
2023
+ } else
2024
+ root.push(node);
2025
+ continue;
2026
+ }
1951
2027
  const openMatch = trimmed.match(/^<(field|block|span)([^>]*)>(.*)$/);
1952
2028
  if (openMatch) {
1953
2029
  const [, tagName, attrsStr, content] = openMatch, attributes = parseAttributes(attrsStr), node = {
@@ -1970,7 +2046,21 @@ const fetchTranslationPrompts = async ({
1970
2046
  parent && parent.children.push(node);
1971
2047
  } else
1972
2048
  root.push(node);
1973
- else
2049
+ else if (attributes.type === "translatable.vtt.captions" || attributes.type === "translatable.richtext") {
2050
+ const contentLines = [];
2051
+ for (i++; i < lines.length; ) {
2052
+ const currentLine = lines[i];
2053
+ if (currentLine.trim().match(/^<\/field>$/))
2054
+ break;
2055
+ contentLines.push(currentLine), i++;
2056
+ }
2057
+ if (node.text = contentLines.join(`
2058
+ `), stack.length > 0) {
2059
+ const parent = stack[stack.length - 1];
2060
+ parent && parent.children.push(node);
2061
+ } else
2062
+ root.push(node);
2063
+ } else
1974
2064
  stack.push(node);
1975
2065
  continue;
1976
2066
  }
@@ -1998,6 +2088,16 @@ const fetchTranslationPrompts = async ({
1998
2088
  applyFieldChanges(doc, path, node);
1999
2089
  }
2000
2090
  }, applyFieldChanges = (doc, path, node) => {
2091
+ if (node.attributes.type === "translatable.vtt.captions") {
2092
+ const fieldValue = get(doc, path);
2093
+ fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.vtt.captions" && node.text && set(doc, [...path, "translationText"], node.text);
2094
+ return;
2095
+ }
2096
+ if (node.attributes.type === "translatable.richtext") {
2097
+ const fieldValue = get(doc, path);
2098
+ fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.richtext" && node.text && set(doc, [...path, "translationText"], node.text);
2099
+ return;
2100
+ }
2001
2101
  if (node.attributes.type === "array") {
2002
2102
  node.children.forEach((child, idx) => {
2003
2103
  applyFieldChanges(doc, [...path, idx], child);
@@ -2064,7 +2164,7 @@ const fetchTranslationPrompts = async ({
2064
2164
  if (path.length === 0) {
2065
2165
  if (isPlainObject(value))
2066
2166
  for (const [key, val] of Object.entries(value))
2067
- !key.startsWith("_") && key !== "language" && serialize(val, [key], indent);
2167
+ !key.startsWith("_") && key !== "language" && serialize(val, [key], 0);
2068
2168
  return;
2069
2169
  }
2070
2170
  const fieldName = path[path.length - 1];
@@ -2077,6 +2177,14 @@ const fetchTranslationPrompts = async ({
2077
2177
  return;
2078
2178
  }
2079
2179
  if (isPlainObject(value)) {
2180
+ if (value._type === "translatable.vtt.captions" && typeof value.translationText == "string") {
2181
+ lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
2182
+ return;
2183
+ }
2184
+ if (value._type === "translatable.richtext" && typeof value.translationText == "string") {
2185
+ lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
2186
+ return;
2187
+ }
2080
2188
  lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
2081
2189
  for (const [key, val] of Object.entries(value))
2082
2190
  key.startsWith("_") || serialize(val, [...path, key], indent + 1);
@@ -2084,7 +2192,7 @@ const fetchTranslationPrompts = async ({
2084
2192
  return;
2085
2193
  }
2086
2194
  if (typeof value == "string") {
2087
- value.length > 0 && lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
2195
+ value.length > 0 && lines.push(`${fieldName}: ${escapeXml(value)}`);
2088
2196
  return;
2089
2197
  }
2090
2198
  (typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
@@ -2217,7 +2325,14 @@ const fetchTranslationPrompts = async ({
2217
2325
  sourceLanguage
2218
2326
  };
2219
2327
  try {
2220
- const sourceText = serializeDocument(sourceDocument), prompt = buildTranslationPrompt$1({
2328
+ const sourceText = serializeDocument(sourceDocument);
2329
+ devLog.info("Serialized document for OpenAI", {
2330
+ ...logContext,
2331
+ characterCount: sourceText.length,
2332
+ estimatedTokens: Math.ceil(sourceText.length / 4),
2333
+ preview: sourceText.substring(0, 500)
2334
+ });
2335
+ const prompt = buildTranslationPrompt$1({
2221
2336
  sourceText,
2222
2337
  sourceLanguage,
2223
2338
  targetLanguage,
@@ -2232,7 +2347,15 @@ const fetchTranslationPrompts = async ({
2232
2347
  ],
2233
2348
  model: openaiConfig.model,
2234
2349
  reasoningEffort: openaiConfig.reasoningEffort
2235
- }), result = deserializeDocument(sourceDocument, response.content);
2350
+ });
2351
+ devLog.info("OpenAI translation response received", {
2352
+ ...logContext,
2353
+ tokensUsed: response.tokensUsed,
2354
+ cost: response.cost,
2355
+ responseLength: response.content.length,
2356
+ responsePreview: response.content.substring(0, 500)
2357
+ });
2358
+ const result = deserializeDocument(sourceDocument, response.content);
2236
2359
  result[languageFieldName] = targetLanguage;
2237
2360
  const validation = validateStructureIntegrity(sourceDocument, result);
2238
2361
  if (!validation.valid) {
@@ -2424,11 +2547,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2424
2547
  openaiConfig,
2425
2548
  releaseId
2426
2549
  }) => {
2550
+ devLog.debug("Applying field transforms before translation", {
2551
+ documentId: sourceDocument._id,
2552
+ documentType: sourceDocument._type
2553
+ });
2554
+ const { transformedDocument, originalStructure } = applyTransforms({
2555
+ document: sourceDocument,
2556
+ registry: globalTransformRegistry,
2557
+ schemaType: sourceDocument._type
2558
+ });
2427
2559
  let translatedDoc;
2428
2560
  switch (translationMode) {
2429
2561
  case "sanity-only": {
2430
2562
  setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
2431
- sourceDocument,
2563
+ sourceDocument: transformedDocument,
2432
2564
  targetLanguage,
2433
2565
  languageFieldName,
2434
2566
  schemaId,
@@ -2442,7 +2574,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2442
2574
  throw new Error("OpenAI is not configured but openai-only mode was selected");
2443
2575
  setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), translatedDoc = await getOpenAITranslation({
2444
2576
  dispatch,
2445
- sourceDocument,
2577
+ sourceDocument: transformedDocument,
2446
2578
  targetLanguage,
2447
2579
  languageFieldName,
2448
2580
  translationPrompts,
@@ -2457,8 +2589,17 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2457
2589
  }
2458
2590
  if (!translatedDoc)
2459
2591
  return null;
2460
- const targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
2461
- ...translatedDoc,
2592
+ devLog.debug("Restoring field transforms after translation", {
2593
+ documentId: sourceDocument._id,
2594
+ documentType: sourceDocument._type
2595
+ });
2596
+ const finalDoc = restoreTransforms({
2597
+ translatedDocument: translatedDoc,
2598
+ originalStructure,
2599
+ registry: globalTransformRegistry,
2600
+ schemaType: sourceDocument._type
2601
+ }), targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
2602
+ ...finalDoc,
2462
2603
  _id: targetDocumentVersionId
2463
2604
  };
2464
2605
  return await client.action({
@@ -2588,7 +2729,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2588
2729
  {
2589
2730
  _id: sourceDocument._id,
2590
2731
  _type: sourceDocument._type,
2591
- // TODO: Handle this properly
2732
+ // TODO: Extract document title more robustly across schema types
2592
2733
  title: sourceDocument.name || sourceDocument.title
2593
2734
  },
2594
2735
  ...references
@@ -3236,7 +3377,427 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3236
3377
  content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
3237
3378
  }
3238
3379
  } : (devLog.error("Missing config in TranslateAction"), null);
3239
- }, aiTranslationPlugin = definePlugin((config) => ({
3380
+ }, translationGlossaryTerm = defineType({
3381
+ name: "translationGlossaryTerm",
3382
+ title: "Translation Glossary Term",
3383
+ icon: TranslateIcon,
3384
+ type: "document",
3385
+ fields: [
3386
+ defineField({
3387
+ name: "translations",
3388
+ title: "Translations",
3389
+ type: "internationalizedArrayString"
3390
+ })
3391
+ ],
3392
+ preview: {
3393
+ select: {
3394
+ translations: "translations"
3395
+ },
3396
+ prepare({ translations }) {
3397
+ const enTranslation = translations?.find((t) => t.locale === "en-us"), firstTranslation = translations?.[0];
3398
+ return {
3399
+ title: enTranslation?.value || firstTranslation?.value || "No translation available"
3400
+ };
3401
+ }
3402
+ }
3403
+ }), translationHistory = defineField({
3404
+ name: "translationHistory",
3405
+ type: "array",
3406
+ of: [
3407
+ {
3408
+ name: "targetLanguageHistory",
3409
+ type: "object",
3410
+ fields: [
3411
+ defineField({
3412
+ name: "language",
3413
+ title: "Language",
3414
+ description: "The language of the target document",
3415
+ type: "string"
3416
+ }),
3417
+ defineField({
3418
+ name: "sourceId",
3419
+ title: "Source ID",
3420
+ description: "The ID of the source document",
3421
+ type: "string"
3422
+ }),
3423
+ defineField({
3424
+ name: "sourceRev",
3425
+ title: "Source Revision",
3426
+ description: "The revision ID of the source document at the time of translation",
3427
+ type: "string"
3428
+ }),
3429
+ defineField({
3430
+ name: "sourceLanguage",
3431
+ title: "Source Language",
3432
+ description: "The language of the source document",
3433
+ type: "string"
3434
+ }),
3435
+ defineField({
3436
+ name: "sourceSnapshot",
3437
+ title: "Source Snapshot",
3438
+ description: "The snapshot of the source document at the time of translation",
3439
+ type: "text"
3440
+ }),
3441
+ defineField({
3442
+ name: "targetId",
3443
+ title: "Target ID",
3444
+ description: "The ID of the target document",
3445
+ type: "string"
3446
+ })
3447
+ ]
3448
+ }
3449
+ ]
3450
+ }), translationSettings = defineType({
3451
+ name: "translationSettings",
3452
+ title: "Translation Settings",
3453
+ icon: TranslateIcon,
3454
+ type: "document",
3455
+ groups: [
3456
+ {
3457
+ name: "general",
3458
+ title: "General",
3459
+ default: !0
3460
+ },
3461
+ {
3462
+ name: "history",
3463
+ title: "History"
3464
+ }
3465
+ ],
3466
+ fields: [
3467
+ defineField({
3468
+ name: "globalPrompt",
3469
+ title: "Global Translation Prompt",
3470
+ description: "A prompt that will given to the Sanity AI Agent for all translation tasks.",
3471
+ type: "text",
3472
+ rows: 5,
3473
+ group: "general"
3474
+ }),
3475
+ defineField({
3476
+ name: "localePrompts",
3477
+ title: "Locale Prompts",
3478
+ description: "Prompts that will be given to the Sanity AI Agent for each locale.",
3479
+ type: "internationalizedArrayText",
3480
+ group: "general"
3481
+ }),
3482
+ defineField({
3483
+ name: "protectedPhrases",
3484
+ title: "Protected Phrases",
3485
+ description: "Phrases that will never be translated.",
3486
+ type: "array",
3487
+ of: [{ type: "string" }],
3488
+ group: "general"
3489
+ }),
3490
+ defineField({
3491
+ name: "changeHistory",
3492
+ title: "Change History",
3493
+ description: "History of changes made via user feedback.",
3494
+ type: "array",
3495
+ readOnly: !0,
3496
+ group: "history",
3497
+ of: [
3498
+ {
3499
+ type: "object",
3500
+ fields: [
3501
+ defineField({
3502
+ name: "timestamp",
3503
+ title: "Timestamp",
3504
+ type: "datetime"
3505
+ }),
3506
+ defineField({
3507
+ name: "changeType",
3508
+ title: "Change Type",
3509
+ type: "string",
3510
+ options: {
3511
+ list: [
3512
+ { title: "Global Prompt", value: "globalPrompt" },
3513
+ { title: "Locale Prompt", value: "localePrompt" },
3514
+ { title: "Protected Phrase", value: "protectedPhrase" },
3515
+ { title: "Glossary Term", value: "glossaryTerm" }
3516
+ ]
3517
+ }
3518
+ }),
3519
+ defineField({
3520
+ name: "description",
3521
+ title: "Description",
3522
+ type: "text",
3523
+ rows: 3
3524
+ }),
3525
+ defineField({
3526
+ name: "locale",
3527
+ title: "Locale",
3528
+ type: "string",
3529
+ description: "Target locale for locale-specific changes"
3530
+ })
3531
+ ],
3532
+ preview: {
3533
+ select: {
3534
+ timestamp: "timestamp",
3535
+ changeType: "changeType",
3536
+ description: "description"
3537
+ },
3538
+ prepare({ timestamp, changeType, description }) {
3539
+ return {
3540
+ title: `${changeType} - ${new Date(timestamp).toLocaleDateString()}`,
3541
+ subtitle: description
3542
+ };
3543
+ }
3544
+ }
3545
+ }
3546
+ ]
3547
+ })
3548
+ ],
3549
+ preview: {
3550
+ prepare: () => ({
3551
+ title: "Translation Settings"
3552
+ })
3553
+ }
3554
+ });
3555
+ function isPortableText(value) {
3556
+ return Array.isArray(value) && value.some((item) => item?._type === "block");
3557
+ }
3558
+ const richTextTransform = {
3559
+ // Matcher combines field name pattern with value-based detection
3560
+ // fieldPath pattern acts as a pre-filter for performance
3561
+ // valueMatcher definitively checks if the value is actually Portable Text
3562
+ matcher: {
3563
+ // Pre-filter: Match all fields to enable detection based solely on value
3564
+ fieldPath: /.*/,
3565
+ // Definitive check: Verify the value is actually Portable Text
3566
+ valueMatcher: (value) => Array.isArray(value) && isPortableText(value)
3567
+ },
3568
+ toTranslatable: (value, context) => {
3569
+ if (!Array.isArray(value) || !isPortableText(value))
3570
+ return {
3571
+ _type: "translatable.richtext",
3572
+ translationText: JSON.stringify(value),
3573
+ _originalValue: value
3574
+ };
3575
+ devLog.info("Applying rich text transform with XML serialization", {
3576
+ fieldPath: context.fieldPath,
3577
+ blockCount: value.length
3578
+ });
3579
+ try {
3580
+ const tempDoc = {
3581
+ _id: "temp",
3582
+ _type: "richTextWrapper",
3583
+ _createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3584
+ _updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3585
+ _rev: "temp",
3586
+ content: value
3587
+ }, serialized = serializeDocument(tempDoc);
3588
+ devLog.debug("Serialized rich text document", {
3589
+ fieldPath: context.fieldPath,
3590
+ serializedLength: serialized.length,
3591
+ preview: serialized.substring(0, 200)
3592
+ });
3593
+ const contentRegex = /<field name="content"[^>]*>([\s\S]*)<\/field>/s, match = serialized.match(contentRegex);
3594
+ if (match?.[1]) {
3595
+ const result = {
3596
+ _type: "translatable.richtext",
3597
+ translationText: match[1].trim(),
3598
+ _originalValue: value
3599
+ };
3600
+ return devLog.debug("Successfully extracted and wrapped rich text", {
3601
+ fieldPath: context.fieldPath,
3602
+ translationTextLength: result.translationText.length
3603
+ }), result;
3604
+ }
3605
+ return devLog.warn("Failed to serialize rich text content - regex did not match", {
3606
+ fieldPath: context.fieldPath,
3607
+ serializedPreview: serialized.substring(0, 500)
3608
+ }), {
3609
+ _type: "translatable.richtext",
3610
+ translationText: JSON.stringify(value),
3611
+ _originalValue: value
3612
+ };
3613
+ } catch (error) {
3614
+ return devLog.error("Error serializing rich text", error, {
3615
+ fieldPath: context.fieldPath
3616
+ }), {
3617
+ _type: "translatable.richtext",
3618
+ translationText: JSON.stringify(value),
3619
+ _originalValue: value
3620
+ };
3621
+ }
3622
+ },
3623
+ fromTranslatable: (translatedValue, originalValue, context) => {
3624
+ if (!translatedValue || translatedValue._type !== "translatable.richtext" || !translatedValue.translationText)
3625
+ return devLog.debug("Value is not a translatable rich text object, returning original", {
3626
+ fieldPath: context.fieldPath,
3627
+ hasCorrectType: translatedValue._type === "translatable.richtext"
3628
+ }), originalValue;
3629
+ devLog.info("Deserializing rich text content", {
3630
+ fieldPath: context.fieldPath,
3631
+ translationTextLength: translatedValue.translationText.length
3632
+ });
3633
+ try {
3634
+ const { translationText, _originalValue } = translatedValue, wrappedSerialized = `<field name="content">
3635
+ ${translationText}
3636
+ </field>`;
3637
+ devLog.debug("Prepared wrapped serialized content", {
3638
+ fieldPath: context.fieldPath,
3639
+ wrappedLength: wrappedSerialized.length,
3640
+ preview: wrappedSerialized.substring(0, 300)
3641
+ });
3642
+ const tempOriginalDoc = {
3643
+ _id: "temp",
3644
+ _type: "richTextWrapper",
3645
+ _createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3646
+ _updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3647
+ _rev: "temp",
3648
+ content: originalValue
3649
+ }, result = deserializeDocument(tempOriginalDoc, wrappedSerialized);
3650
+ return devLog.debug("Deserialization result", {
3651
+ fieldPath: context.fieldPath,
3652
+ hasContent: !!result.content,
3653
+ contentType: Array.isArray(result.content) ? "array" : typeof result.content,
3654
+ contentLength: Array.isArray(result.content) ? result.content.length : void 0
3655
+ }), result.content ? result.content : (devLog.warn("Deserialization returned no content, returning original", {
3656
+ fieldPath: context.fieldPath,
3657
+ resultKeys: Object.keys(result)
3658
+ }), originalValue);
3659
+ } catch (error) {
3660
+ return devLog.error("Failed to deserialize rich text", error, {
3661
+ fieldPath: context.fieldPath
3662
+ }), originalValue;
3663
+ }
3664
+ }
3665
+ }, vttCaptionTransform = {
3666
+ matcher: {
3667
+ fieldName: "captions",
3668
+ schemaType: "video"
3669
+ },
3670
+ toTranslatable: (captionsArray, context) => {
3671
+ if (devLog.info("VTT captions toTranslatable - input:", {
3672
+ fieldPath: context.fieldPath,
3673
+ isArray: Array.isArray(captionsArray),
3674
+ count: Array.isArray(captionsArray) ? captionsArray.length : 0,
3675
+ firstCaption: Array.isArray(captionsArray) && captionsArray.length > 0 ? captionsArray[0] : null
3676
+ }), !Array.isArray(captionsArray))
3677
+ return devLog.debug("VTT caption field is not an array, returning empty transform", {
3678
+ fieldPath: context.fieldPath,
3679
+ value: captionsArray
3680
+ }), {
3681
+ _type: "translatable.vtt.captions",
3682
+ translationText: "",
3683
+ _originalMetadata: []
3684
+ };
3685
+ devLog.info("Applying VTT caption hybrid transform", {
3686
+ fieldPath: context.fieldPath,
3687
+ count: captionsArray.length
3688
+ });
3689
+ const translationText = serializeVTTCaptionText(captionsArray), originalMetadata = captionsArray.map((caption, index) => {
3690
+ const { text: _text, ...metadata } = caption;
3691
+ return {
3692
+ ...metadata,
3693
+ _originalIndex: index
3694
+ };
3695
+ });
3696
+ return devLog.info("VTT captions serialized:", {
3697
+ fieldPath: context.fieldPath,
3698
+ translationTextLength: translationText.length,
3699
+ translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
3700
+ metadataCount: originalMetadata.length,
3701
+ metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
3702
+ }), {
3703
+ _type: "translatable.vtt.captions",
3704
+ translationText,
3705
+ _originalMetadata: originalMetadata
3706
+ };
3707
+ },
3708
+ fromTranslatable: (translatedValue, _originalValue, context) => {
3709
+ if (devLog.info("VTT captions fromTranslatable - input:", {
3710
+ fieldPath: context.fieldPath,
3711
+ valueType: typeof translatedValue,
3712
+ isCorrectType: translatedValue._type === "translatable.vtt.captions",
3713
+ hasText: !!translatedValue.translationText,
3714
+ hasMetadata: Array.isArray(translatedValue._originalMetadata)
3715
+ }), !translatedValue || translatedValue._type !== "translatable.vtt.captions" || !translatedValue.translationText || !Array.isArray(translatedValue._originalMetadata))
3716
+ return devLog.debug("Value is not a translatable VTT caption object, skipping transform", {
3717
+ fieldPath: context.fieldPath,
3718
+ valueType: typeof translatedValue,
3719
+ translatedValue
3720
+ }), [];
3721
+ devLog.info("Processing translated VTT captions", {
3722
+ fieldPath: context.fieldPath,
3723
+ metadataCount: translatedValue._originalMetadata.length
3724
+ });
3725
+ const { translationText, _originalMetadata } = translatedValue;
3726
+ devLog.info("VTT captions translationText:", {
3727
+ fieldPath: context.fieldPath,
3728
+ translationTextLength: translationText.length,
3729
+ translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
3730
+ });
3731
+ const result = deserializeVTTCaptionText(translationText, _originalMetadata);
3732
+ return devLog.info("VTT captions final result:", {
3733
+ fieldPath: context.fieldPath,
3734
+ resultCount: result.length,
3735
+ resultExample: result.length > 0 ? result[0] : null
3736
+ }), result;
3737
+ }
3738
+ };
3739
+ function serializeVTTCaptionText(captions) {
3740
+ if (devLog.debug("serializeVTTCaptionText - input:", {
3741
+ captionsCount: captions.length,
3742
+ firstCaption: captions.length > 0 ? captions[0] : null
3743
+ }), !Array.isArray(captions) || captions.length === 0)
3744
+ return "";
3745
+ const result = captions.map((caption, index) => (devLog.debug(`Processing caption ${index}:`, {
3746
+ text: caption.text,
3747
+ hasText: typeof caption.text == "string"
3748
+ }), `[${index}] ${caption.text || ""}`)).join(`
3749
+ `);
3750
+ return devLog.debug("serializeVTTCaptionText - output:", {
3751
+ resultLength: result.length,
3752
+ resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
3753
+ }), result;
3754
+ }
3755
+ function deserializeVTTCaptionText(translationText, originalMetadata) {
3756
+ if (devLog.debug("deserializeVTTCaptionText - input:", {
3757
+ translationTextLength: translationText.length,
3758
+ translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
3759
+ metadataCount: originalMetadata.length,
3760
+ metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
3761
+ }), !translationText || translationText.trim() === "")
3762
+ return [];
3763
+ const result = [], lines = translationText.split(`
3764
+ `), lineRegex = /^\[(\d+)\]\s*(.*)$/;
3765
+ for (const line of lines) {
3766
+ const match = line.match(lineRegex);
3767
+ if (!match) {
3768
+ devLog.warn("Failed to parse caption line", { line });
3769
+ continue;
3770
+ }
3771
+ const index = Number.parseInt(match[1], 10), text = match[2];
3772
+ devLog.debug("Found caption:", {
3773
+ index,
3774
+ text
3775
+ });
3776
+ const metadata = originalMetadata.find((m) => m._originalIndex === index);
3777
+ if (metadata) {
3778
+ devLog.debug(`Found matching metadata for index ${index}`);
3779
+ const { _originalIndex, ...metadataWithoutIndex } = metadata;
3780
+ result.push({
3781
+ ...metadataWithoutIndex,
3782
+ text
3783
+ });
3784
+ } else
3785
+ devLog.warn("Missing metadata for caption", { index }), result.push({
3786
+ _type: "vtt.cue",
3787
+ _key: `caption-${index}`,
3788
+ text,
3789
+ startTime: 0,
3790
+ endTime: 0
3791
+ });
3792
+ }
3793
+ return result.length < originalMetadata.length && devLog.warn(`Missing captions in translation: found ${result.length} out of ${originalMetadata.length}`), devLog.debug("deserializeVTTCaptionText - output:", {
3794
+ resultCount: result.length,
3795
+ resultExample: result.length > 0 ? result[0] : null
3796
+ }), result;
3797
+ }
3798
+ const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
3799
+ globalTransformRegistry.register(transform);
3800
+ })), {
3240
3801
  name: "sanity-plugin-ai-translation",
3241
3802
  document: {
3242
3803
  actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
@@ -3247,6 +3808,8 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3247
3808
  })), translationHistorySchema = translationHistory;
3248
3809
  export {
3249
3810
  aiTranslationPlugin,
3250
- translationHistorySchema
3811
+ richTextTransform,
3812
+ translationHistorySchema,
3813
+ vttCaptionTransform
3251
3814
  };
3252
3815
  //# sourceMappingURL=index.js.map