@braze/sanity-plugin-ai-translation 0.2.26 → 0.2.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +302 -2
- package/dist/index.js +1138 -427
- 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(
|
|
@@ -1757,7 +1605,109 @@ const fetchTranslationPrompts = async ({
|
|
|
1757
1605
|
payload
|
|
1758
1606
|
});
|
|
1759
1607
|
}
|
|
1760
|
-
},
|
|
1608
|
+
}, DEFAULT_OPTIONS = {
|
|
1609
|
+
maxRetries: 3,
|
|
1610
|
+
initialDelayMs: 1e3,
|
|
1611
|
+
maxDelayMs: 3e4,
|
|
1612
|
+
backoffMultiplier: 2,
|
|
1613
|
+
jitter: !0
|
|
1614
|
+
};
|
|
1615
|
+
function isRetryableError(error) {
|
|
1616
|
+
if (error instanceof Error) {
|
|
1617
|
+
const message = error.message.toLowerCase(), name = error.name.toLowerCase(), nonRetryablePatterns = [
|
|
1618
|
+
"schema not found",
|
|
1619
|
+
"invalid schema",
|
|
1620
|
+
"not found",
|
|
1621
|
+
"unauthorized",
|
|
1622
|
+
"forbidden",
|
|
1623
|
+
"invalid api key",
|
|
1624
|
+
"missing api key",
|
|
1625
|
+
"validation failed",
|
|
1626
|
+
"invalid document",
|
|
1627
|
+
"source language not found",
|
|
1628
|
+
"invalid configuration",
|
|
1629
|
+
"type mismatch"
|
|
1630
|
+
];
|
|
1631
|
+
for (const pattern of nonRetryablePatterns)
|
|
1632
|
+
if (message.includes(pattern) || name.includes(pattern))
|
|
1633
|
+
return !1;
|
|
1634
|
+
const retryablePatterns = [
|
|
1635
|
+
"failed to fetch",
|
|
1636
|
+
"network",
|
|
1637
|
+
"timeout",
|
|
1638
|
+
"econnreset",
|
|
1639
|
+
"econnrefused",
|
|
1640
|
+
"enotfound",
|
|
1641
|
+
"socket",
|
|
1642
|
+
"rate limit",
|
|
1643
|
+
"too many requests",
|
|
1644
|
+
"429",
|
|
1645
|
+
"500",
|
|
1646
|
+
"502",
|
|
1647
|
+
"503",
|
|
1648
|
+
"504",
|
|
1649
|
+
"internal server error",
|
|
1650
|
+
"bad gateway",
|
|
1651
|
+
"service unavailable",
|
|
1652
|
+
"gateway timeout",
|
|
1653
|
+
"temporarily unavailable",
|
|
1654
|
+
"try again"
|
|
1655
|
+
];
|
|
1656
|
+
for (const pattern of retryablePatterns)
|
|
1657
|
+
if (message.includes(pattern) || name.includes(pattern))
|
|
1658
|
+
return !0;
|
|
1659
|
+
}
|
|
1660
|
+
if (typeof error == "object" && error !== null) {
|
|
1661
|
+
const statusCode = error.statusCode || error.status;
|
|
1662
|
+
if (statusCode) {
|
|
1663
|
+
if (statusCode === 429 || statusCode >= 500 && statusCode < 600)
|
|
1664
|
+
return !0;
|
|
1665
|
+
if (statusCode >= 400 && statusCode < 500)
|
|
1666
|
+
return !1;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return !0;
|
|
1670
|
+
}
|
|
1671
|
+
function calculateBackoffDelay(attempt, options) {
|
|
1672
|
+
const { initialDelayMs, maxDelayMs, backoffMultiplier, jitter } = options;
|
|
1673
|
+
let delay = initialDelayMs * backoffMultiplier ** attempt;
|
|
1674
|
+
if (delay = Math.min(delay, maxDelayMs), jitter) {
|
|
1675
|
+
const jitterFactor = 0.75 + Math.random() * 0.5;
|
|
1676
|
+
delay = Math.round(delay * jitterFactor);
|
|
1677
|
+
}
|
|
1678
|
+
return delay;
|
|
1679
|
+
}
|
|
1680
|
+
function sleep(ms) {
|
|
1681
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1682
|
+
}
|
|
1683
|
+
async function withRetry(fn, options = {}) {
|
|
1684
|
+
const opts = { ...DEFAULT_OPTIONS, ...options }, shouldRetry = opts.isRetryable ?? isRetryableError;
|
|
1685
|
+
let lastError, attempts = 0;
|
|
1686
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
1687
|
+
attempts = attempt + 1;
|
|
1688
|
+
try {
|
|
1689
|
+
return { success: !0, result: await fn(), attempts };
|
|
1690
|
+
} catch (error) {
|
|
1691
|
+
if (lastError = error, attempt >= opts.maxRetries) {
|
|
1692
|
+
devLog.debug("Max retries exhausted", { attempt: attempts, error });
|
|
1693
|
+
break;
|
|
1694
|
+
}
|
|
1695
|
+
if (!shouldRetry(error)) {
|
|
1696
|
+
devLog.debug("Error is not retryable", { attempt: attempts, error });
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
const delay = calculateBackoffDelay(attempt, opts);
|
|
1700
|
+
devLog.debug("Scheduling retry", {
|
|
1701
|
+
attempt: attempts,
|
|
1702
|
+
nextAttempt: attempt + 2,
|
|
1703
|
+
delayMs: delay,
|
|
1704
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1705
|
+
}), opts.onRetry && opts.onRetry(attempt + 1, error, delay), await sleep(delay);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return { success: !1, error: lastError, attempts };
|
|
1709
|
+
}
|
|
1710
|
+
const getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
1761
1711
|
(translation) => translation._key === targetLanguage
|
|
1762
1712
|
)?.value._ref || null, getTranslationMetadata = async ({
|
|
1763
1713
|
client,
|
|
@@ -1848,16 +1798,189 @@ const fetchTranslationPrompts = async ({
|
|
|
1848
1798
|
status
|
|
1849
1799
|
})
|
|
1850
1800
|
});
|
|
1851
|
-
}
|
|
1801
|
+
};
|
|
1802
|
+
function applyTransforms({
|
|
1803
|
+
document,
|
|
1804
|
+
registry,
|
|
1805
|
+
schemaType,
|
|
1806
|
+
parentPath = ""
|
|
1807
|
+
}) {
|
|
1808
|
+
if (!document)
|
|
1809
|
+
return { transformedDocument: document, originalStructure: {} };
|
|
1810
|
+
if (typeof document != "object")
|
|
1811
|
+
return { transformedDocument: document, originalStructure: {} };
|
|
1812
|
+
if (Array.isArray(document)) {
|
|
1813
|
+
const originalStructure2 = {};
|
|
1814
|
+
return { transformedDocument: document.map((item, index) => {
|
|
1815
|
+
const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`, { transformedDocument: transformedItem, originalStructure: itemOriginalStructure } = applyTransforms({
|
|
1816
|
+
document: item,
|
|
1817
|
+
registry,
|
|
1818
|
+
schemaType,
|
|
1819
|
+
parentPath: itemPath
|
|
1820
|
+
});
|
|
1821
|
+
return Object.assign(originalStructure2, itemOriginalStructure), transformedItem;
|
|
1822
|
+
}), originalStructure: originalStructure2 };
|
|
1823
|
+
}
|
|
1824
|
+
const transformedObj = {}, originalStructure = {};
|
|
1825
|
+
for (const [key, value] of Object.entries(document)) {
|
|
1826
|
+
const fieldPath = parentPath ? `${parentPath}.${key}` : key;
|
|
1827
|
+
if (key.startsWith("_") && key !== "_type") {
|
|
1828
|
+
transformedObj[key] = value;
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
const transform = registry.findTransform(schemaType, fieldPath, value, document);
|
|
1832
|
+
if (transform) {
|
|
1833
|
+
devLog.debug("Applying transform", { fieldPath, schemaType });
|
|
1834
|
+
const context = {
|
|
1835
|
+
fieldPath,
|
|
1836
|
+
schemaType,
|
|
1837
|
+
document
|
|
1838
|
+
}, transformedValue = transform.toTranslatable(value, context);
|
|
1839
|
+
transformedObj[key] = transformedValue, originalStructure[fieldPath] = {
|
|
1840
|
+
value,
|
|
1841
|
+
transform
|
|
1842
|
+
};
|
|
1843
|
+
} else {
|
|
1844
|
+
const { transformedDocument: transformedValue, originalStructure: valueOriginalStructure } = applyTransforms({
|
|
1845
|
+
document: value,
|
|
1846
|
+
registry,
|
|
1847
|
+
schemaType,
|
|
1848
|
+
parentPath: fieldPath
|
|
1849
|
+
});
|
|
1850
|
+
Object.assign(originalStructure, valueOriginalStructure), transformedObj[key] = transformedValue;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
return { transformedDocument: transformedObj, originalStructure };
|
|
1854
|
+
}
|
|
1855
|
+
function restoreTransforms({
|
|
1856
|
+
translatedDocument,
|
|
1857
|
+
originalStructure,
|
|
1858
|
+
registry,
|
|
1859
|
+
schemaType,
|
|
1860
|
+
parentPath = ""
|
|
1861
|
+
}) {
|
|
1862
|
+
if (translatedDocument == null || typeof translatedDocument != "object")
|
|
1863
|
+
return translatedDocument;
|
|
1864
|
+
if (Array.isArray(translatedDocument))
|
|
1865
|
+
return translatedDocument.map((item, index) => {
|
|
1866
|
+
const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`;
|
|
1867
|
+
return restoreTransforms({
|
|
1868
|
+
translatedDocument: item,
|
|
1869
|
+
originalStructure,
|
|
1870
|
+
registry,
|
|
1871
|
+
schemaType,
|
|
1872
|
+
parentPath: itemPath
|
|
1873
|
+
});
|
|
1874
|
+
});
|
|
1875
|
+
const originalData = originalStructure[parentPath];
|
|
1876
|
+
if (originalData) {
|
|
1877
|
+
const { value: originalValue, transform } = originalData, context = {
|
|
1878
|
+
fieldPath: parentPath,
|
|
1879
|
+
schemaType,
|
|
1880
|
+
document: translatedDocument
|
|
1881
|
+
};
|
|
1882
|
+
return devLog.debug("Restoring transform", { fieldPath: parentPath, schemaType }), transform.fromTranslatable(translatedDocument, originalValue, context);
|
|
1883
|
+
}
|
|
1884
|
+
const restoredObj = {}, translatedObj = translatedDocument;
|
|
1885
|
+
for (const [key, value] of Object.entries(translatedObj)) {
|
|
1886
|
+
const fieldPath = parentPath ? `${parentPath}.${key}` : key, fieldOriginalData = originalStructure[fieldPath];
|
|
1887
|
+
if (fieldOriginalData) {
|
|
1888
|
+
const { value: originalValue, transform } = fieldOriginalData, context = {
|
|
1889
|
+
fieldPath,
|
|
1890
|
+
schemaType,
|
|
1891
|
+
document: translatedDocument
|
|
1892
|
+
};
|
|
1893
|
+
devLog.debug("Restoring transform for field", { fieldPath, schemaType }), restoredObj[key] = transform.fromTranslatable(value, originalValue, context);
|
|
1894
|
+
} else
|
|
1895
|
+
restoredObj[key] = restoreTransforms({
|
|
1896
|
+
translatedDocument: value,
|
|
1897
|
+
originalStructure,
|
|
1898
|
+
registry,
|
|
1899
|
+
schemaType,
|
|
1900
|
+
parentPath: fieldPath
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
return restoredObj;
|
|
1904
|
+
}
|
|
1905
|
+
class TransformRegistry {
|
|
1906
|
+
transforms = [];
|
|
1907
|
+
/**
|
|
1908
|
+
* Register a transform with the registry.
|
|
1909
|
+
* @param transform - Field transform to register
|
|
1910
|
+
*/
|
|
1911
|
+
register(transform) {
|
|
1912
|
+
devLog.debug("Registering field transform", {
|
|
1913
|
+
matcher: transform.matcher
|
|
1914
|
+
}), this.transforms.push(transform);
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Find a matching transform for the given field.
|
|
1918
|
+
* If multiple transforms match, returns the first one (order matters!).
|
|
1919
|
+
* @param schemaType - Document schema type
|
|
1920
|
+
* @param fieldPath - Path to the field
|
|
1921
|
+
* @param value - Optional field value for value-based matching
|
|
1922
|
+
* @param document - Optional full document for context
|
|
1923
|
+
* @returns Matching transform or undefined if none found
|
|
1924
|
+
*/
|
|
1925
|
+
findTransform(schemaType, fieldPath, value, document) {
|
|
1926
|
+
const matches = [];
|
|
1927
|
+
for (const transform of this.transforms) {
|
|
1928
|
+
const { matcher } = transform;
|
|
1929
|
+
if (!(matcher.schemaType && matcher.schemaType !== schemaType)) {
|
|
1930
|
+
if (matcher.fieldPath) {
|
|
1931
|
+
if (typeof matcher.fieldPath == "string") {
|
|
1932
|
+
if (matcher.fieldPath !== fieldPath)
|
|
1933
|
+
continue;
|
|
1934
|
+
} else if (matcher.fieldPath instanceof RegExp && !matcher.fieldPath.test(fieldPath))
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
if (matcher.fieldName) {
|
|
1938
|
+
const segments = fieldPath.split(".");
|
|
1939
|
+
if (segments[segments.length - 1]?.split("[")[0] !== matcher.fieldName)
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
if (matcher.valueMatcher) {
|
|
1943
|
+
if (value === void 0)
|
|
1944
|
+
continue;
|
|
1945
|
+
const context = {
|
|
1946
|
+
fieldPath,
|
|
1947
|
+
schemaType,
|
|
1948
|
+
document: document || {}
|
|
1949
|
+
};
|
|
1950
|
+
if (!matcher.valueMatcher(value, context))
|
|
1951
|
+
continue;
|
|
1952
|
+
}
|
|
1953
|
+
matches.push(transform);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
return matches.length > 1 && devLog.warn("Multiple transforms match the same field - using first match", {
|
|
1957
|
+
schemaType,
|
|
1958
|
+
fieldPath,
|
|
1959
|
+
matchCount: matches.length,
|
|
1960
|
+
firstMatcher: matches[0].matcher
|
|
1961
|
+
}), matches[0];
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKey, request) => (request.model || "gpt-5") === "gpt-5" ? await callGPT5ResponsesAPI(apiKey, request) : await callChatCompletionsAPI(apiKey, request), callGPT5ResponsesAPI = async (apiKey, request) => {
|
|
1852
1965
|
const model = request.model || "gpt-5", requestBody = {
|
|
1853
|
-
model,
|
|
1966
|
+
model: "gpt-5.1",
|
|
1854
1967
|
input: request.messages
|
|
1855
1968
|
// Uses 'input' instead of 'messages'
|
|
1856
1969
|
};
|
|
1857
1970
|
request.reasoningEffort && (requestBody.reasoning = {
|
|
1858
1971
|
effort: request.reasoningEffort,
|
|
1859
1972
|
summary: "auto"
|
|
1860
|
-
//
|
|
1973
|
+
// Enable automatic reasoning summaries
|
|
1974
|
+
}), devLog.debug("OpenAI GPT-5 Request", {
|
|
1975
|
+
model: requestBody.model,
|
|
1976
|
+
reasoningEffort: request.reasoningEffort,
|
|
1977
|
+
messages: request.messages.map((m) => ({
|
|
1978
|
+
role: m.role,
|
|
1979
|
+
contentLength: m.content.length,
|
|
1980
|
+
contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
|
|
1981
|
+
}))
|
|
1982
|
+
}), devLog.debug("OpenAI GPT-5 Full Prompt", {
|
|
1983
|
+
messages: request.messages
|
|
1861
1984
|
});
|
|
1862
1985
|
const response = await fetch("https://api.openai.com/v1/responses", {
|
|
1863
1986
|
method: "POST",
|
|
@@ -1892,7 +2015,19 @@ const fetchTranslationPrompts = async ({
|
|
|
1892
2015
|
model,
|
|
1893
2016
|
messages: request.messages,
|
|
1894
2017
|
temperature: request.temperature ?? 0.3
|
|
1895
|
-
}
|
|
2018
|
+
};
|
|
2019
|
+
devLog.debug("OpenAI Chat Completions Request", {
|
|
2020
|
+
model,
|
|
2021
|
+
temperature: request.temperature ?? 0.3,
|
|
2022
|
+
messages: request.messages.map((m) => ({
|
|
2023
|
+
role: m.role,
|
|
2024
|
+
contentLength: m.content.length,
|
|
2025
|
+
contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
|
|
2026
|
+
}))
|
|
2027
|
+
}), devLog.debug("OpenAI Chat Completions Full Prompt", {
|
|
2028
|
+
messages: request.messages
|
|
2029
|
+
});
|
|
2030
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1896
2031
|
method: "POST",
|
|
1897
2032
|
headers: {
|
|
1898
2033
|
"Content-Type": "application/json",
|
|
@@ -1945,9 +2080,52 @@ const fetchTranslationPrompts = async ({
|
|
|
1945
2080
|
}, parseReviewFormat = (text) => {
|
|
1946
2081
|
const root = [], stack = [], lines = text.split(`
|
|
1947
2082
|
`);
|
|
1948
|
-
for (
|
|
1949
|
-
const trimmed =
|
|
2083
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2084
|
+
const trimmed = lines[i].trim();
|
|
1950
2085
|
if (!trimmed) continue;
|
|
2086
|
+
const compactStringMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
2087
|
+
if (compactStringMatch && !trimmed.includes("<")) {
|
|
2088
|
+
const [, fieldName, text2] = compactStringMatch, node = {
|
|
2089
|
+
type: "field",
|
|
2090
|
+
name: fieldName,
|
|
2091
|
+
text: unescapeXml(text2),
|
|
2092
|
+
attributes: {},
|
|
2093
|
+
children: []
|
|
2094
|
+
};
|
|
2095
|
+
if (stack.length > 0) {
|
|
2096
|
+
const parent = stack[stack.length - 1];
|
|
2097
|
+
parent && parent.children.push(node);
|
|
2098
|
+
} else
|
|
2099
|
+
root.push(node);
|
|
2100
|
+
continue;
|
|
2101
|
+
}
|
|
2102
|
+
const compactStartMatch = trimmed.match(/^---\s+(\w+)\s+---$/);
|
|
2103
|
+
if (compactStartMatch) {
|
|
2104
|
+
const fieldName = compactStartMatch[1], contentLines = [];
|
|
2105
|
+
for (i++; i < lines.length; ) {
|
|
2106
|
+
const currentLine = lines[i];
|
|
2107
|
+
if (currentLine.trim().match(new RegExp(`^---\\s+/${fieldName}\\s+---$`)))
|
|
2108
|
+
break;
|
|
2109
|
+
contentLines.push(currentLine), i++;
|
|
2110
|
+
}
|
|
2111
|
+
const content = contentLines.join(`
|
|
2112
|
+
`);
|
|
2113
|
+
let contentType = "translatable.vtt.captions";
|
|
2114
|
+
(content.includes("<block") || content.includes("<span")) && (contentType = "translatable.richtext");
|
|
2115
|
+
const node = {
|
|
2116
|
+
type: "field",
|
|
2117
|
+
name: fieldName,
|
|
2118
|
+
text: content,
|
|
2119
|
+
attributes: { type: contentType },
|
|
2120
|
+
children: []
|
|
2121
|
+
};
|
|
2122
|
+
if (stack.length > 0) {
|
|
2123
|
+
const parent = stack[stack.length - 1];
|
|
2124
|
+
parent && parent.children.push(node);
|
|
2125
|
+
} else
|
|
2126
|
+
root.push(node);
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
1951
2129
|
const openMatch = trimmed.match(/^<(field|block|span)([^>]*)>(.*)$/);
|
|
1952
2130
|
if (openMatch) {
|
|
1953
2131
|
const [, tagName, attrsStr, content] = openMatch, attributes = parseAttributes(attrsStr), node = {
|
|
@@ -1970,7 +2148,21 @@ const fetchTranslationPrompts = async ({
|
|
|
1970
2148
|
parent && parent.children.push(node);
|
|
1971
2149
|
} else
|
|
1972
2150
|
root.push(node);
|
|
1973
|
-
else
|
|
2151
|
+
else if (attributes.type === "translatable.vtt.captions" || attributes.type === "translatable.richtext") {
|
|
2152
|
+
const contentLines = [];
|
|
2153
|
+
for (i++; i < lines.length; ) {
|
|
2154
|
+
const currentLine = lines[i];
|
|
2155
|
+
if (currentLine.trim().match(/^<\/field>$/))
|
|
2156
|
+
break;
|
|
2157
|
+
contentLines.push(currentLine), i++;
|
|
2158
|
+
}
|
|
2159
|
+
if (node.text = contentLines.join(`
|
|
2160
|
+
`), stack.length > 0) {
|
|
2161
|
+
const parent = stack[stack.length - 1];
|
|
2162
|
+
parent && parent.children.push(node);
|
|
2163
|
+
} else
|
|
2164
|
+
root.push(node);
|
|
2165
|
+
} else
|
|
1974
2166
|
stack.push(node);
|
|
1975
2167
|
continue;
|
|
1976
2168
|
}
|
|
@@ -1998,6 +2190,16 @@ const fetchTranslationPrompts = async ({
|
|
|
1998
2190
|
applyFieldChanges(doc, path, node);
|
|
1999
2191
|
}
|
|
2000
2192
|
}, applyFieldChanges = (doc, path, node) => {
|
|
2193
|
+
if (node.attributes.type === "translatable.vtt.captions") {
|
|
2194
|
+
const fieldValue = get(doc, path);
|
|
2195
|
+
fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.vtt.captions" && node.text && set(doc, [...path, "translationText"], node.text);
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
if (node.attributes.type === "translatable.richtext") {
|
|
2199
|
+
const fieldValue = get(doc, path);
|
|
2200
|
+
fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.richtext" && node.text && set(doc, [...path, "translationText"], node.text);
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2001
2203
|
if (node.attributes.type === "array") {
|
|
2002
2204
|
node.children.forEach((child, idx) => {
|
|
2003
2205
|
applyFieldChanges(doc, [...path, idx], child);
|
|
@@ -2064,7 +2266,7 @@ const fetchTranslationPrompts = async ({
|
|
|
2064
2266
|
if (path.length === 0) {
|
|
2065
2267
|
if (isPlainObject(value))
|
|
2066
2268
|
for (const [key, val] of Object.entries(value))
|
|
2067
|
-
!key.startsWith("_") && key !== "language" && serialize(val, [key],
|
|
2269
|
+
!key.startsWith("_") && key !== "language" && serialize(val, [key], 0);
|
|
2068
2270
|
return;
|
|
2069
2271
|
}
|
|
2070
2272
|
const fieldName = path[path.length - 1];
|
|
@@ -2077,6 +2279,14 @@ const fetchTranslationPrompts = async ({
|
|
|
2077
2279
|
return;
|
|
2078
2280
|
}
|
|
2079
2281
|
if (isPlainObject(value)) {
|
|
2282
|
+
if (value._type === "translatable.vtt.captions" && typeof value.translationText == "string") {
|
|
2283
|
+
lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
if (value._type === "translatable.richtext" && typeof value.translationText == "string") {
|
|
2287
|
+
lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2080
2290
|
lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
|
|
2081
2291
|
for (const [key, val] of Object.entries(value))
|
|
2082
2292
|
key.startsWith("_") || serialize(val, [...path, key], indent + 1);
|
|
@@ -2084,7 +2294,7 @@ const fetchTranslationPrompts = async ({
|
|
|
2084
2294
|
return;
|
|
2085
2295
|
}
|
|
2086
2296
|
if (typeof value == "string") {
|
|
2087
|
-
value.length > 0 && lines.push(`${
|
|
2297
|
+
value.length > 0 && lines.push(`${fieldName}: ${escapeXml(value)}`);
|
|
2088
2298
|
return;
|
|
2089
2299
|
}
|
|
2090
2300
|
(typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
|
|
@@ -2217,7 +2427,14 @@ const fetchTranslationPrompts = async ({
|
|
|
2217
2427
|
sourceLanguage
|
|
2218
2428
|
};
|
|
2219
2429
|
try {
|
|
2220
|
-
const sourceText = serializeDocument(sourceDocument)
|
|
2430
|
+
const sourceText = serializeDocument(sourceDocument);
|
|
2431
|
+
devLog.info("Serialized document for OpenAI", {
|
|
2432
|
+
...logContext,
|
|
2433
|
+
characterCount: sourceText.length,
|
|
2434
|
+
estimatedTokens: Math.ceil(sourceText.length / 4),
|
|
2435
|
+
preview: sourceText.substring(0, 500)
|
|
2436
|
+
});
|
|
2437
|
+
const prompt = buildTranslationPrompt$1({
|
|
2221
2438
|
sourceText,
|
|
2222
2439
|
sourceLanguage,
|
|
2223
2440
|
targetLanguage,
|
|
@@ -2232,7 +2449,15 @@ const fetchTranslationPrompts = async ({
|
|
|
2232
2449
|
],
|
|
2233
2450
|
model: openaiConfig.model,
|
|
2234
2451
|
reasoningEffort: openaiConfig.reasoningEffort
|
|
2235
|
-
})
|
|
2452
|
+
});
|
|
2453
|
+
devLog.info("OpenAI translation response received", {
|
|
2454
|
+
...logContext,
|
|
2455
|
+
tokensUsed: response.tokensUsed,
|
|
2456
|
+
cost: response.cost,
|
|
2457
|
+
responseLength: response.content.length,
|
|
2458
|
+
responsePreview: response.content.substring(0, 500)
|
|
2459
|
+
});
|
|
2460
|
+
const result = deserializeDocument(sourceDocument, response.content);
|
|
2236
2461
|
result[languageFieldName] = targetLanguage;
|
|
2237
2462
|
const validation = validateStructureIntegrity(sourceDocument, result);
|
|
2238
2463
|
if (!validation.valid) {
|
|
@@ -2424,11 +2649,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2424
2649
|
openaiConfig,
|
|
2425
2650
|
releaseId
|
|
2426
2651
|
}) => {
|
|
2652
|
+
devLog.debug("Applying field transforms before translation", {
|
|
2653
|
+
documentId: sourceDocument._id,
|
|
2654
|
+
documentType: sourceDocument._type
|
|
2655
|
+
});
|
|
2656
|
+
const { transformedDocument, originalStructure } = applyTransforms({
|
|
2657
|
+
document: sourceDocument,
|
|
2658
|
+
registry: globalTransformRegistry,
|
|
2659
|
+
schemaType: sourceDocument._type
|
|
2660
|
+
});
|
|
2427
2661
|
let translatedDoc;
|
|
2428
2662
|
switch (translationMode) {
|
|
2429
2663
|
case "sanity-only": {
|
|
2430
2664
|
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
|
|
2431
|
-
sourceDocument,
|
|
2665
|
+
sourceDocument: transformedDocument,
|
|
2432
2666
|
targetLanguage,
|
|
2433
2667
|
languageFieldName,
|
|
2434
2668
|
schemaId,
|
|
@@ -2442,7 +2676,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2442
2676
|
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2443
2677
|
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), translatedDoc = await getOpenAITranslation({
|
|
2444
2678
|
dispatch,
|
|
2445
|
-
sourceDocument,
|
|
2679
|
+
sourceDocument: transformedDocument,
|
|
2446
2680
|
targetLanguage,
|
|
2447
2681
|
languageFieldName,
|
|
2448
2682
|
translationPrompts,
|
|
@@ -2457,8 +2691,17 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2457
2691
|
}
|
|
2458
2692
|
if (!translatedDoc)
|
|
2459
2693
|
return null;
|
|
2460
|
-
|
|
2461
|
-
|
|
2694
|
+
devLog.debug("Restoring field transforms after translation", {
|
|
2695
|
+
documentId: sourceDocument._id,
|
|
2696
|
+
documentType: sourceDocument._type
|
|
2697
|
+
});
|
|
2698
|
+
const finalDoc = restoreTransforms({
|
|
2699
|
+
translatedDocument: translatedDoc,
|
|
2700
|
+
originalStructure,
|
|
2701
|
+
registry: globalTransformRegistry,
|
|
2702
|
+
schemaType: sourceDocument._type
|
|
2703
|
+
}), targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
|
|
2704
|
+
...finalDoc,
|
|
2462
2705
|
_id: targetDocumentVersionId
|
|
2463
2706
|
};
|
|
2464
2707
|
return await client.action({
|
|
@@ -2502,23 +2745,50 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2502
2745
|
});
|
|
2503
2746
|
return;
|
|
2504
2747
|
}
|
|
2505
|
-
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage),
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2748
|
+
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), translationResult = await withRetry(
|
|
2749
|
+
() => handleTranslation({
|
|
2750
|
+
dispatch,
|
|
2751
|
+
client,
|
|
2752
|
+
schemaId,
|
|
2753
|
+
sourceDocument,
|
|
2754
|
+
targetLanguage,
|
|
2755
|
+
existingTranslationId,
|
|
2756
|
+
languageFieldName,
|
|
2757
|
+
translationPrompts,
|
|
2758
|
+
translationMode,
|
|
2759
|
+
openaiConfig,
|
|
2760
|
+
releaseId
|
|
2761
|
+
}),
|
|
2762
|
+
{
|
|
2763
|
+
maxRetries: 3,
|
|
2764
|
+
initialDelayMs: 1e3,
|
|
2765
|
+
isRetryable: isRetryableError,
|
|
2766
|
+
onRetry: (attempt, error, delayMs) => {
|
|
2767
|
+
devLog.warn("Translation failed, scheduling retry", {
|
|
2768
|
+
...logContext,
|
|
2769
|
+
attempt,
|
|
2770
|
+
delayMs,
|
|
2771
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2772
|
+
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "retrying", {
|
|
2773
|
+
retryCount: attempt,
|
|
2774
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
);
|
|
2779
|
+
if (!translationResult.success || !translationResult.result) {
|
|
2780
|
+
const errorMessage = translationResult.error instanceof Error ? translationResult.error.message : String(translationResult.error || "Translation failed");
|
|
2781
|
+
devLog.error("Translation failed after retries", translationResult.error, {
|
|
2782
|
+
...logContext,
|
|
2783
|
+
attempts: translationResult.attempts
|
|
2784
|
+
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
2785
|
+
errorMessage,
|
|
2786
|
+
retryCount: translationResult.attempts - 1
|
|
2787
|
+
// -1 because attempts includes initial try
|
|
2788
|
+
}), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
2520
2789
|
return;
|
|
2521
2790
|
}
|
|
2791
|
+
const newDocument = translationResult.result;
|
|
2522
2792
|
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
|
|
2523
2793
|
targetId: newDocument._id
|
|
2524
2794
|
}), await updateTranslationMetadata({
|
|
@@ -2588,7 +2858,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2588
2858
|
{
|
|
2589
2859
|
_id: sourceDocument._id,
|
|
2590
2860
|
_type: sourceDocument._type,
|
|
2591
|
-
// TODO:
|
|
2861
|
+
// TODO: Extract document title more robustly across schema types
|
|
2592
2862
|
title: sourceDocument.name || sourceDocument.title
|
|
2593
2863
|
},
|
|
2594
2864
|
...references
|
|
@@ -3098,12 +3368,17 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3098
3368
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3099
3369
|
"Error: ",
|
|
3100
3370
|
job.errorMessage
|
|
3371
|
+
] }) }) : null,
|
|
3372
|
+
job.status === "retrying" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3373
|
+
"Retrying after: ",
|
|
3374
|
+
job.errorMessage
|
|
3101
3375
|
] }) }) : null
|
|
3102
3376
|
] })
|
|
3103
3377
|
] }),
|
|
3104
3378
|
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
3105
3379
|
job.status === "skipped" ? /* @__PURE__ */ jsx(SkippedBadge, {}) : null,
|
|
3106
|
-
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {}) : null,
|
|
3380
|
+
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, { retryCount: job.retryCount }) : null,
|
|
3381
|
+
job.status === "retrying" ? /* @__PURE__ */ jsx(RetryingBadge, { retryCount: job.retryCount }) : null,
|
|
3107
3382
|
job.targetId ? /* @__PURE__ */ jsx(
|
|
3108
3383
|
TranslationOpenerButton,
|
|
3109
3384
|
{
|
|
@@ -3183,12 +3458,25 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3183
3458
|
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "There was already a fresh translation for this document and language." }) }),
|
|
3184
3459
|
children: /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: "Skipped" })
|
|
3185
3460
|
}
|
|
3186
|
-
), ErrorBadge = () => /* @__PURE__ */ jsx(
|
|
3461
|
+
), ErrorBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
|
|
3187
3462
|
Tooltip,
|
|
3188
3463
|
{
|
|
3189
3464
|
portal: !0,
|
|
3190
|
-
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "This translation failed. Check the error message for details." }) }),
|
|
3191
|
-
children: /* @__PURE__ */
|
|
3465
|
+
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: retryCount && retryCount > 0 ? `This translation failed after ${retryCount} retry attempt${retryCount > 1 ? "s" : ""}.` : "This translation failed. Check the error message for details." }) }),
|
|
3466
|
+
children: /* @__PURE__ */ jsxs(Badge, { tone: "critical", children: [
|
|
3467
|
+
"Failed",
|
|
3468
|
+
retryCount && retryCount > 0 ? ` (${retryCount} retries)` : ""
|
|
3469
|
+
] })
|
|
3470
|
+
}
|
|
3471
|
+
), RetryingBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
|
|
3472
|
+
Tooltip,
|
|
3473
|
+
{
|
|
3474
|
+
portal: !0,
|
|
3475
|
+
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Retrying translation after a temporary failure." }) }),
|
|
3476
|
+
children: /* @__PURE__ */ jsxs(Badge, { tone: "caution", children: [
|
|
3477
|
+
"Retry ",
|
|
3478
|
+
retryCount || 1
|
|
3479
|
+
] })
|
|
3192
3480
|
}
|
|
3193
3481
|
), getStatusLabel = (status) => ({
|
|
3194
3482
|
idle: "Idle",
|
|
@@ -3198,6 +3486,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3198
3486
|
translating_with_openai: "Translating with GPT-5...",
|
|
3199
3487
|
reviewing_with_openai: "Reviewing translation with GPT-5...",
|
|
3200
3488
|
updating_metadata: "Saving metadata...",
|
|
3489
|
+
retrying: "Retrying...",
|
|
3201
3490
|
error: "Error",
|
|
3202
3491
|
skipped: "Skipped",
|
|
3203
3492
|
completed: "Completed"
|
|
@@ -3236,7 +3525,427 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3236
3525
|
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
|
|
3237
3526
|
}
|
|
3238
3527
|
} : (devLog.error("Missing config in TranslateAction"), null);
|
|
3239
|
-
},
|
|
3528
|
+
}, translationGlossaryTerm = defineType({
|
|
3529
|
+
name: "translationGlossaryTerm",
|
|
3530
|
+
title: "Translation Glossary Term",
|
|
3531
|
+
icon: TranslateIcon,
|
|
3532
|
+
type: "document",
|
|
3533
|
+
fields: [
|
|
3534
|
+
defineField({
|
|
3535
|
+
name: "translations",
|
|
3536
|
+
title: "Translations",
|
|
3537
|
+
type: "internationalizedArrayString"
|
|
3538
|
+
})
|
|
3539
|
+
],
|
|
3540
|
+
preview: {
|
|
3541
|
+
select: {
|
|
3542
|
+
translations: "translations"
|
|
3543
|
+
},
|
|
3544
|
+
prepare({ translations }) {
|
|
3545
|
+
const enTranslation = translations?.find((t) => t.locale === "en-us"), firstTranslation = translations?.[0];
|
|
3546
|
+
return {
|
|
3547
|
+
title: enTranslation?.value || firstTranslation?.value || "No translation available"
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
}), translationHistory = defineField({
|
|
3552
|
+
name: "translationHistory",
|
|
3553
|
+
type: "array",
|
|
3554
|
+
of: [
|
|
3555
|
+
{
|
|
3556
|
+
name: "targetLanguageHistory",
|
|
3557
|
+
type: "object",
|
|
3558
|
+
fields: [
|
|
3559
|
+
defineField({
|
|
3560
|
+
name: "language",
|
|
3561
|
+
title: "Language",
|
|
3562
|
+
description: "The language of the target document",
|
|
3563
|
+
type: "string"
|
|
3564
|
+
}),
|
|
3565
|
+
defineField({
|
|
3566
|
+
name: "sourceId",
|
|
3567
|
+
title: "Source ID",
|
|
3568
|
+
description: "The ID of the source document",
|
|
3569
|
+
type: "string"
|
|
3570
|
+
}),
|
|
3571
|
+
defineField({
|
|
3572
|
+
name: "sourceRev",
|
|
3573
|
+
title: "Source Revision",
|
|
3574
|
+
description: "The revision ID of the source document at the time of translation",
|
|
3575
|
+
type: "string"
|
|
3576
|
+
}),
|
|
3577
|
+
defineField({
|
|
3578
|
+
name: "sourceLanguage",
|
|
3579
|
+
title: "Source Language",
|
|
3580
|
+
description: "The language of the source document",
|
|
3581
|
+
type: "string"
|
|
3582
|
+
}),
|
|
3583
|
+
defineField({
|
|
3584
|
+
name: "sourceSnapshot",
|
|
3585
|
+
title: "Source Snapshot",
|
|
3586
|
+
description: "The snapshot of the source document at the time of translation",
|
|
3587
|
+
type: "text"
|
|
3588
|
+
}),
|
|
3589
|
+
defineField({
|
|
3590
|
+
name: "targetId",
|
|
3591
|
+
title: "Target ID",
|
|
3592
|
+
description: "The ID of the target document",
|
|
3593
|
+
type: "string"
|
|
3594
|
+
})
|
|
3595
|
+
]
|
|
3596
|
+
}
|
|
3597
|
+
]
|
|
3598
|
+
}), translationSettings = defineType({
|
|
3599
|
+
name: "translationSettings",
|
|
3600
|
+
title: "Translation Settings",
|
|
3601
|
+
icon: TranslateIcon,
|
|
3602
|
+
type: "document",
|
|
3603
|
+
groups: [
|
|
3604
|
+
{
|
|
3605
|
+
name: "general",
|
|
3606
|
+
title: "General",
|
|
3607
|
+
default: !0
|
|
3608
|
+
},
|
|
3609
|
+
{
|
|
3610
|
+
name: "history",
|
|
3611
|
+
title: "History"
|
|
3612
|
+
}
|
|
3613
|
+
],
|
|
3614
|
+
fields: [
|
|
3615
|
+
defineField({
|
|
3616
|
+
name: "globalPrompt",
|
|
3617
|
+
title: "Global Translation Prompt",
|
|
3618
|
+
description: "A prompt that will given to the Sanity AI Agent for all translation tasks.",
|
|
3619
|
+
type: "text",
|
|
3620
|
+
rows: 5,
|
|
3621
|
+
group: "general"
|
|
3622
|
+
}),
|
|
3623
|
+
defineField({
|
|
3624
|
+
name: "localePrompts",
|
|
3625
|
+
title: "Locale Prompts",
|
|
3626
|
+
description: "Prompts that will be given to the Sanity AI Agent for each locale.",
|
|
3627
|
+
type: "internationalizedArrayText",
|
|
3628
|
+
group: "general"
|
|
3629
|
+
}),
|
|
3630
|
+
defineField({
|
|
3631
|
+
name: "protectedPhrases",
|
|
3632
|
+
title: "Protected Phrases",
|
|
3633
|
+
description: "Phrases that will never be translated.",
|
|
3634
|
+
type: "array",
|
|
3635
|
+
of: [{ type: "string" }],
|
|
3636
|
+
group: "general"
|
|
3637
|
+
}),
|
|
3638
|
+
defineField({
|
|
3639
|
+
name: "changeHistory",
|
|
3640
|
+
title: "Change History",
|
|
3641
|
+
description: "History of changes made via user feedback.",
|
|
3642
|
+
type: "array",
|
|
3643
|
+
readOnly: !0,
|
|
3644
|
+
group: "history",
|
|
3645
|
+
of: [
|
|
3646
|
+
{
|
|
3647
|
+
type: "object",
|
|
3648
|
+
fields: [
|
|
3649
|
+
defineField({
|
|
3650
|
+
name: "timestamp",
|
|
3651
|
+
title: "Timestamp",
|
|
3652
|
+
type: "datetime"
|
|
3653
|
+
}),
|
|
3654
|
+
defineField({
|
|
3655
|
+
name: "changeType",
|
|
3656
|
+
title: "Change Type",
|
|
3657
|
+
type: "string",
|
|
3658
|
+
options: {
|
|
3659
|
+
list: [
|
|
3660
|
+
{ title: "Global Prompt", value: "globalPrompt" },
|
|
3661
|
+
{ title: "Locale Prompt", value: "localePrompt" },
|
|
3662
|
+
{ title: "Protected Phrase", value: "protectedPhrase" },
|
|
3663
|
+
{ title: "Glossary Term", value: "glossaryTerm" }
|
|
3664
|
+
]
|
|
3665
|
+
}
|
|
3666
|
+
}),
|
|
3667
|
+
defineField({
|
|
3668
|
+
name: "description",
|
|
3669
|
+
title: "Description",
|
|
3670
|
+
type: "text",
|
|
3671
|
+
rows: 3
|
|
3672
|
+
}),
|
|
3673
|
+
defineField({
|
|
3674
|
+
name: "locale",
|
|
3675
|
+
title: "Locale",
|
|
3676
|
+
type: "string",
|
|
3677
|
+
description: "Target locale for locale-specific changes"
|
|
3678
|
+
})
|
|
3679
|
+
],
|
|
3680
|
+
preview: {
|
|
3681
|
+
select: {
|
|
3682
|
+
timestamp: "timestamp",
|
|
3683
|
+
changeType: "changeType",
|
|
3684
|
+
description: "description"
|
|
3685
|
+
},
|
|
3686
|
+
prepare({ timestamp, changeType, description }) {
|
|
3687
|
+
return {
|
|
3688
|
+
title: `${changeType} - ${new Date(timestamp).toLocaleDateString()}`,
|
|
3689
|
+
subtitle: description
|
|
3690
|
+
};
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
]
|
|
3695
|
+
})
|
|
3696
|
+
],
|
|
3697
|
+
preview: {
|
|
3698
|
+
prepare: () => ({
|
|
3699
|
+
title: "Translation Settings"
|
|
3700
|
+
})
|
|
3701
|
+
}
|
|
3702
|
+
});
|
|
3703
|
+
function isPortableText(value) {
|
|
3704
|
+
return Array.isArray(value) && value.some((item) => item?._type === "block");
|
|
3705
|
+
}
|
|
3706
|
+
const richTextTransform = {
|
|
3707
|
+
// Matcher combines field name pattern with value-based detection
|
|
3708
|
+
// fieldPath pattern acts as a pre-filter for performance
|
|
3709
|
+
// valueMatcher definitively checks if the value is actually Portable Text
|
|
3710
|
+
matcher: {
|
|
3711
|
+
// Pre-filter: Match all fields to enable detection based solely on value
|
|
3712
|
+
fieldPath: /.*/,
|
|
3713
|
+
// Definitive check: Verify the value is actually Portable Text
|
|
3714
|
+
valueMatcher: (value) => Array.isArray(value) && isPortableText(value)
|
|
3715
|
+
},
|
|
3716
|
+
toTranslatable: (value, context) => {
|
|
3717
|
+
if (!Array.isArray(value) || !isPortableText(value))
|
|
3718
|
+
return {
|
|
3719
|
+
_type: "translatable.richtext",
|
|
3720
|
+
translationText: JSON.stringify(value),
|
|
3721
|
+
_originalValue: value
|
|
3722
|
+
};
|
|
3723
|
+
devLog.info("Applying rich text transform with XML serialization", {
|
|
3724
|
+
fieldPath: context.fieldPath,
|
|
3725
|
+
blockCount: value.length
|
|
3726
|
+
});
|
|
3727
|
+
try {
|
|
3728
|
+
const tempDoc = {
|
|
3729
|
+
_id: "temp",
|
|
3730
|
+
_type: "richTextWrapper",
|
|
3731
|
+
_createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3732
|
+
_updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3733
|
+
_rev: "temp",
|
|
3734
|
+
content: value
|
|
3735
|
+
}, serialized = serializeDocument(tempDoc);
|
|
3736
|
+
devLog.debug("Serialized rich text document", {
|
|
3737
|
+
fieldPath: context.fieldPath,
|
|
3738
|
+
serializedLength: serialized.length,
|
|
3739
|
+
preview: serialized.substring(0, 200)
|
|
3740
|
+
});
|
|
3741
|
+
const contentRegex = /<field name="content"[^>]*>([\s\S]*)<\/field>/s, match = serialized.match(contentRegex);
|
|
3742
|
+
if (match?.[1]) {
|
|
3743
|
+
const result = {
|
|
3744
|
+
_type: "translatable.richtext",
|
|
3745
|
+
translationText: match[1].trim(),
|
|
3746
|
+
_originalValue: value
|
|
3747
|
+
};
|
|
3748
|
+
return devLog.debug("Successfully extracted and wrapped rich text", {
|
|
3749
|
+
fieldPath: context.fieldPath,
|
|
3750
|
+
translationTextLength: result.translationText.length
|
|
3751
|
+
}), result;
|
|
3752
|
+
}
|
|
3753
|
+
return devLog.warn("Failed to serialize rich text content - regex did not match", {
|
|
3754
|
+
fieldPath: context.fieldPath,
|
|
3755
|
+
serializedPreview: serialized.substring(0, 500)
|
|
3756
|
+
}), {
|
|
3757
|
+
_type: "translatable.richtext",
|
|
3758
|
+
translationText: JSON.stringify(value),
|
|
3759
|
+
_originalValue: value
|
|
3760
|
+
};
|
|
3761
|
+
} catch (error) {
|
|
3762
|
+
return devLog.error("Error serializing rich text", error, {
|
|
3763
|
+
fieldPath: context.fieldPath
|
|
3764
|
+
}), {
|
|
3765
|
+
_type: "translatable.richtext",
|
|
3766
|
+
translationText: JSON.stringify(value),
|
|
3767
|
+
_originalValue: value
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
},
|
|
3771
|
+
fromTranslatable: (translatedValue, originalValue, context) => {
|
|
3772
|
+
if (!translatedValue || translatedValue._type !== "translatable.richtext" || !translatedValue.translationText)
|
|
3773
|
+
return devLog.debug("Value is not a translatable rich text object, returning original", {
|
|
3774
|
+
fieldPath: context.fieldPath,
|
|
3775
|
+
hasCorrectType: translatedValue._type === "translatable.richtext"
|
|
3776
|
+
}), originalValue;
|
|
3777
|
+
devLog.info("Deserializing rich text content", {
|
|
3778
|
+
fieldPath: context.fieldPath,
|
|
3779
|
+
translationTextLength: translatedValue.translationText.length
|
|
3780
|
+
});
|
|
3781
|
+
try {
|
|
3782
|
+
const { translationText, _originalValue } = translatedValue, wrappedSerialized = `<field name="content">
|
|
3783
|
+
${translationText}
|
|
3784
|
+
</field>`;
|
|
3785
|
+
devLog.debug("Prepared wrapped serialized content", {
|
|
3786
|
+
fieldPath: context.fieldPath,
|
|
3787
|
+
wrappedLength: wrappedSerialized.length,
|
|
3788
|
+
preview: wrappedSerialized.substring(0, 300)
|
|
3789
|
+
});
|
|
3790
|
+
const tempOriginalDoc = {
|
|
3791
|
+
_id: "temp",
|
|
3792
|
+
_type: "richTextWrapper",
|
|
3793
|
+
_createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3794
|
+
_updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3795
|
+
_rev: "temp",
|
|
3796
|
+
content: originalValue
|
|
3797
|
+
}, result = deserializeDocument(tempOriginalDoc, wrappedSerialized);
|
|
3798
|
+
return devLog.debug("Deserialization result", {
|
|
3799
|
+
fieldPath: context.fieldPath,
|
|
3800
|
+
hasContent: !!result.content,
|
|
3801
|
+
contentType: Array.isArray(result.content) ? "array" : typeof result.content,
|
|
3802
|
+
contentLength: Array.isArray(result.content) ? result.content.length : void 0
|
|
3803
|
+
}), result.content ? result.content : (devLog.warn("Deserialization returned no content, returning original", {
|
|
3804
|
+
fieldPath: context.fieldPath,
|
|
3805
|
+
resultKeys: Object.keys(result)
|
|
3806
|
+
}), originalValue);
|
|
3807
|
+
} catch (error) {
|
|
3808
|
+
return devLog.error("Failed to deserialize rich text", error, {
|
|
3809
|
+
fieldPath: context.fieldPath
|
|
3810
|
+
}), originalValue;
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
}, vttCaptionTransform = {
|
|
3814
|
+
matcher: {
|
|
3815
|
+
fieldName: "captions",
|
|
3816
|
+
schemaType: "video"
|
|
3817
|
+
},
|
|
3818
|
+
toTranslatable: (captionsArray, context) => {
|
|
3819
|
+
if (devLog.info("VTT captions toTranslatable - input:", {
|
|
3820
|
+
fieldPath: context.fieldPath,
|
|
3821
|
+
isArray: Array.isArray(captionsArray),
|
|
3822
|
+
count: Array.isArray(captionsArray) ? captionsArray.length : 0,
|
|
3823
|
+
firstCaption: Array.isArray(captionsArray) && captionsArray.length > 0 ? captionsArray[0] : null
|
|
3824
|
+
}), !Array.isArray(captionsArray))
|
|
3825
|
+
return devLog.debug("VTT caption field is not an array, returning empty transform", {
|
|
3826
|
+
fieldPath: context.fieldPath,
|
|
3827
|
+
value: captionsArray
|
|
3828
|
+
}), {
|
|
3829
|
+
_type: "translatable.vtt.captions",
|
|
3830
|
+
translationText: "",
|
|
3831
|
+
_originalMetadata: []
|
|
3832
|
+
};
|
|
3833
|
+
devLog.info("Applying VTT caption hybrid transform", {
|
|
3834
|
+
fieldPath: context.fieldPath,
|
|
3835
|
+
count: captionsArray.length
|
|
3836
|
+
});
|
|
3837
|
+
const translationText = serializeVTTCaptionText(captionsArray), originalMetadata = captionsArray.map((caption, index) => {
|
|
3838
|
+
const { text: _text, ...metadata } = caption;
|
|
3839
|
+
return {
|
|
3840
|
+
...metadata,
|
|
3841
|
+
_originalIndex: index
|
|
3842
|
+
};
|
|
3843
|
+
});
|
|
3844
|
+
return devLog.info("VTT captions serialized:", {
|
|
3845
|
+
fieldPath: context.fieldPath,
|
|
3846
|
+
translationTextLength: translationText.length,
|
|
3847
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
|
|
3848
|
+
metadataCount: originalMetadata.length,
|
|
3849
|
+
metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
|
|
3850
|
+
}), {
|
|
3851
|
+
_type: "translatable.vtt.captions",
|
|
3852
|
+
translationText,
|
|
3853
|
+
_originalMetadata: originalMetadata
|
|
3854
|
+
};
|
|
3855
|
+
},
|
|
3856
|
+
fromTranslatable: (translatedValue, _originalValue, context) => {
|
|
3857
|
+
if (devLog.info("VTT captions fromTranslatable - input:", {
|
|
3858
|
+
fieldPath: context.fieldPath,
|
|
3859
|
+
valueType: typeof translatedValue,
|
|
3860
|
+
isCorrectType: translatedValue._type === "translatable.vtt.captions",
|
|
3861
|
+
hasText: !!translatedValue.translationText,
|
|
3862
|
+
hasMetadata: Array.isArray(translatedValue._originalMetadata)
|
|
3863
|
+
}), !translatedValue || translatedValue._type !== "translatable.vtt.captions" || !translatedValue.translationText || !Array.isArray(translatedValue._originalMetadata))
|
|
3864
|
+
return devLog.debug("Value is not a translatable VTT caption object, skipping transform", {
|
|
3865
|
+
fieldPath: context.fieldPath,
|
|
3866
|
+
valueType: typeof translatedValue,
|
|
3867
|
+
translatedValue
|
|
3868
|
+
}), [];
|
|
3869
|
+
devLog.info("Processing translated VTT captions", {
|
|
3870
|
+
fieldPath: context.fieldPath,
|
|
3871
|
+
metadataCount: translatedValue._originalMetadata.length
|
|
3872
|
+
});
|
|
3873
|
+
const { translationText, _originalMetadata } = translatedValue;
|
|
3874
|
+
devLog.info("VTT captions translationText:", {
|
|
3875
|
+
fieldPath: context.fieldPath,
|
|
3876
|
+
translationTextLength: translationText.length,
|
|
3877
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
|
|
3878
|
+
});
|
|
3879
|
+
const result = deserializeVTTCaptionText(translationText, _originalMetadata);
|
|
3880
|
+
return devLog.info("VTT captions final result:", {
|
|
3881
|
+
fieldPath: context.fieldPath,
|
|
3882
|
+
resultCount: result.length,
|
|
3883
|
+
resultExample: result.length > 0 ? result[0] : null
|
|
3884
|
+
}), result;
|
|
3885
|
+
}
|
|
3886
|
+
};
|
|
3887
|
+
function serializeVTTCaptionText(captions) {
|
|
3888
|
+
if (devLog.debug("serializeVTTCaptionText - input:", {
|
|
3889
|
+
captionsCount: captions.length,
|
|
3890
|
+
firstCaption: captions.length > 0 ? captions[0] : null
|
|
3891
|
+
}), !Array.isArray(captions) || captions.length === 0)
|
|
3892
|
+
return "";
|
|
3893
|
+
const result = captions.map((caption, index) => (devLog.debug(`Processing caption ${index}:`, {
|
|
3894
|
+
text: caption.text,
|
|
3895
|
+
hasText: typeof caption.text == "string"
|
|
3896
|
+
}), `[${index}] ${caption.text || ""}`)).join(`
|
|
3897
|
+
`);
|
|
3898
|
+
return devLog.debug("serializeVTTCaptionText - output:", {
|
|
3899
|
+
resultLength: result.length,
|
|
3900
|
+
resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
|
|
3901
|
+
}), result;
|
|
3902
|
+
}
|
|
3903
|
+
function deserializeVTTCaptionText(translationText, originalMetadata) {
|
|
3904
|
+
if (devLog.debug("deserializeVTTCaptionText - input:", {
|
|
3905
|
+
translationTextLength: translationText.length,
|
|
3906
|
+
translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
|
|
3907
|
+
metadataCount: originalMetadata.length,
|
|
3908
|
+
metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
|
|
3909
|
+
}), !translationText || translationText.trim() === "")
|
|
3910
|
+
return [];
|
|
3911
|
+
const result = [], lines = translationText.split(`
|
|
3912
|
+
`), lineRegex = /^\[(\d+)\]\s*(.*)$/;
|
|
3913
|
+
for (const line of lines) {
|
|
3914
|
+
const match = line.match(lineRegex);
|
|
3915
|
+
if (!match) {
|
|
3916
|
+
devLog.warn("Failed to parse caption line", { line });
|
|
3917
|
+
continue;
|
|
3918
|
+
}
|
|
3919
|
+
const index = Number.parseInt(match[1], 10), text = match[2];
|
|
3920
|
+
devLog.debug("Found caption:", {
|
|
3921
|
+
index,
|
|
3922
|
+
text
|
|
3923
|
+
});
|
|
3924
|
+
const metadata = originalMetadata.find((m) => m._originalIndex === index);
|
|
3925
|
+
if (metadata) {
|
|
3926
|
+
devLog.debug(`Found matching metadata for index ${index}`);
|
|
3927
|
+
const { _originalIndex, ...metadataWithoutIndex } = metadata;
|
|
3928
|
+
result.push({
|
|
3929
|
+
...metadataWithoutIndex,
|
|
3930
|
+
text
|
|
3931
|
+
});
|
|
3932
|
+
} else
|
|
3933
|
+
devLog.warn("Missing metadata for caption", { index }), result.push({
|
|
3934
|
+
_type: "vtt.cue",
|
|
3935
|
+
_key: `caption-${index}`,
|
|
3936
|
+
text,
|
|
3937
|
+
startTime: 0,
|
|
3938
|
+
endTime: 0
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3941
|
+
return result.length < originalMetadata.length && devLog.warn(`Missing captions in translation: found ${result.length} out of ${originalMetadata.length}`), devLog.debug("deserializeVTTCaptionText - output:", {
|
|
3942
|
+
resultCount: result.length,
|
|
3943
|
+
resultExample: result.length > 0 ? result[0] : null
|
|
3944
|
+
}), result;
|
|
3945
|
+
}
|
|
3946
|
+
const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
|
|
3947
|
+
globalTransformRegistry.register(transform);
|
|
3948
|
+
})), {
|
|
3240
3949
|
name: "sanity-plugin-ai-translation",
|
|
3241
3950
|
document: {
|
|
3242
3951
|
actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
|
|
@@ -3247,6 +3956,8 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3247
3956
|
})), translationHistorySchema = translationHistory;
|
|
3248
3957
|
export {
|
|
3249
3958
|
aiTranslationPlugin,
|
|
3250
|
-
|
|
3959
|
+
richTextTransform,
|
|
3960
|
+
translationHistorySchema,
|
|
3961
|
+
vttCaptionTransform
|
|
3251
3962
|
};
|
|
3252
3963
|
//# sourceMappingURL=index.js.map
|