@braze/sanity-plugin-ai-translation 0.2.25 → 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.d.ts +302 -2
- package/dist/index.js +972 -407
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,16 +1,68 @@
|
|
|
1
|
-
import { useClient,
|
|
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,
|
|
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
|
|
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
|
-
}),
|
|
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
|
-
},
|
|
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
|
|
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
|
-
},
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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[
|
|
1412
|
+
localePrompts[_key in $targetLanguages],
|
|
1587
1413
|
protectedPhrases
|
|
1588
1414
|
}`,
|
|
1589
1415
|
{ targetLanguages }
|
|
1590
|
-
)
|
|
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 && (
|
|
1611
|
-
|
|
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
|
-
}
|
|
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
|
-
//
|
|
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
|
-
}
|
|
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 (
|
|
1949
|
-
const trimmed =
|
|
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],
|
|
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(`${
|
|
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)
|
|
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,17 @@ const fetchTranslationPrompts = async ({
|
|
|
2232
2347
|
],
|
|
2233
2348
|
model: openaiConfig.model,
|
|
2234
2349
|
reasoningEffort: openaiConfig.reasoningEffort
|
|
2235
|
-
})
|
|
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);
|
|
2359
|
+
result[languageFieldName] = targetLanguage;
|
|
2360
|
+
const validation = validateStructureIntegrity(sourceDocument, result);
|
|
2236
2361
|
if (!validation.valid) {
|
|
2237
2362
|
devLog.error("Structure validation failed", {
|
|
2238
2363
|
...logContext,
|
|
@@ -2422,11 +2547,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2422
2547
|
openaiConfig,
|
|
2423
2548
|
releaseId
|
|
2424
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
|
+
});
|
|
2425
2559
|
let translatedDoc;
|
|
2426
2560
|
switch (translationMode) {
|
|
2427
2561
|
case "sanity-only": {
|
|
2428
2562
|
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
|
|
2429
|
-
sourceDocument,
|
|
2563
|
+
sourceDocument: transformedDocument,
|
|
2430
2564
|
targetLanguage,
|
|
2431
2565
|
languageFieldName,
|
|
2432
2566
|
schemaId,
|
|
@@ -2440,7 +2574,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2440
2574
|
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2441
2575
|
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), translatedDoc = await getOpenAITranslation({
|
|
2442
2576
|
dispatch,
|
|
2443
|
-
sourceDocument,
|
|
2577
|
+
sourceDocument: transformedDocument,
|
|
2444
2578
|
targetLanguage,
|
|
2445
2579
|
languageFieldName,
|
|
2446
2580
|
translationPrompts,
|
|
@@ -2455,8 +2589,17 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2455
2589
|
}
|
|
2456
2590
|
if (!translatedDoc)
|
|
2457
2591
|
return null;
|
|
2458
|
-
|
|
2459
|
-
|
|
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,
|
|
2460
2603
|
_id: targetDocumentVersionId
|
|
2461
2604
|
};
|
|
2462
2605
|
return await client.action({
|
|
@@ -2586,7 +2729,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2586
2729
|
{
|
|
2587
2730
|
_id: sourceDocument._id,
|
|
2588
2731
|
_type: sourceDocument._type,
|
|
2589
|
-
// TODO:
|
|
2732
|
+
// TODO: Extract document title more robustly across schema types
|
|
2590
2733
|
title: sourceDocument.name || sourceDocument.title
|
|
2591
2734
|
},
|
|
2592
2735
|
...references
|
|
@@ -3234,7 +3377,427 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3234
3377
|
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
|
|
3235
3378
|
}
|
|
3236
3379
|
} : (devLog.error("Missing config in TranslateAction"), null);
|
|
3237
|
-
},
|
|
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
|
+
})), {
|
|
3238
3801
|
name: "sanity-plugin-ai-translation",
|
|
3239
3802
|
document: {
|
|
3240
3803
|
actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
|
|
@@ -3245,6 +3808,8 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3245
3808
|
})), translationHistorySchema = translationHistory;
|
|
3246
3809
|
export {
|
|
3247
3810
|
aiTranslationPlugin,
|
|
3248
|
-
|
|
3811
|
+
richTextTransform,
|
|
3812
|
+
translationHistorySchema,
|
|
3813
|
+
vttCaptionTransform
|
|
3249
3814
|
};
|
|
3250
3815
|
//# sourceMappingURL=index.js.map
|