@braze/sanity-plugin-ai-translation 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +385 -220
- package/dist/index.d.ts +52 -4
- package/dist/index.js +1670 -1543
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
import { createContext, useReducer, useEffect, useContext, useState, useCallback
|
|
1
|
+
import { getPublishedId, pathToString, useClient, defineType, defineField, definePlugin } from "sanity";
|
|
2
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { WarningOutlineIcon, SearchIcon, CheckmarkIcon, ErrorOutlineIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
|
|
4
|
+
import { createContext, useReducer, useRef, useEffect, useContext, useMemo, useState, useCallback } from "react";
|
|
5
5
|
import defaults from "lodash.defaults";
|
|
6
|
-
import { Card,
|
|
7
|
-
import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
|
|
6
|
+
import { Card, Stack, Flex, Text, Box, Heading, Button, Spinner, Inline, Checkbox, Grid, Radio, TextInput, Badge, Select, Label, Code, Tooltip } from "@sanity/ui";
|
|
8
7
|
import { toPlainText } from "@portabletext/react";
|
|
9
8
|
import { uuid } from "@sanity/uuid";
|
|
10
9
|
import cloneDeep from "lodash.clonedeep";
|
|
11
10
|
import get from "lodash.get";
|
|
12
11
|
import set from "lodash.set";
|
|
12
|
+
import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
|
|
13
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
14
|
info: (message, context) => {
|
|
15
15
|
isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
|
|
@@ -62,19 +62,66 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
62
62
|
error: error instanceof Error ? error.message : "Network error"
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
}, undraftId = (id) => id.replace(/^drafts\./, ""), activeTranslations = /* @__PURE__ */ new Map(), registerActiveTranslation = (documentId, releaseId) => {
|
|
66
|
+
const entry = {
|
|
67
|
+
state: {
|
|
68
|
+
documentId,
|
|
69
|
+
releaseId,
|
|
70
|
+
overallStatus: "translating",
|
|
71
|
+
translationJobs: /* @__PURE__ */ new Map(),
|
|
72
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
73
|
+
},
|
|
74
|
+
subscribers: /* @__PURE__ */ new Set()
|
|
75
|
+
};
|
|
76
|
+
activeTranslations.set(documentId, entry);
|
|
77
|
+
}, updateActiveTranslationStatus = (documentId, status) => {
|
|
78
|
+
const entry = activeTranslations.get(documentId);
|
|
79
|
+
entry && (entry.state.overallStatus = status, notifySubscribers(entry));
|
|
80
|
+
}, updateActiveTranslationJob = (documentId, job) => {
|
|
81
|
+
const entry = activeTranslations.get(documentId);
|
|
82
|
+
if (entry) {
|
|
83
|
+
const jobKey = `${undraftId(job._id)}_${job.language}`, existingJob = entry.state.translationJobs.get(jobKey), jobWithDefinedValues = Object.fromEntries(
|
|
84
|
+
Object.entries(job).filter(([_, value]) => value !== void 0)
|
|
85
|
+
), mergedJob = existingJob ? { ...existingJob, ...jobWithDefinedValues } : jobWithDefinedValues;
|
|
86
|
+
entry.state.translationJobs.set(jobKey, mergedJob), notifySubscribers(entry);
|
|
87
|
+
}
|
|
88
|
+
}, getActiveTranslation = (documentId) => activeTranslations.get(documentId)?.state ?? null, subscribeToActiveTranslation = (documentId, callback) => {
|
|
89
|
+
const entry = activeTranslations.get(documentId);
|
|
90
|
+
return entry ? (entry.subscribers.add(callback), callback(entry.state), () => {
|
|
91
|
+
entry.subscribers.delete(callback);
|
|
92
|
+
}) : () => {
|
|
93
|
+
};
|
|
94
|
+
}, cleanupActiveTranslation = (documentId, delayMs = 6e4) => {
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
const entry = activeTranslations.get(documentId);
|
|
97
|
+
entry && ["completed", "error"].includes(entry.state.overallStatus) && activeTranslations.delete(documentId);
|
|
98
|
+
}, delayMs);
|
|
99
|
+
}, notifySubscribers = (entry) => {
|
|
100
|
+
for (const subscriber of entry.subscribers)
|
|
101
|
+
try {
|
|
102
|
+
subscriber(entry.state);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error("Error in active translation subscriber:", error);
|
|
105
|
+
}
|
|
106
|
+
}, forceCleanupActiveTranslation = (documentId) => {
|
|
107
|
+
activeTranslations.delete(documentId);
|
|
65
108
|
}, TranslationContext = createContext({
|
|
66
109
|
document: null,
|
|
67
110
|
config: null,
|
|
68
111
|
overallStatus: "idle",
|
|
69
112
|
selectedTargetLanguages: [],
|
|
70
113
|
translationJobs: /* @__PURE__ */ new Map(),
|
|
71
|
-
shouldOverwriteFreshTranslations: !1,
|
|
72
114
|
shouldTranslateReferences: !1,
|
|
73
115
|
maxReferenceDepth: 1,
|
|
74
116
|
references: [],
|
|
117
|
+
selectedReferenceIds: /* @__PURE__ */ new Set(),
|
|
75
118
|
translationMetadata: null,
|
|
76
119
|
translationMode: "sanity-only",
|
|
77
120
|
openaiValidated: !1,
|
|
121
|
+
releaseConfig: {
|
|
122
|
+
mode: "new",
|
|
123
|
+
newReleaseTitle: ""
|
|
124
|
+
},
|
|
78
125
|
dispatch: () => {
|
|
79
126
|
}
|
|
80
127
|
}), translationReducer = (context, action) => {
|
|
@@ -85,7 +132,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
85
132
|
};
|
|
86
133
|
switch (devLog.debug("Translation action dispatched", {
|
|
87
134
|
...logContext,
|
|
88
|
-
payload: action.payload
|
|
135
|
+
payload: "payload" in action ? action.payload : void 0
|
|
89
136
|
}), action.type) {
|
|
90
137
|
case "SET_OVERALL_STATUS":
|
|
91
138
|
return { ...context, overallStatus: action.payload };
|
|
@@ -116,48 +163,106 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
116
163
|
...context,
|
|
117
164
|
maxReferenceDepth: action.payload
|
|
118
165
|
};
|
|
119
|
-
case "
|
|
166
|
+
case "SET_REFERENCES":
|
|
120
167
|
return {
|
|
121
168
|
...context,
|
|
122
|
-
|
|
169
|
+
references: action.payload
|
|
123
170
|
};
|
|
124
|
-
case "
|
|
171
|
+
case "SET_SELECTED_REFERENCE_IDS":
|
|
125
172
|
return {
|
|
126
173
|
...context,
|
|
127
|
-
|
|
174
|
+
selectedReferenceIds: action.payload
|
|
128
175
|
};
|
|
129
176
|
case "SET_TRANSLATION_JOB_STATUS": {
|
|
130
|
-
|
|
177
|
+
if (!action.payload._id)
|
|
178
|
+
return devLog.warn("SET_TRANSLATION_JOB_STATUS action missing _id", {
|
|
179
|
+
...logContext,
|
|
180
|
+
payload: action.payload
|
|
181
|
+
}), context;
|
|
182
|
+
const jobKey = `${undraftId(action.payload._id)}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs), existingJob = newTranslationJobs.get(jobKey), mergedJob = defaults(action.payload, existingJob);
|
|
131
183
|
return newTranslationJobs.set(jobKey, mergedJob), {
|
|
132
184
|
...context,
|
|
133
185
|
translationJobs: newTranslationJobs
|
|
134
186
|
};
|
|
135
187
|
}
|
|
188
|
+
case "SET_RELEASE_CONFIG":
|
|
189
|
+
return {
|
|
190
|
+
...context,
|
|
191
|
+
releaseConfig: {
|
|
192
|
+
...context.releaseConfig,
|
|
193
|
+
...action.payload
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
case "RESET_TRANSLATION_STATE":
|
|
197
|
+
return devLog.info("Resetting translation state for new translation", {
|
|
198
|
+
...logContext,
|
|
199
|
+
previousStatus: context.overallStatus,
|
|
200
|
+
previousJobCount: context.translationJobs.size
|
|
201
|
+
}), {
|
|
202
|
+
...context,
|
|
203
|
+
overallStatus: "idle",
|
|
204
|
+
translationJobs: /* @__PURE__ */ new Map(),
|
|
205
|
+
selectedReferenceIds: /* @__PURE__ */ new Set(),
|
|
206
|
+
// Clear any release conflicts from previous run
|
|
207
|
+
releaseConfig: {
|
|
208
|
+
...context.releaseConfig,
|
|
209
|
+
conflicts: void 0,
|
|
210
|
+
replaceExisting: void 0
|
|
211
|
+
}
|
|
212
|
+
};
|
|
136
213
|
default:
|
|
137
214
|
return context;
|
|
138
215
|
}
|
|
139
|
-
}, TranslationContextProvider = ({ document, config, children }) => {
|
|
216
|
+
}, generateDefaultReleaseTitle = (document) => `[AI Translation] ${document.title || document.name || document._id.split("-").slice(0, 2).join("-")}`, TranslationContextProvider = ({ document, config, children }) => {
|
|
140
217
|
devLog.debug("TranslationContextProvider initialized", {
|
|
141
218
|
documentId: document._id,
|
|
142
219
|
documentType: document._type,
|
|
143
220
|
supportedLanguages: config.supportedLanguages?.map((l) => l.id),
|
|
144
221
|
component: "TranslationContextProvider"
|
|
145
222
|
});
|
|
146
|
-
const initialContext = {
|
|
223
|
+
const defaultReleaseTitle = generateDefaultReleaseTitle(document), activeTranslation = getActiveTranslation(document._id), initialContext = {
|
|
147
224
|
document,
|
|
148
225
|
config,
|
|
149
|
-
|
|
226
|
+
// If there's an active translation, restore its state
|
|
227
|
+
overallStatus: activeTranslation?.overallStatus ?? "idle",
|
|
150
228
|
translationMetadata: null,
|
|
151
229
|
selectedTargetLanguages: [],
|
|
152
|
-
shouldOverwriteFreshTranslations: !1,
|
|
153
230
|
shouldTranslateReferences: !1,
|
|
154
231
|
maxReferenceDepth: 1,
|
|
155
232
|
references: [],
|
|
156
|
-
|
|
233
|
+
selectedReferenceIds: /* @__PURE__ */ new Set(),
|
|
234
|
+
// Restore jobs from active translation if available
|
|
235
|
+
translationJobs: activeTranslation?.translationJobs ?? /* @__PURE__ */ new Map(),
|
|
157
236
|
translationMode: config?.openai?.defaultMode || "sanity-only",
|
|
158
|
-
openaiValidated: !1
|
|
159
|
-
|
|
237
|
+
openaiValidated: !1,
|
|
238
|
+
releaseConfig: {
|
|
239
|
+
mode: "new",
|
|
240
|
+
newReleaseTitle: defaultReleaseTitle
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
activeTranslation && devLog.info("Reconnecting to active translation", {
|
|
244
|
+
documentId: document._id,
|
|
245
|
+
status: activeTranslation.overallStatus,
|
|
246
|
+
jobCount: activeTranslation.translationJobs.size,
|
|
247
|
+
component: "TranslationContextProvider"
|
|
248
|
+
});
|
|
249
|
+
const [context, dispatch] = useReducer(translationReducer, initialContext), lastProcessedRefsRef = useRef("");
|
|
160
250
|
return useEffect(() => {
|
|
251
|
+
const handleActiveTranslationUpdate = (state) => {
|
|
252
|
+
dispatch({
|
|
253
|
+
type: "SET_OVERALL_STATUS",
|
|
254
|
+
payload: state.overallStatus
|
|
255
|
+
});
|
|
256
|
+
for (const job of state.translationJobs.values())
|
|
257
|
+
dispatch({
|
|
258
|
+
type: "SET_TRANSLATION_JOB_STATUS",
|
|
259
|
+
payload: job
|
|
260
|
+
});
|
|
261
|
+
}, unsubscribe = subscribeToActiveTranslation(document._id, handleActiveTranslationUpdate);
|
|
262
|
+
return () => {
|
|
263
|
+
unsubscribe();
|
|
264
|
+
};
|
|
265
|
+
}, [document._id]), useEffect(() => {
|
|
161
266
|
(async () => {
|
|
162
267
|
if (config?.openai?.enabled && config?.openai?.apiKey)
|
|
163
268
|
try {
|
|
@@ -181,7 +286,28 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
181
286
|
});
|
|
182
287
|
}
|
|
183
288
|
})();
|
|
184
|
-
}, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]),
|
|
289
|
+
}, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), useEffect(() => {
|
|
290
|
+
const currentRefsKey = context.references.map((ref) => ref._id).sort().join(",");
|
|
291
|
+
if (currentRefsKey !== lastProcessedRefsRef.current) {
|
|
292
|
+
if (context.references.length > 0) {
|
|
293
|
+
const currentReferenceIds = new Set(context.references.map((ref) => ref._id)), previousReferenceIds = new Set(
|
|
294
|
+
lastProcessedRefsRef.current ? lastProcessedRefsRef.current.split(",") : []
|
|
295
|
+
), hasNewReferences = Array.from(currentReferenceIds).some((id) => !previousReferenceIds.has(id)), currentSelection = context.selectedReferenceIds;
|
|
296
|
+
if (currentSelection.size === 0 || hasNewReferences) {
|
|
297
|
+
const newSelection = new Set(currentSelection);
|
|
298
|
+
currentReferenceIds.forEach((id) => {
|
|
299
|
+
newSelection.add(id);
|
|
300
|
+
});
|
|
301
|
+
const currentIds = Array.from(currentSelection).sort(), newIds = Array.from(newSelection).sort();
|
|
302
|
+
(currentIds.length !== newIds.length || !currentIds.every((id, i) => id === newIds[i])) && dispatch({
|
|
303
|
+
type: "SET_SELECTED_REFERENCE_IDS",
|
|
304
|
+
payload: newSelection
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
lastProcessedRefsRef.current = currentRefsKey;
|
|
309
|
+
}
|
|
310
|
+
}, [context.references, context.selectedReferenceIds]), devLog.debug("Translation context state initialized", {
|
|
185
311
|
documentId: document._id,
|
|
186
312
|
overallStatus: context.overallStatus,
|
|
187
313
|
selectedLanguages: context.selectedTargetLanguages,
|
|
@@ -196,1044 +322,238 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
196
322
|
"useTranslationContext must be used within a TranslationContextProvider with document and config props"
|
|
197
323
|
);
|
|
198
324
|
return context;
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
325
|
+
}, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
326
|
+
(translation) => translation._key === targetLanguage
|
|
327
|
+
)?.value._ref || null, TRANSLATION_METADATA_TYPE = "translation.metadata", getTranslationMetadata = async ({
|
|
328
|
+
client,
|
|
329
|
+
sourceDocument,
|
|
330
|
+
sourceLanguage
|
|
331
|
+
}) => {
|
|
332
|
+
try {
|
|
333
|
+
return await client.fetch(
|
|
334
|
+
"*[_type == $tmType && references($sourceDocumentId)][0]",
|
|
335
|
+
{
|
|
336
|
+
tmType: TRANSLATION_METADATA_TYPE,
|
|
337
|
+
sourceDocumentId: undraftId(sourceDocument._id)
|
|
338
|
+
}
|
|
339
|
+
) || await client.create({
|
|
340
|
+
_type: TRANSLATION_METADATA_TYPE,
|
|
341
|
+
translations: [
|
|
342
|
+
{
|
|
343
|
+
_key: sourceLanguage,
|
|
344
|
+
_type: "internationalizedArrayReferenceValue",
|
|
345
|
+
value: {
|
|
346
|
+
_type: "reference",
|
|
347
|
+
_ref: undraftId(sourceDocument._id),
|
|
348
|
+
_weak: !0,
|
|
349
|
+
_strengthenOnPublish: {
|
|
350
|
+
_type: sourceDocument._type,
|
|
351
|
+
_weak: !1
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
],
|
|
356
|
+
translationHistory: []
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
|
|
360
|
+
}
|
|
361
|
+
}, checkDocumentConflicts = async ({
|
|
362
|
+
client,
|
|
363
|
+
releaseId,
|
|
364
|
+
documentId,
|
|
365
|
+
documentTitle,
|
|
366
|
+
documentType,
|
|
367
|
+
targetLanguages,
|
|
368
|
+
languageFieldName
|
|
369
|
+
}) => {
|
|
370
|
+
const conflicts = [], document = await client.withConfig({ perspective: "drafts" }).getDocument(documentId);
|
|
371
|
+
if (!document)
|
|
372
|
+
return conflicts;
|
|
373
|
+
const sourceLanguage = document[languageFieldName], translationMetadata = await getTranslationMetadata({
|
|
374
|
+
client,
|
|
375
|
+
sourceDocument: document,
|
|
376
|
+
sourceLanguage
|
|
206
377
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
378
|
+
for (const targetLanguage of targetLanguages) {
|
|
379
|
+
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage);
|
|
380
|
+
if (existingTranslationId) {
|
|
381
|
+
const versionedId = `versions.${releaseId}.${existingTranslationId}`;
|
|
382
|
+
await client.getDocument(versionedId) && (conflicts.push({
|
|
383
|
+
sourceDocumentId: documentId,
|
|
384
|
+
sourceDocumentTitle: documentTitle || document.title || document.name,
|
|
385
|
+
sourceDocumentType: documentType || document._type,
|
|
386
|
+
targetLanguage,
|
|
387
|
+
existingVersionId: versionedId
|
|
388
|
+
}), devLog.debug("Found existing version in release", {
|
|
389
|
+
releaseId,
|
|
390
|
+
sourceDocumentId: documentId,
|
|
391
|
+
targetLanguage,
|
|
392
|
+
versionedId
|
|
393
|
+
}));
|
|
219
394
|
}
|
|
220
|
-
dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
|
|
221
|
-
}, [data, loading, error, dispatch]), {
|
|
222
|
-
data,
|
|
223
|
-
loading,
|
|
224
|
-
error
|
|
225
|
-
};
|
|
226
|
-
};
|
|
227
|
-
async function validateSuggestion(client, suggestion) {
|
|
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" };
|
|
229
|
-
}
|
|
230
|
-
async function validateGlossaryTerm(client, suggestion) {
|
|
231
|
-
const { sourceValue, targetValue, sourceLocale, targetLocale } = suggestion;
|
|
232
|
-
if (!sourceValue || !targetValue || !sourceLocale || !targetLocale)
|
|
233
|
-
return {
|
|
234
|
-
valid: !1,
|
|
235
|
-
status: "invalid",
|
|
236
|
-
reason: "Missing required fields for glossary term"
|
|
237
|
-
};
|
|
238
|
-
if (sourceValue.trim() === targetValue.trim())
|
|
239
|
-
return {
|
|
240
|
-
valid: !1,
|
|
241
|
-
status: "invalid",
|
|
242
|
-
reason: `Source and target are identical ("${sourceValue}") - this should be a protected phrase instead`
|
|
243
|
-
};
|
|
244
|
-
const protectedPhrases = await client.fetch('*[_id == "translation-settings"][0].protectedPhrases');
|
|
245
|
-
if (protectedPhrases?.some((phrase) => phrase.toLowerCase() === sourceValue.toLowerCase()))
|
|
246
|
-
return {
|
|
247
|
-
valid: !1,
|
|
248
|
-
status: "invalid",
|
|
249
|
-
reason: `Cannot add "${sourceValue}" to glossary because it is already a protected phrase. Protected phrases are never translated. If you want to create a translation mapping for this term, please remove it from protected phrases first by editing Translation Settings.`
|
|
250
|
-
};
|
|
251
|
-
if (targetValue && protectedPhrases?.some((phrase) => phrase.toLowerCase() === targetValue.toLowerCase()))
|
|
252
|
-
return {
|
|
253
|
-
valid: !1,
|
|
254
|
-
status: "invalid",
|
|
255
|
-
reason: `Cannot use "${targetValue}" as a translation target because it is a protected phrase. Protected phrases should not appear as translation targets.`
|
|
256
|
-
};
|
|
257
|
-
const existingTerm = await client.fetch(
|
|
258
|
-
`*[_type == "translationGlossaryTerm" && count(translations[_key == $sourceLocale && value == $sourceValue]) > 0][0] {
|
|
259
|
-
translations
|
|
260
|
-
}`,
|
|
261
|
-
{ sourceLocale, sourceValue }
|
|
262
|
-
);
|
|
263
|
-
if (existingTerm) {
|
|
264
|
-
const existingTarget = existingTerm.translations.find((t) => t._key === targetLocale);
|
|
265
|
-
if (existingTarget?.value === targetValue)
|
|
266
|
-
return {
|
|
267
|
-
valid: !0,
|
|
268
|
-
status: "warning",
|
|
269
|
-
reason: `This translation already exists in the glossary: "${sourceValue}" \u2192 "${targetValue}"`
|
|
270
|
-
};
|
|
271
|
-
if (existingTarget && existingTarget.value !== targetValue)
|
|
272
|
-
return {
|
|
273
|
-
valid: !0,
|
|
274
|
-
status: "warning",
|
|
275
|
-
reason: `"${sourceValue}" already has a different translation in the glossary: "${existingTarget.value}". Applying this will update it to "${targetValue}".`
|
|
276
|
-
};
|
|
277
395
|
}
|
|
278
|
-
return
|
|
279
|
-
}
|
|
280
|
-
async function validateProtectedPhrase(client, suggestion) {
|
|
281
|
-
const phrase = suggestion.description.trim();
|
|
282
|
-
if (!phrase)
|
|
283
|
-
return {
|
|
284
|
-
valid: !1,
|
|
285
|
-
status: "invalid",
|
|
286
|
-
reason: "Protected phrase cannot be empty"
|
|
287
|
-
};
|
|
288
|
-
if ((await client.fetch('*[_id == "translation-settings"][0].protectedPhrases'))?.some((p) => p.toLowerCase() === phrase.toLowerCase()))
|
|
289
|
-
return {
|
|
290
|
-
valid: !0,
|
|
291
|
-
status: "warning",
|
|
292
|
-
reason: `"${phrase}" is already in protected phrases`
|
|
293
|
-
};
|
|
294
|
-
const glossaryTermWithPhrase = await client.fetch(
|
|
295
|
-
`*[_type == "translationGlossaryTerm" && count(translations[value == $phrase]) > 0][0] {
|
|
296
|
-
_id,
|
|
297
|
-
translations
|
|
298
|
-
}`,
|
|
299
|
-
{ phrase }
|
|
300
|
-
);
|
|
301
|
-
if (glossaryTermWithPhrase) {
|
|
302
|
-
const matchingTranslations = glossaryTermWithPhrase.translations.filter((t) => t.value.toLowerCase() === phrase.toLowerCase()).map((t) => t._key).join(", ");
|
|
303
|
-
return {
|
|
304
|
-
valid: !1,
|
|
305
|
-
status: "invalid",
|
|
306
|
-
reason: `Cannot add "${phrase}" as a protected phrase because it exists in the glossary as a translation (${matchingTranslations}). If you want to prevent this term from being translated, please remove it from the glossary first by editing Translation Settings.`
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
return { valid: !0, status: "valid" };
|
|
310
|
-
}
|
|
311
|
-
function validatePrompt(suggestion) {
|
|
312
|
-
const update = suggestion.description.trim();
|
|
313
|
-
return update ? update.toLowerCase().includes("has been") || update.toLowerCase().includes("was changed") || update.toLowerCase().includes("corrected to") ? {
|
|
314
|
-
valid: !0,
|
|
315
|
-
status: "warning",
|
|
316
|
-
reason: "This update may be describing past changes rather than adding new rules"
|
|
317
|
-
} : { valid: !0, status: "valid" } : {
|
|
318
|
-
valid: !1,
|
|
319
|
-
status: "invalid",
|
|
320
|
-
reason: "Prompt update cannot be empty"
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
async function analyzeFeedback({
|
|
324
|
-
client,
|
|
325
|
-
sourceLanguage,
|
|
326
|
-
targetLanguage,
|
|
327
|
-
userFeedback,
|
|
328
|
-
shouldIncludeEdits,
|
|
329
|
-
sourceSnapshot,
|
|
330
|
-
targetSnapshot,
|
|
331
|
-
currentDocument,
|
|
332
|
-
onProgress
|
|
333
|
-
}) {
|
|
334
|
-
onProgress?.({ stage: "analyzing", message: "Analyzing feedback and edits..." });
|
|
335
|
-
const analysisPrompt = buildAnalysisPrompt({
|
|
336
|
-
sourceLanguage,
|
|
337
|
-
targetLanguage,
|
|
338
|
-
userFeedback,
|
|
339
|
-
shouldIncludeEdits,
|
|
340
|
-
sourceSnapshot,
|
|
341
|
-
targetSnapshot,
|
|
342
|
-
currentDocument
|
|
343
|
-
}), analysis = await analyzeWithAI(client, analysisPrompt), suggestions = convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage);
|
|
344
|
-
onProgress?.({ stage: "analyzing", message: "Validating suggestions..." });
|
|
345
|
-
const validatedSuggestions = await Promise.all(
|
|
346
|
-
suggestions.map(async (suggestion) => {
|
|
347
|
-
const validation = await validateSuggestion(client, suggestion);
|
|
348
|
-
return {
|
|
349
|
-
...suggestion,
|
|
350
|
-
warning: validation.reason,
|
|
351
|
-
validationStatus: validation.status,
|
|
352
|
-
// Only auto-approve valid suggestions; invalid ones cannot be approved
|
|
353
|
-
approved: validation.status === "valid"
|
|
354
|
-
};
|
|
355
|
-
})
|
|
356
|
-
);
|
|
357
|
-
return devLog.info("Feedback analysis complete", {
|
|
358
|
-
sourceLanguage,
|
|
359
|
-
targetLanguage,
|
|
360
|
-
total: validatedSuggestions.length,
|
|
361
|
-
valid: validatedSuggestions.filter((s) => s.validationStatus === "valid").length,
|
|
362
|
-
warnings: validatedSuggestions.filter((s) => s.validationStatus === "warning").length,
|
|
363
|
-
invalid: validatedSuggestions.filter((s) => s.validationStatus === "invalid").length
|
|
364
|
-
}), validatedSuggestions;
|
|
365
|
-
}
|
|
366
|
-
async function applySuggestions({
|
|
396
|
+
return conflicts;
|
|
397
|
+
}, checkReleaseConflicts = async ({
|
|
367
398
|
client,
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}) {
|
|
374
|
-
|
|
375
|
-
throw devLog.error("targetLanguage is undefined in applySuggestions!", {
|
|
376
|
-
sourceLanguage,
|
|
377
|
-
targetLanguage,
|
|
378
|
-
suggestionsCount: suggestions.length
|
|
379
|
-
}), new Error("targetLanguage is required but was undefined");
|
|
380
|
-
const approvedSuggestions = suggestions.filter((s) => s.approved);
|
|
381
|
-
approvedSuggestions.length !== 0 && (onProgress?.({ stage: "applying", message: "Applying updates to translation settings..." }), await applyImprovementsFromSuggestions(client, schemaId, targetLanguage, approvedSuggestions, onProgress), devLog.info("Suggestions applied successfully", {
|
|
382
|
-
sourceLanguage,
|
|
383
|
-
targetLanguage,
|
|
384
|
-
count: approvedSuggestions.length
|
|
385
|
-
}));
|
|
386
|
-
}
|
|
387
|
-
function buildAnalysisPrompt({
|
|
388
|
-
sourceLanguage,
|
|
389
|
-
targetLanguage,
|
|
390
|
-
userFeedback,
|
|
391
|
-
shouldIncludeEdits,
|
|
392
|
-
sourceSnapshot,
|
|
393
|
-
targetSnapshot,
|
|
394
|
-
currentDocument
|
|
395
|
-
}) {
|
|
396
|
-
const parts = [
|
|
397
|
-
"You are analyzing feedback about AI translation quality to improve future translations.",
|
|
398
|
-
`Source language: ${sourceLanguage}`,
|
|
399
|
-
`Target language: ${targetLanguage}`,
|
|
400
|
-
"",
|
|
401
|
-
"Your task is to analyze the feedback and infer specific improvements to:",
|
|
402
|
-
"1. Global translation prompt (general guidance for all translations)",
|
|
403
|
-
"2. Locale-specific prompt (guidance specific to this target language)",
|
|
404
|
-
"3. Protected phrases (terms that should never be translated)",
|
|
405
|
-
"4. Glossary terms (specific term-to-term translation mappings)",
|
|
406
|
-
""
|
|
407
|
-
];
|
|
408
|
-
return userFeedback && parts.push("USER FEEDBACK:", userFeedback, ""), shouldIncludeEdits && sourceSnapshot && targetSnapshot && currentDocument && parts.push(
|
|
409
|
-
"EDIT ANALYSIS:",
|
|
410
|
-
"Compare the original translation with the edited version to understand what the user changed.",
|
|
411
|
-
"",
|
|
412
|
-
"Original source document:",
|
|
413
|
-
sourceSnapshot,
|
|
414
|
-
"",
|
|
415
|
-
"Original AI translation:",
|
|
416
|
-
targetSnapshot,
|
|
417
|
-
"",
|
|
418
|
-
"Current edited document:",
|
|
419
|
-
JSON.stringify(currentDocument, null, 2),
|
|
420
|
-
""
|
|
421
|
-
), parts.push(
|
|
422
|
-
"INSTRUCTIONS:",
|
|
423
|
-
"Analyze the feedback and/or edits to identify patterns and improvements.",
|
|
424
|
-
"",
|
|
425
|
-
"For edits, look for:",
|
|
426
|
-
"- Terminology changes that suggest glossary additions",
|
|
427
|
-
"- Phrases that were protected/unprotected incorrectly",
|
|
428
|
-
"- Style, tone, or formatting issues that suggest prompt improvements",
|
|
429
|
-
"- Consistent patterns across multiple changes",
|
|
430
|
-
"",
|
|
431
|
-
"IMPORTANT RULES:",
|
|
432
|
-
"1. DO NOT create redundant updates. Choose ONE mechanism for each issue:",
|
|
433
|
-
" - Protected phrases = terms that should NEVER be translated (language-agnostic)",
|
|
434
|
-
' - Glossary terms = specific translation mappings (e.g., "chat" \u2192 "cat" in French)',
|
|
435
|
-
" - Locale prompts = guidance specific to this source\u2192target language pair",
|
|
436
|
-
" - Global prompts = guidance that applies to ALL translations",
|
|
437
|
-
"",
|
|
438
|
-
"2. Be locale-specific:",
|
|
439
|
-
` - This feedback is specifically about ${sourceLanguage} \u2192 ${targetLanguage} translation`,
|
|
440
|
-
" - DO NOT assume rules apply to other language pairs unless explicitly stated",
|
|
441
|
-
" - Prefer locale-specific prompts over global prompts unless universally applicable",
|
|
442
|
-
"",
|
|
443
|
-
"3. Protected phrases vs. Locale prompts:",
|
|
444
|
-
" - Use protected phrases ONLY for brand names, product names, proper nouns, technical terms that should NEVER be translated in ANY language",
|
|
445
|
-
' - Use locale prompts for language-pair-specific guidance (e.g., "Keep technical terms in English when translating to German")',
|
|
446
|
-
" - NEVER mention specific terms in both protected phrases AND prompts",
|
|
447
|
-
"",
|
|
448
|
-
"4. Glossary vs. Protected phrases:",
|
|
449
|
-
' - Glossary = "Always translate X as Y" (provides the correct translation)',
|
|
450
|
-
' - Protected = "Never translate X" (keeps it as-is)',
|
|
451
|
-
" - CRITICAL: If sourceValue === targetValue, use PROTECTED PHRASES, NOT glossary",
|
|
452
|
-
" - Only use glossary when the term actually changes between languages",
|
|
453
|
-
" - Use protected when the term should remain unchanged across all languages",
|
|
454
|
-
"",
|
|
455
|
-
"Output a JSON object with this structure:",
|
|
456
|
-
"{",
|
|
457
|
-
' "globalPromptUpdates": "string | null - ONLY if this applies to ALL language pairs",',
|
|
458
|
-
` "localePromptUpdates": "string | null - ONLY if this is specific to ${sourceLanguage}\u2192${targetLanguage}",`,
|
|
459
|
-
' "protectedPhrasesToAdd": ["ONLY brand names, products, proper nouns that should NEVER translate"],',
|
|
460
|
-
' "glossaryTermsToAdd": [{"sourceLocale": "sourceLocale","sourceValue": "term", "targetLocale": "targetLocale", "targetValue": "translation"} - ONLY specific term mappings],',
|
|
461
|
-
' "reasoning": "string - Explain your analysis and which mechanism you chose for each issue"',
|
|
462
|
-
"}",
|
|
463
|
-
"",
|
|
464
|
-
"Be conservative: only suggest changes when there is clear evidence.",
|
|
465
|
-
"If in doubt, prefer locale-specific updates over global updates.",
|
|
466
|
-
"Avoid redundancy - if you add a protected phrase, do NOT mention it in prompts.",
|
|
467
|
-
"",
|
|
468
|
-
"CONCRETE EXAMPLES:",
|
|
469
|
-
"",
|
|
470
|
-
"Example 1 - Brand name protection:",
|
|
471
|
-
'User feedback: "Braze should never be translated"',
|
|
472
|
-
"Correct output:",
|
|
473
|
-
"{",
|
|
474
|
-
' "protectedPhrasesToAdd": ["Braze"],',
|
|
475
|
-
' "reasoning": "User explicitly stated Braze is a brand name that should not be translated"',
|
|
476
|
-
"}",
|
|
477
|
-
"WRONG: Do not add this to glossary or prompts.",
|
|
478
|
-
"",
|
|
479
|
-
"Example 2 - Terminology correction:",
|
|
480
|
-
'Edit analysis: User changed "discussion" to "chat" consistently in French translation',
|
|
481
|
-
"Correct output:",
|
|
482
|
-
"{",
|
|
483
|
-
` "glossaryTermsToAdd": [{"sourceLocale": "${sourceLanguage}", "sourceValue": "discussion", "targetLocale": "${targetLanguage}", "targetValue": "chat"}],`,
|
|
484
|
-
' "reasoning": "User consistently changed discussion\u2192chat, indicating preferred terminology"',
|
|
485
|
-
"}",
|
|
486
|
-
"WRONG: Do not add to protected phrases (values differ) or prompts (specific mapping).",
|
|
487
|
-
"",
|
|
488
|
-
"Example 3 - Style guidance:",
|
|
489
|
-
'User feedback: "Translation is too formal, use casual tone"',
|
|
490
|
-
"Correct output:",
|
|
491
|
-
"{",
|
|
492
|
-
` "localePromptUpdates": "Use a casual, conversational tone when translating to ${targetLanguage}. Avoid overly formal language.",`,
|
|
493
|
-
' "reasoning": "This is style guidance specific to this language pair"',
|
|
494
|
-
"}",
|
|
495
|
-
"WRONG: Do not make global (applies only to this target language).",
|
|
496
|
-
"",
|
|
497
|
-
"Example 4 - Technical term in English:",
|
|
498
|
-
`User feedback: "Keep technical terms like 'API' and 'webhook' in English"`,
|
|
499
|
-
"Correct output:",
|
|
500
|
-
"{",
|
|
501
|
-
` "localePromptUpdates": "When translating to ${targetLanguage}, keep technical terms like 'API', 'webhook', and similar technical jargon in English.",`,
|
|
502
|
-
' "reasoning": "This is language-pair-specific guidance about handling technical terms"',
|
|
503
|
-
"}",
|
|
504
|
-
'WRONG: Do not add "API" or "webhook" to protected phrases (already covered by prompt guidance).',
|
|
505
|
-
"",
|
|
506
|
-
"Example 5 - Multiple issues:",
|
|
507
|
-
'User feedback: "MyApp is our product name. Also, use more concise language."',
|
|
508
|
-
"Correct output:",
|
|
509
|
-
"{",
|
|
510
|
-
' "protectedPhrasesToAdd": ["MyApp"],',
|
|
511
|
-
` "localePromptUpdates": "Use concise language when translating to ${targetLanguage}. Prefer shorter phrases over lengthy explanations.",`,
|
|
512
|
-
' "reasoning": "MyApp is a product name (protected). Conciseness is a style preference for this language pair (locale prompt)."',
|
|
513
|
-
"}",
|
|
514
|
-
"",
|
|
515
|
-
"Example 6 - Identical source and target:",
|
|
516
|
-
'Edit analysis: User kept "dashboard" as "dashboard" in Spanish',
|
|
517
|
-
"Correct output:",
|
|
518
|
-
"{",
|
|
519
|
-
' "protectedPhrasesToAdd": ["dashboard"],',
|
|
520
|
-
' "reasoning": "User kept the term unchanged, indicating it should not be translated"',
|
|
521
|
-
"}",
|
|
522
|
-
"WRONG: Do not add to glossary when source === target."
|
|
523
|
-
), parts.join(`
|
|
524
|
-
`);
|
|
525
|
-
}
|
|
526
|
-
function convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage) {
|
|
527
|
-
const suggestions = [];
|
|
528
|
-
if (analysis.globalPromptUpdates && suggestions.push({
|
|
529
|
-
id: crypto.randomUUID(),
|
|
530
|
-
type: "globalPrompt",
|
|
531
|
-
description: analysis.globalPromptUpdates,
|
|
532
|
-
approved: !0
|
|
533
|
-
}), analysis.localePromptUpdates && suggestions.push({
|
|
534
|
-
id: crypto.randomUUID(),
|
|
535
|
-
type: "localePrompt",
|
|
536
|
-
description: analysis.localePromptUpdates,
|
|
537
|
-
approved: !0
|
|
538
|
-
}), analysis.protectedPhrasesToAdd)
|
|
539
|
-
for (const phrase of analysis.protectedPhrasesToAdd)
|
|
540
|
-
suggestions.push({
|
|
541
|
-
id: crypto.randomUUID(),
|
|
542
|
-
type: "protectedPhrase",
|
|
543
|
-
description: phrase,
|
|
544
|
-
approved: !0
|
|
545
|
-
});
|
|
546
|
-
if (analysis.glossaryTermsToAdd)
|
|
547
|
-
for (const term of analysis.glossaryTermsToAdd)
|
|
548
|
-
suggestions.push({
|
|
549
|
-
id: crypto.randomUUID(),
|
|
550
|
-
type: "glossaryTerm",
|
|
551
|
-
description: `${term.sourceValue} \u2192 ${term.targetValue}`,
|
|
552
|
-
approved: !0,
|
|
553
|
-
// Use explicit source/target languages from history instead of AI-provided values
|
|
554
|
-
sourceLocale: sourceLanguage,
|
|
555
|
-
sourceValue: term.sourceValue,
|
|
556
|
-
targetLocale: targetLanguage,
|
|
557
|
-
targetValue: term.targetValue
|
|
558
|
-
});
|
|
559
|
-
return suggestions;
|
|
560
|
-
}
|
|
561
|
-
async function analyzeWithAI(client, prompt) {
|
|
399
|
+
releaseId,
|
|
400
|
+
sourceDocument,
|
|
401
|
+
targetLanguages,
|
|
402
|
+
languageFieldName,
|
|
403
|
+
references = []
|
|
404
|
+
}) => {
|
|
405
|
+
const allConflicts = [];
|
|
562
406
|
try {
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
407
|
+
const documentsToCheck = [
|
|
408
|
+
{
|
|
409
|
+
_id: sourceDocument._id,
|
|
410
|
+
_type: sourceDocument._type,
|
|
411
|
+
title: sourceDocument.title || sourceDocument.name
|
|
412
|
+
},
|
|
413
|
+
...references.map((ref) => ({
|
|
414
|
+
_id: ref._id,
|
|
415
|
+
_type: ref._type,
|
|
416
|
+
title: ref.title
|
|
417
|
+
}))
|
|
418
|
+
], conflictChecks = await Promise.all(
|
|
419
|
+
documentsToCheck.map(
|
|
420
|
+
(doc) => checkDocumentConflicts({
|
|
421
|
+
client,
|
|
422
|
+
releaseId,
|
|
423
|
+
documentId: doc._id,
|
|
424
|
+
documentTitle: doc.title,
|
|
425
|
+
documentType: doc._type,
|
|
426
|
+
targetLanguages,
|
|
427
|
+
languageFieldName
|
|
428
|
+
})
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
for (const conflicts of conflictChecks)
|
|
432
|
+
allConflicts.push(...conflicts);
|
|
433
|
+
devLog.debug("Release conflict check complete", {
|
|
434
|
+
releaseId,
|
|
435
|
+
documentsChecked: documentsToCheck.length,
|
|
436
|
+
conflictsFound: allConflicts.length
|
|
566
437
|
});
|
|
567
|
-
devLog.debug("AI response received", { responseType: typeof response });
|
|
568
|
-
const responseText = typeof response == "string" ? response : response && typeof response == "object" ? JSON.stringify(response) : (() => {
|
|
569
|
-
throw new Error(`Unexpected response type: ${typeof response}`);
|
|
570
|
-
})();
|
|
571
|
-
return parseAIResponse(responseText);
|
|
572
438
|
} catch (error) {
|
|
573
|
-
|
|
439
|
+
devLog.error("Error checking release conflicts", error instanceof Error ? error : new Error(String(error)), {
|
|
440
|
+
releaseId,
|
|
441
|
+
sourceDocumentId: sourceDocument._id,
|
|
442
|
+
targetLanguages,
|
|
443
|
+
referencesCount: references.length
|
|
444
|
+
});
|
|
574
445
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
|
583
|
-
if (!jsonMatch) throw new Error("No JSON found in AI response");
|
|
584
|
-
parsed = JSON.parse(jsonMatch[0]);
|
|
585
|
-
}
|
|
586
|
-
if (!parsed || typeof parsed != "object")
|
|
587
|
-
throw new Error("Response is not a valid object");
|
|
588
|
-
const analysis = parsed;
|
|
589
|
-
if (typeof analysis.reasoning != "string")
|
|
590
|
-
throw new Error("Invalid response structure: missing reasoning");
|
|
591
|
-
return {
|
|
592
|
-
globalPromptUpdates: analysis.globalPromptUpdates || void 0,
|
|
593
|
-
localePromptUpdates: analysis.localePromptUpdates || void 0,
|
|
594
|
-
protectedPhrasesToAdd: Array.isArray(analysis.protectedPhrasesToAdd) ? analysis.protectedPhrasesToAdd : [],
|
|
595
|
-
glossaryTermsToAdd: Array.isArray(analysis.glossaryTermsToAdd) ? analysis.glossaryTermsToAdd : [],
|
|
596
|
-
reasoning: analysis.reasoning
|
|
597
|
-
};
|
|
598
|
-
} catch (error) {
|
|
599
|
-
throw devLog.error("Error parsing AI response", { error, responseText }), new Error("Failed to parse AI response as JSON");
|
|
446
|
+
return allConflicts;
|
|
447
|
+
};
|
|
448
|
+
function isNetworkError(error) {
|
|
449
|
+
if (!error) return !1;
|
|
450
|
+
if (error instanceof TypeError) {
|
|
451
|
+
const message = error.message.toLowerCase();
|
|
452
|
+
return message.includes("fetch") || message.includes("network") || message.includes("connection") || message.includes("failed to fetch") || message.includes("networkerror");
|
|
600
453
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
await client.fetch("*[_id == $id][0]", { id: settingsId }) || await client.createIfNotExists({
|
|
605
|
-
_id: settingsId,
|
|
606
|
-
_type: "translationSettings"
|
|
607
|
-
});
|
|
608
|
-
const updates = [], globalPrompts = suggestions.filter((s) => s.type === "globalPrompt" && s.description.trim()), localePrompts = suggestions.filter((s) => s.type === "localePrompt" && s.description.trim()), protectedPhrases = suggestions.filter((s) => s.type === "protectedPhrase" && s.description.trim()), glossaryTerms = suggestions.filter(
|
|
609
|
-
(s) => s.type === "glossaryTerm" && s.sourceLocale?.trim() && s.sourceValue?.trim() && s.targetLocale?.trim() && s.targetValue?.trim()
|
|
610
|
-
);
|
|
611
|
-
if ((globalPrompts.length > 0 || localePrompts.length > 0) && (onProgress?.({
|
|
612
|
-
stage: "applying",
|
|
613
|
-
message: globalPrompts.length > 0 && localePrompts.length > 0 ? "Updating global and locale translation prompts..." : globalPrompts.length > 0 ? "Updating global translation prompt..." : `Updating ${targetLanguage} translation prompt...`
|
|
614
|
-
}), updates.push(updatePromptsWithTemplate(client, schemaId, settingsId, targetLanguage, globalPrompts, localePrompts))), protectedPhrases.length > 0) {
|
|
615
|
-
onProgress?.({ stage: "applying", message: "Adding protected phrases..." });
|
|
616
|
-
const phrases = protectedPhrases.map((s) => s.description);
|
|
617
|
-
updates.push(addProtectedPhrases(client, settingsId, phrases));
|
|
454
|
+
if (error && typeof error == "object" && "code" in error) {
|
|
455
|
+
const code = error.code;
|
|
456
|
+
return ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "ECONNREFUSED", "EHOSTUNREACH"].includes(code);
|
|
618
457
|
}
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
sourceLocale: s.sourceLocale,
|
|
624
|
-
sourceValue: s.sourceValue,
|
|
625
|
-
targetLocale: s.targetLocale,
|
|
626
|
-
targetValue: s.targetValue
|
|
627
|
-
// biome-ignore-end lint/style/noNonNullAssertion: safely handled
|
|
628
|
-
}));
|
|
629
|
-
updates.push(addGlossaryTerms(client, terms));
|
|
458
|
+
if (error && typeof error == "object" && "response" in error) {
|
|
459
|
+
const response = error.response;
|
|
460
|
+
if (response?.status)
|
|
461
|
+
return response.status >= 500 || response.status === 429;
|
|
630
462
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
463
|
+
if (error instanceof Error) {
|
|
464
|
+
const message = error.message;
|
|
465
|
+
if (/\b(50[0-9]|429)\b/.test(message))
|
|
466
|
+
return !0;
|
|
467
|
+
}
|
|
468
|
+
return !1;
|
|
637
469
|
}
|
|
638
|
-
|
|
639
|
-
if (!
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
470
|
+
function isSanityMutationError(error) {
|
|
471
|
+
if (!error) return !1;
|
|
472
|
+
if (error instanceof Error) {
|
|
473
|
+
const message = error.message.toLowerCase();
|
|
474
|
+
return [
|
|
475
|
+
"validation",
|
|
476
|
+
"schema",
|
|
477
|
+
"invalid document",
|
|
478
|
+
"invalid field",
|
|
479
|
+
"type mismatch",
|
|
480
|
+
"reference",
|
|
481
|
+
"patch",
|
|
482
|
+
"mutation",
|
|
483
|
+
"document structure",
|
|
484
|
+
"required field",
|
|
485
|
+
"field type",
|
|
486
|
+
"sanity"
|
|
487
|
+
].some((pattern) => message.includes(pattern));
|
|
656
488
|
}
|
|
657
|
-
if (
|
|
658
|
-
const
|
|
659
|
-
if (
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
` : "",
|
|
664
|
-
`## User Feedback (${timestamp})`,
|
|
665
|
-
...updates.map((update) => `- ${update}`)
|
|
666
|
-
].filter(Boolean).join(`
|
|
667
|
-
`), localeIndex = existingLocalePrompts.findIndex((lp) => lp._key === targetLanguage);
|
|
668
|
-
await client.patch(settingsId).set({ [`localePrompts[${localeIndex}].value`]: newValue }).commit();
|
|
669
|
-
} else {
|
|
670
|
-
const newValue = [`## User Feedback (${timestamp})`, ...updates.map((update) => `- ${update}`)].filter(Boolean).join(`
|
|
671
|
-
`);
|
|
672
|
-
await client.patch(settingsId).setIfMissing({ localePrompts: [] }).append("localePrompts", [
|
|
673
|
-
{
|
|
674
|
-
_type: "internationalizedArrayTextValue",
|
|
675
|
-
_key: targetLanguage,
|
|
676
|
-
value: newValue
|
|
677
|
-
}
|
|
678
|
-
]).commit();
|
|
489
|
+
if (error && typeof error == "object") {
|
|
490
|
+
const errorObj = error;
|
|
491
|
+
if (("statusCode" in errorObj || "response" in errorObj || "details" in errorObj) && "statusCode" in errorObj) {
|
|
492
|
+
const statusCode = errorObj.statusCode;
|
|
493
|
+
if (statusCode >= 400 && statusCode < 500 && statusCode !== 429)
|
|
494
|
+
return !0;
|
|
679
495
|
}
|
|
680
496
|
}
|
|
681
|
-
|
|
682
|
-
targetLanguage,
|
|
683
|
-
globalUpdated: globalPrompts.length > 0,
|
|
684
|
-
localeUpdated: localePrompts.length > 0
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
async function addProtectedPhrases(client, settingsId, phrases) {
|
|
688
|
-
const currentPhrases = await client.fetch("*[_id == $settingsId][0].protectedPhrases", { settingsId });
|
|
689
|
-
let newPhrases = phrases.filter((phrase) => !currentPhrases?.includes(phrase));
|
|
690
|
-
const conflictingPhrases = [];
|
|
691
|
-
for (const phrase of newPhrases)
|
|
692
|
-
await client.fetch(
|
|
693
|
-
'count(*[_type == "translationGlossaryTerm" && count(translations[value == $phrase]) > 0]) > 0',
|
|
694
|
-
{ phrase }
|
|
695
|
-
) && (conflictingPhrases.push(phrase), devLog.warn("Skipping protected phrase due to glossary conflict", { phrase }));
|
|
696
|
-
newPhrases = newPhrases.filter((phrase) => !conflictingPhrases.includes(phrase)), newPhrases.length !== 0 && (await client.patch(settingsId).setIfMissing({ protectedPhrases: [] }).append("protectedPhrases", newPhrases).commit(), devLog.info("Protected phrases added", { count: newPhrases.length }));
|
|
497
|
+
return !1;
|
|
697
498
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (validTerms.length === 0) {
|
|
710
|
-
devLog.info("No valid glossary terms to add");
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
for (const { sourceLocale, sourceValue, targetLocale, targetValue } of validTerms) {
|
|
714
|
-
const existingTerm = await client.fetch(
|
|
715
|
-
`*[_type == "translationGlossaryTerm" && count(translations[_key == $sourceLocale && value == $sourceValue]) > 0
|
|
716
|
-
][0] {
|
|
717
|
-
_id,
|
|
718
|
-
translations
|
|
719
|
-
}`,
|
|
720
|
-
{ sourceLocale, sourceValue }
|
|
721
|
-
);
|
|
722
|
-
if (existingTerm) {
|
|
723
|
-
const existingTargetIndex = existingTerm.translations.findIndex((t) => t._key === targetLocale), existingTargetValue = existingTargetIndex >= 0 ? existingTerm.translations[existingTargetIndex].value : null;
|
|
724
|
-
existingTargetIndex === -1 ? await client.patch(existingTerm._id).append("translations", [
|
|
725
|
-
{
|
|
726
|
-
_type: "internationalizedArrayStringValue",
|
|
727
|
-
_key: targetLocale,
|
|
728
|
-
value: targetValue
|
|
729
|
-
}
|
|
730
|
-
]).commit() : existingTargetValue !== targetValue && await client.patch(existingTerm._id).set({ [`translations[${existingTargetIndex}].value`]: targetValue }).commit();
|
|
731
|
-
} else
|
|
732
|
-
await client.create({
|
|
733
|
-
_type: "translationGlossaryTerm",
|
|
734
|
-
translations: [
|
|
735
|
-
{
|
|
736
|
-
_type: "internationalizedArrayStringValue",
|
|
737
|
-
_key: sourceLocale,
|
|
738
|
-
value: sourceValue
|
|
739
|
-
},
|
|
740
|
-
{
|
|
741
|
-
_type: "internationalizedArrayStringValue",
|
|
742
|
-
_key: targetLocale,
|
|
743
|
-
value: targetValue
|
|
744
|
-
}
|
|
745
|
-
]
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
devLog.info("Glossary terms processed", {
|
|
749
|
-
processed: validTerms.length,
|
|
750
|
-
skipped: terms.length - validTerms.length
|
|
751
|
-
});
|
|
499
|
+
const DEFAULT_RETRY_OPTIONS = {
|
|
500
|
+
maxAttempts: 2,
|
|
501
|
+
initialDelayMs: 0,
|
|
502
|
+
maxDelayMs: 5e3,
|
|
503
|
+
backoffMultiplier: 2,
|
|
504
|
+
retryableErrors: isNetworkError,
|
|
505
|
+
logRetries: !0
|
|
506
|
+
};
|
|
507
|
+
function calculateDelay(attempt, options) {
|
|
508
|
+
const delay = options.initialDelayMs * Math.pow(options.backoffMultiplier, attempt);
|
|
509
|
+
return Math.min(delay, options.maxDelayMs);
|
|
752
510
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
_type: "object",
|
|
756
|
-
_key: crypto.randomUUID(),
|
|
757
|
-
timestamp,
|
|
758
|
-
changeType: suggestion.type,
|
|
759
|
-
description: suggestion.description,
|
|
760
|
-
locale: suggestion.type === "localePrompt" ? targetLanguage : void 0
|
|
761
|
-
}));
|
|
762
|
-
historyEntries.length !== 0 && await client.patch(settingsId).setIfMissing({ changeHistory: [] }).append("changeHistory", historyEntries).commit();
|
|
511
|
+
function sleep(ms) {
|
|
512
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
763
513
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
title,
|
|
770
|
-
description,
|
|
771
|
-
tone,
|
|
772
|
-
status = "in_progress",
|
|
773
|
-
icon,
|
|
774
|
-
...props
|
|
775
|
-
}) => /* @__PURE__ */ jsx(Card, { tone, padding: 4, ...props, children: /* @__PURE__ */ jsxs(Flex, { gap: 4, align: "center", justify: "center", children: [
|
|
776
|
-
icon || progressIconMap[status],
|
|
777
|
-
/* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
778
|
-
title && /* @__PURE__ */ jsx(Text, { size: 2, weight: "bold", children: title }),
|
|
779
|
-
description && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: description })
|
|
780
|
-
] })
|
|
781
|
-
] }) }), IMPROVEMENT_TYPE_LABELS = {
|
|
782
|
-
globalPrompt: "Global Prompt",
|
|
783
|
-
localePrompt: "Locale Prompt",
|
|
784
|
-
protectedPhrase: "Protected Phrase",
|
|
785
|
-
glossaryTerm: "Glossary Term"
|
|
786
|
-
}, createNewSuggestion = (type = "protectedPhrase", sourceLanguage, targetLanguage) => {
|
|
787
|
-
const baseSuggestion = {
|
|
788
|
-
id: crypto.randomUUID(),
|
|
789
|
-
type,
|
|
790
|
-
description: "",
|
|
791
|
-
approved: !0
|
|
792
|
-
};
|
|
793
|
-
return type === "glossaryTerm" ? {
|
|
794
|
-
...baseSuggestion,
|
|
795
|
-
sourceValue: "",
|
|
796
|
-
targetValue: "",
|
|
797
|
-
sourceLocale: sourceLanguage,
|
|
798
|
-
targetLocale: targetLanguage
|
|
799
|
-
} : baseSuggestion;
|
|
800
|
-
}, SuggestionReviewList = ({
|
|
801
|
-
suggestions,
|
|
802
|
-
onSuggestionsChange,
|
|
803
|
-
sourceLanguage,
|
|
804
|
-
targetLanguage
|
|
805
|
-
}) => {
|
|
806
|
-
const [editingId, setEditingId] = useState(null), handleToggleApproval = (id) => {
|
|
807
|
-
const updated = suggestions.map((s) => s.id === id && s.validationStatus !== "invalid" ? { ...s, approved: !s.approved } : s);
|
|
808
|
-
onSuggestionsChange(updated);
|
|
809
|
-
}, handleTypeChange = (id, newType) => {
|
|
810
|
-
const updated = suggestions.map((s) => {
|
|
811
|
-
if (s.id !== id)
|
|
812
|
-
return s;
|
|
813
|
-
if (newType === "glossaryTerm" && s.type !== "glossaryTerm") {
|
|
814
|
-
const match = s.description.match(/^(.+?)\s*→\s*(.+)$/);
|
|
815
|
-
return {
|
|
816
|
-
...s,
|
|
817
|
-
type: newType,
|
|
818
|
-
sourceValue: match ? match[1].trim() : "",
|
|
819
|
-
targetValue: match ? match[2].trim() : "",
|
|
820
|
-
sourceLocale: sourceLanguage,
|
|
821
|
-
targetLocale: targetLanguage
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
return newType !== "glossaryTerm" && s.type === "glossaryTerm" ? {
|
|
825
|
-
...s,
|
|
826
|
-
type: newType,
|
|
827
|
-
description: s.sourceValue && s.targetValue ? `${s.sourceValue} \u2192 ${s.targetValue}` : s.description,
|
|
828
|
-
sourceValue: void 0,
|
|
829
|
-
targetValue: void 0
|
|
830
|
-
} : { ...s, type: newType };
|
|
831
|
-
});
|
|
832
|
-
onSuggestionsChange(updated);
|
|
833
|
-
}, handleDescriptionChange = (id, newDescription) => {
|
|
834
|
-
const updated = suggestions.map((s) => s.id === id ? { ...s, description: newDescription } : s);
|
|
835
|
-
onSuggestionsChange(updated);
|
|
836
|
-
}, handleGlossarySourceChange = (id, newSource) => {
|
|
837
|
-
const updated = suggestions.map(
|
|
838
|
-
(s) => s.id === id ? {
|
|
839
|
-
...s,
|
|
840
|
-
sourceValue: newSource,
|
|
841
|
-
description: `${newSource} \u2192 ${s.targetValue || ""}`
|
|
842
|
-
// Preserve sourceLocale and targetLocale
|
|
843
|
-
} : s
|
|
844
|
-
);
|
|
845
|
-
onSuggestionsChange(updated);
|
|
846
|
-
}, handleGlossaryTargetChange = (id, newTarget) => {
|
|
847
|
-
const updated = suggestions.map(
|
|
848
|
-
(s) => s.id === id ? {
|
|
849
|
-
...s,
|
|
850
|
-
targetValue: newTarget,
|
|
851
|
-
description: `${s.sourceValue || ""} \u2192 ${newTarget}`
|
|
852
|
-
// Preserve sourceLocale and targetLocale
|
|
853
|
-
} : s
|
|
854
|
-
);
|
|
855
|
-
onSuggestionsChange(updated);
|
|
856
|
-
}, handleRemove = (id) => {
|
|
857
|
-
const updated = suggestions.filter((s) => s.id !== id);
|
|
858
|
-
onSuggestionsChange(updated);
|
|
859
|
-
}, handleAddSuggestion = (type) => {
|
|
860
|
-
const newSuggestion = createNewSuggestion(type, sourceLanguage, targetLanguage);
|
|
861
|
-
onSuggestionsChange([...suggestions, newSuggestion]), setEditingId(newSuggestion.id);
|
|
514
|
+
async function retryWithBackoff(operation, options = {}) {
|
|
515
|
+
const opts = {
|
|
516
|
+
...DEFAULT_RETRY_OPTIONS,
|
|
517
|
+
...options,
|
|
518
|
+
retryableErrors: options.retryableErrors || DEFAULT_RETRY_OPTIONS.retryableErrors
|
|
862
519
|
};
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
{
|
|
867
|
-
status: "error",
|
|
868
|
-
tone: "transparent",
|
|
869
|
-
title: "No suggestions generated",
|
|
870
|
-
description: 'Try providing more specific feedback or enabling "Include Edits".',
|
|
871
|
-
icon: /* @__PURE__ */ jsx(EmptyIcon, { fontSize: 24 })
|
|
872
|
-
}
|
|
873
|
-
),
|
|
874
|
-
/* @__PURE__ */ jsx(Flex, { justify: "flex-start", children: /* @__PURE__ */ jsx(
|
|
875
|
-
Button,
|
|
876
|
-
{
|
|
877
|
-
mode: "ghost",
|
|
878
|
-
tone: "primary",
|
|
879
|
-
icon: AddIcon,
|
|
880
|
-
text: "Add Suggestion",
|
|
881
|
-
fontSize: 1,
|
|
882
|
-
onClick: () => handleAddSuggestion("protectedPhrase")
|
|
883
|
-
}
|
|
884
|
-
) })
|
|
885
|
-
] }) : /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
886
|
-
suggestions.map((suggestion) => {
|
|
887
|
-
const isEditing = editingId === suggestion.id, isGlossaryTerm = suggestion.type === "glossaryTerm", isPrompt = suggestion.type === "globalPrompt" || suggestion.type === "localePrompt", hasWarning = suggestion.warning && suggestion.validationStatus !== "valid", isInvalid = suggestion.validationStatus === "invalid";
|
|
888
|
-
return /* @__PURE__ */ jsx(
|
|
889
|
-
Card,
|
|
890
|
-
{
|
|
891
|
-
padding: 4,
|
|
892
|
-
border: !0,
|
|
893
|
-
tone: isInvalid ? "critical" : hasWarning ? "caution" : suggestion.approved ? "default" : "transparent",
|
|
894
|
-
style: { opacity: suggestion.approved ? 1 : 0.5 },
|
|
895
|
-
children: /* @__PURE__ */ jsx(Stack, { space: 3, children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 3, children: [
|
|
896
|
-
/* @__PURE__ */ jsx(Box, { paddingTop: 1, children: isInvalid ? /* @__PURE__ */ jsx(
|
|
897
|
-
Tooltip,
|
|
898
|
-
{
|
|
899
|
-
content: /* @__PURE__ */ jsx(Box, { padding: 2, style: { maxWidth: "280px" }, children: /* @__PURE__ */ jsx(Text, { size: 1, children: "This suggestion cannot be applied because it conflicts with existing settings. See the error message below for details." }) }),
|
|
900
|
-
placement: "top",
|
|
901
|
-
portal: !0,
|
|
902
|
-
children: /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Checkbox, { checked: !1, disabled: !0, readOnly: !0 }) })
|
|
903
|
-
}
|
|
904
|
-
) : /* @__PURE__ */ jsx(Checkbox, { checked: suggestion.approved, onChange: () => handleToggleApproval(suggestion.id) }) }),
|
|
905
|
-
/* @__PURE__ */ jsxs(Stack, { space: 3, flex: 1, children: [
|
|
906
|
-
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", wrap: "wrap", children: [
|
|
907
|
-
/* @__PURE__ */ jsx(Box, { flex: 1, style: { minWidth: "200px" }, children: /* @__PURE__ */ jsx(
|
|
908
|
-
Select,
|
|
909
|
-
{
|
|
910
|
-
fontSize: 1,
|
|
911
|
-
value: suggestion.type,
|
|
912
|
-
onChange: (event) => handleTypeChange(suggestion.id, event.currentTarget.value),
|
|
913
|
-
"aria-label": "Suggestion type",
|
|
914
|
-
children: Object.entries(IMPROVEMENT_TYPE_LABELS).map(([value, label]) => /* @__PURE__ */ jsx("option", { value, children: label }, value))
|
|
915
|
-
}
|
|
916
|
-
) }),
|
|
917
|
-
/* @__PURE__ */ jsx(
|
|
918
|
-
Button,
|
|
919
|
-
{
|
|
920
|
-
mode: "bleed",
|
|
921
|
-
tone: "critical",
|
|
922
|
-
icon: TrashIcon,
|
|
923
|
-
fontSize: 1,
|
|
924
|
-
padding: 2,
|
|
925
|
-
onClick: () => handleRemove(suggestion.id),
|
|
926
|
-
"aria-label": "Remove suggestion",
|
|
927
|
-
title: "Remove suggestion"
|
|
928
|
-
}
|
|
929
|
-
)
|
|
930
|
-
] }),
|
|
931
|
-
isGlossaryTerm ? /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
932
|
-
(suggestion.sourceLocale || suggestion.targetLocale) && /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
933
|
-
/* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Text, { size: 0, muted: !0, weight: "medium", children: suggestion.sourceLocale ? `Source (${suggestion.sourceLocale})` : "Source" }) }),
|
|
934
|
-
/* @__PURE__ */ jsx(Box, { style: { width: "20px" } }),
|
|
935
|
-
/* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Text, { size: 0, muted: !0, weight: "medium", children: suggestion.targetLocale ? `Target (${suggestion.targetLocale})` : "Target" }) })
|
|
936
|
-
] }),
|
|
937
|
-
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
938
|
-
/* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
|
|
939
|
-
TextInput,
|
|
940
|
-
{
|
|
941
|
-
fontSize: 1,
|
|
942
|
-
placeholder: "Source term",
|
|
943
|
-
value: suggestion.sourceValue || "",
|
|
944
|
-
onChange: (event) => handleGlossarySourceChange(suggestion.id, event.currentTarget.value),
|
|
945
|
-
"aria-label": `Source term in ${suggestion.sourceLocale || "source language"}`
|
|
946
|
-
}
|
|
947
|
-
) }),
|
|
948
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, "aria-hidden": "true", children: "\u2192" }),
|
|
949
|
-
/* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
|
|
950
|
-
TextInput,
|
|
951
|
-
{
|
|
952
|
-
fontSize: 1,
|
|
953
|
-
placeholder: "Target term",
|
|
954
|
-
value: suggestion.targetValue || "",
|
|
955
|
-
onChange: (event) => handleGlossaryTargetChange(suggestion.id, event.currentTarget.value),
|
|
956
|
-
"aria-label": `Target term in ${suggestion.targetLocale || "target language"}`
|
|
957
|
-
}
|
|
958
|
-
) })
|
|
959
|
-
] })
|
|
960
|
-
] }) : isPrompt ? /* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsx(
|
|
961
|
-
TextArea,
|
|
962
|
-
{
|
|
963
|
-
fontSize: 1,
|
|
964
|
-
rows: isEditing ? 6 : 3,
|
|
965
|
-
value: suggestion.description,
|
|
966
|
-
onChange: (event) => handleDescriptionChange(suggestion.id, event.currentTarget.value),
|
|
967
|
-
onFocus: () => setEditingId(suggestion.id),
|
|
968
|
-
onBlur: () => setEditingId(null),
|
|
969
|
-
placeholder: "Enter prompt guidance...",
|
|
970
|
-
"aria-label": `${IMPROVEMENT_TYPE_LABELS[suggestion.type]} content`
|
|
971
|
-
}
|
|
972
|
-
) }) : /* @__PURE__ */ jsx(
|
|
973
|
-
TextInput,
|
|
974
|
-
{
|
|
975
|
-
fontSize: 1,
|
|
976
|
-
value: suggestion.description,
|
|
977
|
-
onChange: (event) => handleDescriptionChange(suggestion.id, event.currentTarget.value),
|
|
978
|
-
placeholder: "Enter protected phrase...",
|
|
979
|
-
"aria-label": "Protected phrase"
|
|
980
|
-
}
|
|
981
|
-
),
|
|
982
|
-
hasWarning && /* @__PURE__ */ jsx(Card, { padding: 3, tone: isInvalid ? "critical" : "caution", border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 2, children: [
|
|
983
|
-
/* @__PURE__ */ jsx(Text, { size: 1, children: isInvalid ? /* @__PURE__ */ jsx(WarningOutlineIcon, {}) : /* @__PURE__ */ jsx(InfoOutlineIcon, {}) }),
|
|
984
|
-
/* @__PURE__ */ jsxs(Stack, { space: 2, flex: 1, children: [
|
|
985
|
-
/* @__PURE__ */ jsx(Text, { size: 1, weight: "medium", children: isInvalid ? "Invalid Suggestion" : "Warning" }),
|
|
986
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: suggestion.warning })
|
|
987
|
-
] })
|
|
988
|
-
] }) })
|
|
989
|
-
] })
|
|
990
|
-
] }) })
|
|
991
|
-
},
|
|
992
|
-
suggestion.id
|
|
993
|
-
);
|
|
994
|
-
}),
|
|
995
|
-
/* @__PURE__ */ jsx(Flex, { justify: "flex-start", children: /* @__PURE__ */ jsx(
|
|
996
|
-
Button,
|
|
997
|
-
{
|
|
998
|
-
mode: "ghost",
|
|
999
|
-
tone: "neutral",
|
|
1000
|
-
icon: AddIcon,
|
|
1001
|
-
text: "Add Suggestion",
|
|
1002
|
-
fontSize: 1,
|
|
1003
|
-
onClick: () => handleAddSuggestion("protectedPhrase")
|
|
1004
|
-
}
|
|
1005
|
-
) })
|
|
1006
|
-
] });
|
|
1007
|
-
}, GiveFeedbackDialog = ({ setIsDialogOpen }) => {
|
|
1008
|
-
const client = useClient({ apiVersion: "vX" }), { document, config, translationMetadata } = useTranslationContext(), { loading } = useTranslationMetadata({
|
|
1009
|
-
documentId: document._id
|
|
1010
|
-
}), [feedback, setFeedback] = useState(""), [shouldIncludeEdits, setShouldIncludeEdits] = useState(!1), [processingState, setProcessingState] = useState("idle"), [suggestions, setSuggestions] = useState([]), [changeSummary, setChangeSummary] = useState(""), [error, setError] = useState(""), [sourceLanguage, setSourceLanguage] = useState(""), [targetLanguage, setTargetLanguage] = useState(""), handleShouldIncludeEditsChange = () => {
|
|
1011
|
-
setShouldIncludeEdits(!shouldIncludeEdits);
|
|
1012
|
-
}, handleCancel = () => {
|
|
1013
|
-
setIsDialogOpen(!1);
|
|
1014
|
-
}, handleAnalyze = async () => {
|
|
1015
|
-
if (!feedback && !shouldIncludeEdits) {
|
|
1016
|
-
setError('Please provide feedback or enable "Include Edits"');
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
if (!config) {
|
|
1020
|
-
setError("Missing plugin configuration");
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
setError(""), setProcessingState("analyzing"), setChangeSummary("");
|
|
1024
|
-
try {
|
|
1025
|
-
const translationHistory2 = translationMetadata?.translationHistory?.find(
|
|
1026
|
-
(history) => history.language === document[config.languageFieldName]
|
|
1027
|
-
);
|
|
1028
|
-
if (!translationHistory2)
|
|
1029
|
-
throw new Error(
|
|
1030
|
-
"No translation history found for this document. Please ensure this document was translated using the AI Translation plugin."
|
|
1031
|
-
);
|
|
1032
|
-
const { sourceSnapshot, targetSnapshot, sourceLanguage: srcLang } = translationHistory2;
|
|
1033
|
-
if (!sourceSnapshot || !targetSnapshot)
|
|
1034
|
-
throw new Error("Translation snapshots are missing from history.");
|
|
1035
|
-
const srcLanguage = srcLang || "en-us", tgtLanguage = document[config.languageFieldName];
|
|
1036
|
-
setSourceLanguage(srcLanguage), setTargetLanguage(tgtLanguage);
|
|
1037
|
-
const generatedSuggestions = await analyzeFeedback({
|
|
1038
|
-
client,
|
|
1039
|
-
schemaId: config.schemaId,
|
|
1040
|
-
sourceLanguage: srcLanguage,
|
|
1041
|
-
targetLanguage: tgtLanguage,
|
|
1042
|
-
userFeedback: feedback || void 0,
|
|
1043
|
-
shouldIncludeEdits,
|
|
1044
|
-
sourceSnapshot,
|
|
1045
|
-
targetSnapshot,
|
|
1046
|
-
currentDocument: shouldIncludeEdits ? document : void 0,
|
|
1047
|
-
onProgress: ({ stage, message }) => {
|
|
1048
|
-
setProcessingState(stage), setChangeSummary(message);
|
|
1049
|
-
}
|
|
1050
|
-
});
|
|
1051
|
-
setSuggestions(generatedSuggestions), setProcessingState("reviewing");
|
|
1052
|
-
} catch (err) {
|
|
1053
|
-
console.error("Error analyzing feedback:", err), setError(err instanceof Error ? err.message : "An unexpected error occurred"), setProcessingState("error");
|
|
1054
|
-
}
|
|
1055
|
-
}, handleApplySuggestions = async () => {
|
|
1056
|
-
if (!config) {
|
|
1057
|
-
setError("Missing plugin configuration");
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
if (suggestions.filter((s) => s.approved).length === 0) {
|
|
1061
|
-
setError("Please approve at least one suggestion to apply");
|
|
1062
|
-
return;
|
|
1063
|
-
}
|
|
1064
|
-
setError(""), setProcessingState("applying");
|
|
520
|
+
let lastError;
|
|
521
|
+
const maxAttempts = opts.maxAttempts + 1;
|
|
522
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++)
|
|
1065
523
|
try {
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
}
|
|
1084
|
-
};
|
|
1085
|
-
if (loading)
|
|
1086
|
-
return /* @__PURE__ */ jsx(ProgressPanel, { title: "Loading translation metadata" });
|
|
1087
|
-
if (processingState === "analyzing")
|
|
1088
|
-
return /* @__PURE__ */ jsx(ProgressPanel, { title: "Analyzing feedback with AI...", description: changeSummary });
|
|
1089
|
-
if (processingState === "reviewing")
|
|
1090
|
-
return /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
1091
|
-
/* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
1092
|
-
/* @__PURE__ */ jsx(Heading, { children: "Review Suggestions" }),
|
|
1093
|
-
/* @__PURE__ */ jsx(Text, { muted: !0, children: "The AI has analyzed your feedback and generated the following suggestions. Review and edit them before applying." })
|
|
1094
|
-
] }),
|
|
1095
|
-
/* @__PURE__ */ jsx(
|
|
1096
|
-
SuggestionReviewList,
|
|
1097
|
-
{
|
|
1098
|
-
suggestions,
|
|
1099
|
-
onSuggestionsChange: setSuggestions,
|
|
1100
|
-
sourceLanguage,
|
|
1101
|
-
targetLanguage
|
|
1102
|
-
}
|
|
1103
|
-
),
|
|
1104
|
-
error && /* @__PURE__ */ jsx(Card, { padding: 3, tone: "critical", border: !0, children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) }),
|
|
1105
|
-
/* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, paddingTop: 2, children: [
|
|
1106
|
-
/* @__PURE__ */ jsx(Flex, { gap: 2, children: /* @__PURE__ */ jsx(
|
|
1107
|
-
Button,
|
|
1108
|
-
{
|
|
1109
|
-
type: "button",
|
|
1110
|
-
mode: "bleed",
|
|
1111
|
-
tone: "neutral",
|
|
1112
|
-
onClick: () => setProcessingState("idle"),
|
|
1113
|
-
icon: /* @__PURE__ */ jsx(ArrowLeftIcon, {}),
|
|
1114
|
-
text: "Back"
|
|
1115
|
-
}
|
|
1116
|
-
) }),
|
|
1117
|
-
/* @__PURE__ */ jsxs(
|
|
1118
|
-
Button,
|
|
1119
|
-
{
|
|
1120
|
-
type: "button",
|
|
1121
|
-
mode: "ghost",
|
|
1122
|
-
tone: "positive",
|
|
1123
|
-
size: 1,
|
|
1124
|
-
onClick: handleApplySuggestions,
|
|
1125
|
-
disabled: suggestions.filter((s) => s.approved).length === 0,
|
|
1126
|
-
children: [
|
|
1127
|
-
"Apply ",
|
|
1128
|
-
suggestions.filter((s) => s.approved).length,
|
|
1129
|
-
" Suggestion",
|
|
1130
|
-
suggestions.filter((s) => s.approved).length !== 1 ? "s" : ""
|
|
1131
|
-
]
|
|
1132
|
-
}
|
|
1133
|
-
)
|
|
1134
|
-
] })
|
|
1135
|
-
] });
|
|
1136
|
-
if (processingState === "applying")
|
|
1137
|
-
return /* @__PURE__ */ jsx(ProgressPanel, { title: "Updating translation settings...", description: changeSummary, icon: /* @__PURE__ */ jsx(EditIcon, {}) });
|
|
1138
|
-
if (processingState === "completed") {
|
|
1139
|
-
const appliedCount = suggestions.filter((s) => s.approved).length, description = `${appliedCount} suggestion${appliedCount !== 1 ? "s" : ""} applied. Translation prompts and glossary havebeen updated based on your feedback.`;
|
|
1140
|
-
return /* @__PURE__ */ jsxs(Box, { padding: 4, children: [
|
|
1141
|
-
/* @__PURE__ */ jsx(ProgressPanel, { title: "Translation settings updated", description, status: "completed" }),
|
|
1142
|
-
/* @__PURE__ */ jsx(Flex, { justify: "flex-end", paddingTop: 3, children: /* @__PURE__ */ jsx(Button, { type: "button", mode: "ghost", tone: "primary", size: 1, onClick: handleCancel, children: "Close" }) })
|
|
1143
|
-
] });
|
|
1144
|
-
}
|
|
1145
|
-
return processingState === "error" ? /* @__PURE__ */ jsx(Box, { padding: 4, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
1146
|
-
/* @__PURE__ */ jsx(ProgressPanel, { status: "error", title: "Error processing feedback", description: error, tone: "critical" }),
|
|
1147
|
-
/* @__PURE__ */ jsxs(Flex, { justify: "flex-end", gap: 2, children: [
|
|
1148
|
-
/* @__PURE__ */ jsx(Button, { type: "button", mode: "ghost", onClick: handleCancel, children: "Close" }),
|
|
1149
|
-
/* @__PURE__ */ jsx(Button, { type: "reset", mode: "ghost", tone: "primary", onClick: () => setProcessingState("idle"), children: "Reset" })
|
|
1150
|
-
] })
|
|
1151
|
-
] }) }) : /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
1152
|
-
/* @__PURE__ */ jsx(Card, { tone: "transparent", padding: 4, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
1153
|
-
/* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
1154
|
-
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
1155
|
-
/* @__PURE__ */ jsx(Text, { size: 2, muted: !0, children: "Enter your feedback for the translation AI agent." }),
|
|
1156
|
-
/* @__PURE__ */ jsx(
|
|
1157
|
-
Tooltip,
|
|
1158
|
-
{
|
|
1159
|
-
portal: !0,
|
|
1160
|
-
content: /* @__PURE__ */ jsx(Box, { padding: 2, style: { maxWidth: "280px" }, children: /* @__PURE__ */ jsx(Text, { size: 1, children: "The AI agent will use your feedback to improve translation prompts, update protected phrases, and modify the glossary. Be specific about what should be changed." }) }),
|
|
1161
|
-
children: /* @__PURE__ */ jsx(Text, { size: 1, children: /* @__PURE__ */ jsx(InfoOutlineIcon, {}) })
|
|
1162
|
-
}
|
|
1163
|
-
)
|
|
1164
|
-
] }),
|
|
1165
|
-
/* @__PURE__ */ jsx(
|
|
1166
|
-
TextArea,
|
|
524
|
+
const result = await operation();
|
|
525
|
+
return attempt > 1 && opts.logRetries && devLog.info(`Operation succeeded after ${attempt} attempts`, {
|
|
526
|
+
...opts.context,
|
|
527
|
+
attempt,
|
|
528
|
+
totalAttempts: attempt
|
|
529
|
+
}), result;
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (lastError = error, !opts.retryableErrors(error))
|
|
532
|
+
throw opts.logRetries && devLog.warn("Error is not retryable, aborting retries", {
|
|
533
|
+
...opts.context,
|
|
534
|
+
attempt,
|
|
535
|
+
error: error instanceof Error ? error.message : String(error)
|
|
536
|
+
}), error;
|
|
537
|
+
if (attempt >= maxAttempts)
|
|
538
|
+
throw opts.logRetries && devLog.error(
|
|
539
|
+
`Operation failed after ${maxAttempts} attempts`,
|
|
540
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1167
541
|
{
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
onChange: (event) => setFeedback(event.currentTarget.value),
|
|
1171
|
-
padding: 3,
|
|
1172
|
-
placeholder: 'Example: "Braze" is a branded term and should not be translated.',
|
|
1173
|
-
value: feedback
|
|
542
|
+
...opts.context,
|
|
543
|
+
totalAttempts: maxAttempts
|
|
1174
544
|
}
|
|
1175
|
-
)
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
tone: shouldIncludeEdits ? "primary" : "default",
|
|
1185
|
-
children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 3, children: [
|
|
1186
|
-
/* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Checkbox, { checked: shouldIncludeEdits, readOnly: !0, style: { pointerEvents: "none" } }) }),
|
|
1187
|
-
/* @__PURE__ */ jsxs(Stack, { space: 2, flex: 1, children: [
|
|
1188
|
-
/* @__PURE__ */ jsx(Text, { size: 2, weight: "medium", children: "Include Edits in Feedback" }),
|
|
1189
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, style: { textAlign: "left" }, children: "The AI will analyze edits you made to the translation." })
|
|
1190
|
-
] })
|
|
1191
|
-
] })
|
|
1192
|
-
}
|
|
1193
|
-
) }) })
|
|
1194
|
-
] }) }),
|
|
1195
|
-
/* @__PURE__ */ jsxs(Flex, { justify: "flex-end", gap: 2, paddingTop: 2, children: [
|
|
1196
|
-
/* @__PURE__ */ jsx(Button, { type: "button", mode: "ghost", size: 1, onClick: handleCancel, children: "Cancel" }),
|
|
1197
|
-
/* @__PURE__ */ jsx(
|
|
1198
|
-
Button,
|
|
1199
|
-
{
|
|
1200
|
-
type: "submit",
|
|
1201
|
-
mode: "ghost",
|
|
1202
|
-
tone: "positive",
|
|
1203
|
-
size: 1,
|
|
1204
|
-
onClick: handleAnalyze,
|
|
1205
|
-
disabled: !feedback.trim() && !shouldIncludeEdits,
|
|
1206
|
-
children: "Analyze Feedback"
|
|
1207
|
-
}
|
|
1208
|
-
)
|
|
1209
|
-
] })
|
|
1210
|
-
] });
|
|
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 }) })
|
|
545
|
+
), error;
|
|
546
|
+
const delay = calculateDelay(attempt - 1, opts);
|
|
547
|
+
opts.logRetries && devLog.warn(`Operation failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {
|
|
548
|
+
...opts.context,
|
|
549
|
+
attempt,
|
|
550
|
+
totalAttempts: maxAttempts,
|
|
551
|
+
delayMs: delay,
|
|
552
|
+
error: error instanceof Error ? error.message : String(error)
|
|
553
|
+
}), delay > 0 && await sleep(delay);
|
|
1234
554
|
}
|
|
1235
|
-
|
|
1236
|
-
}
|
|
555
|
+
throw lastError;
|
|
556
|
+
}
|
|
1237
557
|
class Semaphore {
|
|
1238
558
|
permits;
|
|
1239
559
|
queue = [];
|
|
@@ -1584,13 +904,27 @@ const fetchTranslationPrompts = async ({
|
|
|
1584
904
|
}
|
|
1585
905
|
}), hasChanges = !0);
|
|
1586
906
|
}
|
|
1587
|
-
hasChanges && (await
|
|
907
|
+
hasChanges && (await retryWithBackoff(
|
|
908
|
+
async () => {
|
|
909
|
+
await patch.commit();
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
maxAttempts: 2,
|
|
913
|
+
initialDelayMs: 0,
|
|
914
|
+
retryableErrors: isNetworkError,
|
|
915
|
+
context: {
|
|
916
|
+
operation: "localizeReferences",
|
|
917
|
+
documentId,
|
|
918
|
+
referenceCount: refs.length
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
), devLog.info("Updated references in batch", {
|
|
1588
922
|
documentId,
|
|
1589
923
|
count: refs.length,
|
|
1590
924
|
translatedCount: translationMap.size,
|
|
1591
925
|
component: "updateReferences"
|
|
1592
926
|
}));
|
|
1593
|
-
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages) => {
|
|
927
|
+
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages, mainSourceDocumentId) => {
|
|
1594
928
|
for (const sourceDocument of sourceDocuments)
|
|
1595
929
|
for (const lang of targetLanguages) {
|
|
1596
930
|
const payload = {
|
|
@@ -1603,146 +937,8 @@ const fetchTranslationPrompts = async ({
|
|
|
1603
937
|
dispatch({
|
|
1604
938
|
type: "SET_TRANSLATION_JOB_STATUS",
|
|
1605
939
|
payload
|
|
1606
|
-
});
|
|
1607
|
-
}
|
|
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);
|
|
940
|
+
}), mainSourceDocumentId && updateActiveTranslationJob(mainSourceDocumentId, payload);
|
|
1706
941
|
}
|
|
1707
|
-
}
|
|
1708
|
-
return { success: !1, error: lastError, attempts };
|
|
1709
|
-
}
|
|
1710
|
-
const getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
1711
|
-
(translation) => translation._key === targetLanguage
|
|
1712
|
-
)?.value._ref || null, getTranslationMetadata = async ({
|
|
1713
|
-
client,
|
|
1714
|
-
sourceDocument,
|
|
1715
|
-
sourceLanguage
|
|
1716
|
-
}) => {
|
|
1717
|
-
try {
|
|
1718
|
-
return await client.fetch(
|
|
1719
|
-
"*[_type == $tmType && references($sourceDocumentId)][0]",
|
|
1720
|
-
{
|
|
1721
|
-
tmType: TRANSLATION_METADATA_TYPE,
|
|
1722
|
-
sourceDocumentId: undraftId(sourceDocument._id)
|
|
1723
|
-
}
|
|
1724
|
-
) || await client.create({
|
|
1725
|
-
_type: TRANSLATION_METADATA_TYPE,
|
|
1726
|
-
translations: [
|
|
1727
|
-
{
|
|
1728
|
-
_key: sourceLanguage,
|
|
1729
|
-
_type: "internationalizedArrayReferenceValue",
|
|
1730
|
-
value: {
|
|
1731
|
-
_type: "reference",
|
|
1732
|
-
_ref: undraftId(sourceDocument._id),
|
|
1733
|
-
_weak: !0,
|
|
1734
|
-
_strengthenOnPublish: {
|
|
1735
|
-
_type: sourceDocument._type,
|
|
1736
|
-
_weak: !1
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
],
|
|
1741
|
-
translationHistory: []
|
|
1742
|
-
});
|
|
1743
|
-
} catch (error) {
|
|
1744
|
-
throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
|
|
1745
|
-
}
|
|
1746
942
|
}, updateTranslationMetadata = async ({
|
|
1747
943
|
client,
|
|
1748
944
|
targetLanguage,
|
|
@@ -1776,11 +972,71 @@ const getExistingTranslationId = (translationMetadata, targetLanguage) => transl
|
|
|
1776
972
|
targetRev: newDocument._rev,
|
|
1777
973
|
targetSnapshot: JSON.stringify(newDocument)
|
|
1778
974
|
}, translationPatch = client.patch(translationMetadata._id).setIfMissing({ translations: [] });
|
|
1779
|
-
isNewTranslation ? await
|
|
975
|
+
isNewTranslation ? await retryWithBackoff(
|
|
976
|
+
async () => {
|
|
977
|
+
await translationPatch.insert("after", "translations[-1]", [refToNewTranslation]).commit();
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
maxAttempts: 2,
|
|
981
|
+
initialDelayMs: 0,
|
|
982
|
+
retryableErrors: isNetworkError,
|
|
983
|
+
context: {
|
|
984
|
+
operation: "updateTranslationMetadata.translations",
|
|
985
|
+
sourceDocumentId: sourceDocument._id,
|
|
986
|
+
targetLanguage,
|
|
987
|
+
isNewTranslation: !0
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
) : await retryWithBackoff(
|
|
991
|
+
async () => {
|
|
992
|
+
await translationPatch.insert("replace", `translations[_key == "${targetLanguage}"]`, [refToNewTranslation]).commit();
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
maxAttempts: 2,
|
|
996
|
+
initialDelayMs: 0,
|
|
997
|
+
retryableErrors: isNetworkError,
|
|
998
|
+
context: {
|
|
999
|
+
operation: "updateTranslationMetadata.translations",
|
|
1000
|
+
sourceDocumentId: sourceDocument._id,
|
|
1001
|
+
targetLanguage,
|
|
1002
|
+
isNewTranslation: !1
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
);
|
|
1780
1006
|
const historyPatch = client.patch(translationMetadata._id).setIfMissing({ translationHistory: [] });
|
|
1781
1007
|
translationMetadata.translationHistory?.some(
|
|
1782
1008
|
(history) => history.language === targetLanguage
|
|
1783
|
-
) ? await
|
|
1009
|
+
) ? await retryWithBackoff(
|
|
1010
|
+
async () => {
|
|
1011
|
+
await historyPatch.insert("replace", `translationHistory[_key == "${targetLanguage}"]`, [newTranslationHistory]).commit();
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
maxAttempts: 2,
|
|
1015
|
+
initialDelayMs: 0,
|
|
1016
|
+
retryableErrors: isNetworkError,
|
|
1017
|
+
context: {
|
|
1018
|
+
operation: "updateTranslationMetadata.history",
|
|
1019
|
+
sourceDocumentId: sourceDocument._id,
|
|
1020
|
+
targetLanguage,
|
|
1021
|
+
isNewHistory: !1
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
) : await retryWithBackoff(
|
|
1025
|
+
async () => {
|
|
1026
|
+
await historyPatch.insert("after", "translationHistory[-1]", [newTranslationHistory]).commit();
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
maxAttempts: 2,
|
|
1030
|
+
initialDelayMs: 0,
|
|
1031
|
+
retryableErrors: isNetworkError,
|
|
1032
|
+
context: {
|
|
1033
|
+
operation: "updateTranslationMetadata.history",
|
|
1034
|
+
sourceDocumentId: sourceDocument._id,
|
|
1035
|
+
targetLanguage,
|
|
1036
|
+
isNewHistory: !0
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
1784
1040
|
} catch (error) {
|
|
1785
1041
|
throw devLog.error("Error updating translation metadata", error instanceof Error ? error : new Error(String(error)), {
|
|
1786
1042
|
sourceDocumentId: sourceDocument._id,
|
|
@@ -1789,15 +1045,16 @@ const getExistingTranslationId = (translationMetadata, targetLanguage) => transl
|
|
|
1789
1045
|
component: "updateTranslationMetadata"
|
|
1790
1046
|
}), error;
|
|
1791
1047
|
}
|
|
1792
|
-
}, setJobStatus = (dispatch, _id, language, status, payload) => {
|
|
1048
|
+
}, setJobStatus = (dispatch, _id, language, status, payload, sourceDocumentId) => {
|
|
1049
|
+
const job = defaults(payload, {
|
|
1050
|
+
_id,
|
|
1051
|
+
language,
|
|
1052
|
+
status
|
|
1053
|
+
});
|
|
1793
1054
|
dispatch({
|
|
1794
1055
|
type: "SET_TRANSLATION_JOB_STATUS",
|
|
1795
|
-
payload:
|
|
1796
|
-
|
|
1797
|
-
language,
|
|
1798
|
-
status
|
|
1799
|
-
})
|
|
1800
|
-
});
|
|
1056
|
+
payload: job
|
|
1057
|
+
}), sourceDocumentId && updateActiveTranslationJob(sourceDocumentId, job);
|
|
1801
1058
|
};
|
|
1802
1059
|
function shouldExcludeField(fieldPath, excludeFields) {
|
|
1803
1060
|
for (const pattern of excludeFields) {
|
|
@@ -2030,7 +1287,9 @@ function restoreTransforms({
|
|
|
2030
1287
|
schemaType,
|
|
2031
1288
|
document: translatedDocument
|
|
2032
1289
|
};
|
|
2033
|
-
devLog.debug("Restoring transform for field", { fieldPath, schemaType })
|
|
1290
|
+
devLog.debug("Restoring transform for field", { fieldPath, schemaType });
|
|
1291
|
+
const restoredValue = transform.fromTranslatable(value, originalValue, context);
|
|
1292
|
+
restoredObj[key] = restoredValue;
|
|
2034
1293
|
} else
|
|
2035
1294
|
restoredObj[key] = restoreTransforms({
|
|
2036
1295
|
translatedDocument: value,
|
|
@@ -2123,14 +1382,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2123
1382
|
}), devLog.debug("OpenAI GPT-5 Full Prompt", {
|
|
2124
1383
|
messages: request.messages
|
|
2125
1384
|
});
|
|
2126
|
-
const response = await
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
1385
|
+
const response = await retryWithBackoff(
|
|
1386
|
+
async () => {
|
|
1387
|
+
const fetchResponse = await fetch("https://api.openai.com/v1/responses", {
|
|
1388
|
+
method: "POST",
|
|
1389
|
+
headers: {
|
|
1390
|
+
"Content-Type": "application/json",
|
|
1391
|
+
Authorization: `Bearer ${apiKey}`
|
|
1392
|
+
},
|
|
1393
|
+
body: JSON.stringify(requestBody)
|
|
1394
|
+
});
|
|
1395
|
+
if (!fetchResponse.ok) {
|
|
1396
|
+
const errorMessage = `GPT-5 API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
|
|
1397
|
+
throw errorObj.response = { status: fetchResponse.status }, errorObj;
|
|
1398
|
+
}
|
|
1399
|
+
return fetchResponse;
|
|
2131
1400
|
},
|
|
2132
|
-
|
|
2133
|
-
|
|
1401
|
+
{
|
|
1402
|
+
maxAttempts: 2,
|
|
1403
|
+
initialDelayMs: 0,
|
|
1404
|
+
retryableErrors: isNetworkError,
|
|
1405
|
+
context: { model: requestBody.model, endpoint: "responses" }
|
|
1406
|
+
}
|
|
1407
|
+
);
|
|
2134
1408
|
if (!response.ok) {
|
|
2135
1409
|
const error = await response.json();
|
|
2136
1410
|
throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
|
|
@@ -2169,14 +1443,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2169
1443
|
}), devLog.debug("OpenAI Chat Completions Full Prompt", {
|
|
2170
1444
|
messages: request.messages
|
|
2171
1445
|
});
|
|
2172
|
-
const response = await
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
1446
|
+
const response = await retryWithBackoff(
|
|
1447
|
+
async () => {
|
|
1448
|
+
const fetchResponse = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1449
|
+
method: "POST",
|
|
1450
|
+
headers: {
|
|
1451
|
+
"Content-Type": "application/json",
|
|
1452
|
+
Authorization: `Bearer ${apiKey}`
|
|
1453
|
+
},
|
|
1454
|
+
body: JSON.stringify(requestBody)
|
|
1455
|
+
});
|
|
1456
|
+
if (!fetchResponse.ok) {
|
|
1457
|
+
const errorMessage = `OpenAI API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
|
|
1458
|
+
throw errorObj.response = { status: fetchResponse.status }, errorObj;
|
|
1459
|
+
}
|
|
1460
|
+
return fetchResponse;
|
|
2177
1461
|
},
|
|
2178
|
-
|
|
2179
|
-
|
|
1462
|
+
{
|
|
1463
|
+
maxAttempts: 2,
|
|
1464
|
+
initialDelayMs: 0,
|
|
1465
|
+
retryableErrors: isNetworkError,
|
|
1466
|
+
context: { model, endpoint: "chat/completions" }
|
|
1467
|
+
}
|
|
1468
|
+
);
|
|
2180
1469
|
if (!response.ok) {
|
|
2181
1470
|
const error = await response.json();
|
|
2182
1471
|
throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
|
|
@@ -2238,16 +1527,63 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2238
1527
|
}, modelPricing = pricing[model] || pricing["gpt-5"], cachedInputRate = model === "gpt-5.1" ? 0.125 / 1e6 : modelPricing.input;
|
|
2239
1528
|
return Math.max(0, tokens.prompt - tokens.cached) * modelPricing.input + tokens.cached * cachedInputRate + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
|
|
2240
1529
|
}, deserializeDocument = (originalDoc, reviewedText) => {
|
|
2241
|
-
|
|
1530
|
+
devLog.debug("Raw GPT-5 response received", {
|
|
1531
|
+
length: reviewedText.length,
|
|
1532
|
+
preview: reviewedText.substring(0, 1e3),
|
|
1533
|
+
hasReasoning: reviewedText.includes("<reasoning>"),
|
|
1534
|
+
hasCodeBlocks: reviewedText.includes("```")
|
|
1535
|
+
});
|
|
1536
|
+
const cleanedText = preprocessResponse(reviewedText);
|
|
1537
|
+
cleanedText.length !== reviewedText.length && devLog.debug("Response after preprocessing", {
|
|
1538
|
+
originalLength: reviewedText.length,
|
|
1539
|
+
cleanedLength: cleanedText.length,
|
|
1540
|
+
preview: cleanedText.substring(0, 500)
|
|
1541
|
+
});
|
|
1542
|
+
const parsed = parseReviewFormat(cleanedText, originalDoc);
|
|
1543
|
+
devLog.debug("Parsed response structure", {
|
|
1544
|
+
rootNodeCount: parsed.length,
|
|
1545
|
+
rootNodeNames: parsed.map((n) => n.type === "field" && n.name ? n.name : "unknown")
|
|
1546
|
+
});
|
|
1547
|
+
const result = cloneDeep(originalDoc);
|
|
2242
1548
|
return applyTextChanges(result, parsed), result;
|
|
2243
|
-
},
|
|
1549
|
+
}, preprocessResponse = (text) => {
|
|
1550
|
+
const originalLength = text.length;
|
|
1551
|
+
let cleaned = text;
|
|
1552
|
+
const beforeReasoning = cleaned.length;
|
|
1553
|
+
cleaned = cleaned.replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, "");
|
|
1554
|
+
const reasoningRemoved = beforeReasoning !== cleaned.length, beforeCodeBlocks = cleaned.length;
|
|
1555
|
+
cleaned = cleaned.replace(/^```[a-z]*\s*$/gim, ""), cleaned = cleaned.replace(/^```\s*$/gim, "");
|
|
1556
|
+
const codeBlocksRemoved = beforeCodeBlocks !== cleaned.length, finalLength = cleaned.trim().length;
|
|
1557
|
+
return (reasoningRemoved || codeBlocksRemoved) && devLog.debug("Preprocessed GPT-5 response", {
|
|
1558
|
+
originalLength,
|
|
1559
|
+
finalLength,
|
|
1560
|
+
reasoningRemoved,
|
|
1561
|
+
codeBlocksRemoved,
|
|
1562
|
+
preview: cleaned.trim().substring(0, 200)
|
|
1563
|
+
}), cleaned.trim();
|
|
1564
|
+
}, parseReviewFormat = (text, originalDoc) => {
|
|
2244
1565
|
const root = [], stack = [], lines = text.split(`
|
|
2245
1566
|
`);
|
|
1567
|
+
devLog.debug("Parsing response text", {
|
|
1568
|
+
lineCount: lines.length,
|
|
1569
|
+
firstLines: lines.slice(0, 10).map((l, idx) => ({ line: idx, content: l.substring(0, 100) })),
|
|
1570
|
+
hasFieldTags: text.includes("<field"),
|
|
1571
|
+
hasBlockTags: text.includes("<block"),
|
|
1572
|
+
hasSpanTags: text.includes("<span"),
|
|
1573
|
+
textLength: text.length,
|
|
1574
|
+
textPreview: text.substring(0, 500)
|
|
1575
|
+
});
|
|
1576
|
+
let processedLineCount = 0, skippedEmptyLineCount = 0, matchedPatternCount = 0;
|
|
2246
1577
|
for (let i = 0; i < lines.length; i++) {
|
|
2247
1578
|
const trimmed = lines[i].trim();
|
|
2248
|
-
if (!trimmed)
|
|
1579
|
+
if (!trimmed) {
|
|
1580
|
+
skippedEmptyLineCount++;
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
processedLineCount++;
|
|
2249
1584
|
const compactStringMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
2250
1585
|
if (compactStringMatch && !trimmed.includes("<")) {
|
|
1586
|
+
matchedPatternCount++;
|
|
2251
1587
|
const [, fieldName, text2] = compactStringMatch, node = {
|
|
2252
1588
|
type: "field",
|
|
2253
1589
|
name: fieldName,
|
|
@@ -2264,6 +1600,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2264
1600
|
}
|
|
2265
1601
|
const compactStartMatch = trimmed.match(/^---\s+(\w+)\s+---$/);
|
|
2266
1602
|
if (compactStartMatch) {
|
|
1603
|
+
matchedPatternCount++;
|
|
2267
1604
|
const fieldName = compactStartMatch[1], contentLines = [];
|
|
2268
1605
|
for (i++; i < lines.length; ) {
|
|
2269
1606
|
const currentLine = lines[i];
|
|
@@ -2273,8 +1610,16 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2273
1610
|
}
|
|
2274
1611
|
const content = contentLines.join(`
|
|
2275
1612
|
`);
|
|
2276
|
-
let contentType
|
|
2277
|
-
|
|
1613
|
+
let contentType;
|
|
1614
|
+
try {
|
|
1615
|
+
const fieldValue = get(originalDoc, [fieldName]);
|
|
1616
|
+
if (fieldValue && typeof fieldValue == "object" && fieldValue !== null) {
|
|
1617
|
+
const fieldType = fieldValue._type;
|
|
1618
|
+
typeof fieldType == "string" && fieldType.startsWith("translatable.") && (contentType = fieldType);
|
|
1619
|
+
}
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1622
|
+
contentType || (content.includes("<block") || content.includes("<span") ? contentType = "translatable.richtext" : contentType = "translatable.vtt.captions");
|
|
2278
1623
|
const node = {
|
|
2279
1624
|
type: "field",
|
|
2280
1625
|
name: fieldName,
|
|
@@ -2291,6 +1636,12 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2291
1636
|
}
|
|
2292
1637
|
const openMatch = trimmed.match(/^<(field|block|span)([^>]*)>(.*)$/);
|
|
2293
1638
|
if (openMatch) {
|
|
1639
|
+
matchedPatternCount++, devLog.debug("Matched opening tag", {
|
|
1640
|
+
line: i,
|
|
1641
|
+
tagName: openMatch[1],
|
|
1642
|
+
attributes: openMatch[2],
|
|
1643
|
+
contentPreview: openMatch[3]?.substring(0, 50)
|
|
1644
|
+
});
|
|
2294
1645
|
const [, tagName, attrsStr, content] = openMatch, attributes = parseAttributes(attrsStr), node = {
|
|
2295
1646
|
type: tagName,
|
|
2296
1647
|
name: attributes.name,
|
|
@@ -2311,7 +1662,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2311
1662
|
parent && parent.children.push(node);
|
|
2312
1663
|
} else
|
|
2313
1664
|
root.push(node);
|
|
2314
|
-
else if (attributes.type
|
|
1665
|
+
else if (attributes.type && typeof attributes.type == "string" && attributes.type.startsWith("translatable.")) {
|
|
2315
1666
|
const contentLines = [];
|
|
2316
1667
|
for (i++; i < lines.length; ) {
|
|
2317
1668
|
const currentLine = lines[i];
|
|
@@ -2331,11 +1682,26 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2331
1682
|
parent && parent.children.push(node);
|
|
2332
1683
|
} else
|
|
2333
1684
|
root.push(node);
|
|
2334
|
-
else
|
|
1685
|
+
else if (attributes.type === "string" && !content) {
|
|
1686
|
+
const contentLines = [];
|
|
1687
|
+
for (i++; i < lines.length; ) {
|
|
1688
|
+
const currentLine = lines[i];
|
|
1689
|
+
if (currentLine.trim() === "</field>")
|
|
1690
|
+
break;
|
|
1691
|
+
contentLines.push(currentLine), i++;
|
|
1692
|
+
}
|
|
1693
|
+
if (node.text = contentLines.join(`
|
|
1694
|
+
`).trim(), stack.length > 0) {
|
|
1695
|
+
const parent = stack[stack.length - 1];
|
|
1696
|
+
parent && parent.children.push(node);
|
|
1697
|
+
} else
|
|
1698
|
+
root.push(node);
|
|
1699
|
+
} else
|
|
2335
1700
|
stack.push(node);
|
|
2336
1701
|
continue;
|
|
2337
1702
|
}
|
|
2338
1703
|
if (trimmed.match(/^<\/(field|block|span)>$/) && stack.length > 0) {
|
|
1704
|
+
matchedPatternCount++;
|
|
2339
1705
|
const node = stack.pop();
|
|
2340
1706
|
if (node)
|
|
2341
1707
|
if (stack.length > 0) {
|
|
@@ -2343,9 +1709,24 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2343
1709
|
parent && parent.children.push(node);
|
|
2344
1710
|
} else
|
|
2345
1711
|
root.push(node);
|
|
1712
|
+
continue;
|
|
2346
1713
|
}
|
|
1714
|
+
processedLineCount <= 20 && devLog.debug("Line did not match any pattern", {
|
|
1715
|
+
line: i,
|
|
1716
|
+
trimmed,
|
|
1717
|
+
startsWithLessThan: trimmed.startsWith("<"),
|
|
1718
|
+
startsWithField: trimmed.startsWith("<field"),
|
|
1719
|
+
startsWithBacktick: trimmed.startsWith("`")
|
|
1720
|
+
});
|
|
2347
1721
|
}
|
|
2348
|
-
return
|
|
1722
|
+
return devLog.debug("Parse complete", {
|
|
1723
|
+
totalLines: lines.length,
|
|
1724
|
+
processedLines: processedLineCount,
|
|
1725
|
+
skippedEmptyLines: skippedEmptyLineCount,
|
|
1726
|
+
rootNodeCount: root.length,
|
|
1727
|
+
stackLength: stack.length,
|
|
1728
|
+
matchedPatterns: matchedPatternCount
|
|
1729
|
+
}), root;
|
|
2349
1730
|
}, parseAttributes = (attrStr) => {
|
|
2350
1731
|
const attrs = {}, attrRegex = /(\w+)="([^"]*)"/g;
|
|
2351
1732
|
let match = attrRegex.exec(attrStr);
|
|
@@ -2353,20 +1734,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2353
1734
|
attrs[match[1]] = unescapeXml(match[2]), match = attrRegex.exec(attrStr);
|
|
2354
1735
|
return attrs;
|
|
2355
1736
|
}, unescapeXml = (str) => str.replace(/'/g, "'").replace(/"/g, '"').replace(/>/g, ">").replace(/</g, "<").replace(/&/g, "&"), applyTextChanges = (doc, parsedNodes) => {
|
|
1737
|
+
devLog.debug("Applying text changes from parsed nodes", {
|
|
1738
|
+
nodeCount: parsedNodes.length,
|
|
1739
|
+
nodeNames: parsedNodes.map((n) => n.type === "field" && n.name ? n.name : "unknown")
|
|
1740
|
+
});
|
|
2356
1741
|
for (const node of parsedNodes)
|
|
2357
1742
|
if (node.type === "field" && node.name) {
|
|
2358
|
-
const path = [node.name];
|
|
1743
|
+
const path = [node.name], beforeValue = get(doc, path);
|
|
2359
1744
|
applyFieldChanges(doc, path, node);
|
|
1745
|
+
const afterValue = get(doc, path);
|
|
1746
|
+
beforeValue !== afterValue && devLog.debug("Field updated during deserialization", {
|
|
1747
|
+
path: path.join("."),
|
|
1748
|
+
before: typeof beforeValue == "string" ? beforeValue.substring(0, 50) : beforeValue,
|
|
1749
|
+
after: typeof afterValue == "string" ? afterValue.substring(0, 50) : afterValue
|
|
1750
|
+
});
|
|
2360
1751
|
}
|
|
2361
1752
|
}, applyFieldChanges = (doc, path, node) => {
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.vtt.captions" && node.text && set(doc, [...path, "translationText"], node.text);
|
|
2365
|
-
return;
|
|
2366
|
-
}
|
|
2367
|
-
if (node.attributes.type === "translatable.richtext") {
|
|
1753
|
+
const nodeType = node.attributes.type;
|
|
1754
|
+
if (typeof nodeType == "string" && nodeType.startsWith("translatable.")) {
|
|
2368
1755
|
const fieldValue = get(doc, path);
|
|
2369
|
-
fieldValue && typeof fieldValue == "object" && fieldValue
|
|
1756
|
+
if (fieldValue && typeof fieldValue == "object" && fieldValue !== null) {
|
|
1757
|
+
const fieldType = fieldValue._type;
|
|
1758
|
+
typeof fieldType == "string" && fieldType === nodeType && node.text && set(doc, [...path, "translationText"], node.text);
|
|
1759
|
+
}
|
|
2370
1760
|
return;
|
|
2371
1761
|
}
|
|
2372
1762
|
if (node.attributes.type === "array") {
|
|
@@ -2454,12 +1844,9 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2454
1844
|
return;
|
|
2455
1845
|
}
|
|
2456
1846
|
if (isPlainObject(value)) {
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
}
|
|
2461
|
-
if (value._type === "translatable.richtext" && typeof value.translationText == "string") {
|
|
2462
|
-
lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
|
|
1847
|
+
const valueObj = value, valueType = valueObj._type, translationText = valueObj.translationText;
|
|
1848
|
+
if (typeof valueType == "string" && valueType.startsWith("translatable.") && typeof translationText == "string") {
|
|
1849
|
+
lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
|
|
2463
1850
|
return;
|
|
2464
1851
|
}
|
|
2465
1852
|
lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
|
|
@@ -2595,7 +1982,9 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2595
1982
|
languageFieldName,
|
|
2596
1983
|
translationPrompts,
|
|
2597
1984
|
targetLanguage,
|
|
2598
|
-
openaiConfig
|
|
1985
|
+
openaiConfig,
|
|
1986
|
+
previousError,
|
|
1987
|
+
errorContext
|
|
2599
1988
|
}) => {
|
|
2600
1989
|
const sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
2601
1990
|
sourceId: sourceDocument._id,
|
|
@@ -2613,7 +2002,8 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2613
2002
|
sourceText,
|
|
2614
2003
|
sourceLanguage,
|
|
2615
2004
|
targetLanguage,
|
|
2616
|
-
translationPrompts
|
|
2005
|
+
translationPrompts,
|
|
2006
|
+
errorContext
|
|
2617
2007
|
}), response = await callGPT5(openaiConfig.apiKey, {
|
|
2618
2008
|
messages: [
|
|
2619
2009
|
{
|
|
@@ -2671,14 +2061,21 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2671
2061
|
sourceText,
|
|
2672
2062
|
sourceLanguage,
|
|
2673
2063
|
targetLanguage,
|
|
2674
|
-
translationPrompts
|
|
2064
|
+
translationPrompts,
|
|
2065
|
+
errorContext
|
|
2675
2066
|
}) => {
|
|
2676
2067
|
const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
|
|
2677
2068
|
return `# TRANSLATION TASK
|
|
2678
2069
|
|
|
2679
2070
|
Translate this ${sourceLanguage} document into ${targetLanguage}.
|
|
2680
2071
|
|
|
2681
|
-
|
|
2072
|
+
${errorContext ? `## \u26A0\uFE0F IMPORTANT: Previous Attempt Error
|
|
2073
|
+
|
|
2074
|
+
${errorContext}
|
|
2075
|
+
|
|
2076
|
+
Please carefully review the error above and ensure your translation fixes the issue.
|
|
2077
|
+
|
|
2078
|
+
` : ""}## Guidelines
|
|
2682
2079
|
|
|
2683
2080
|
${globalPrompt ? `### General Guidelines
|
|
2684
2081
|
${globalPrompt}
|
|
@@ -2852,21 +2249,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2852
2249
|
languageFieldName,
|
|
2853
2250
|
targetLanguage
|
|
2854
2251
|
});
|
|
2855
|
-
return await
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2252
|
+
return await retryWithBackoff(
|
|
2253
|
+
async () => await client.agent.action.translate({
|
|
2254
|
+
schemaId,
|
|
2255
|
+
toLanguage,
|
|
2256
|
+
fromLanguage,
|
|
2257
|
+
documentId: sourceDocument._id,
|
|
2258
|
+
noWrite: !0,
|
|
2259
|
+
// Do not write to the document, just return the translated content
|
|
2260
|
+
languageFieldPath: [languageFieldName],
|
|
2261
|
+
styleGuide,
|
|
2262
|
+
protectedPhrases: translationPrompts.protectedPhrases,
|
|
2263
|
+
conditionalPaths: {
|
|
2264
|
+
defaultHidden: !1,
|
|
2265
|
+
defaultReadOnly: !1
|
|
2266
|
+
}
|
|
2267
|
+
}),
|
|
2268
|
+
{
|
|
2269
|
+
maxAttempts: 2,
|
|
2270
|
+
initialDelayMs: 0,
|
|
2271
|
+
retryableErrors: isNetworkError,
|
|
2272
|
+
context: {
|
|
2273
|
+
operation: "agent.action.translate",
|
|
2274
|
+
documentId: sourceDocument._id,
|
|
2275
|
+
targetLanguage,
|
|
2276
|
+
schemaId
|
|
2277
|
+
}
|
|
2868
2278
|
}
|
|
2869
|
-
|
|
2279
|
+
);
|
|
2870
2280
|
}, handleTranslation = async ({
|
|
2871
2281
|
dispatch,
|
|
2872
2282
|
client,
|
|
@@ -2879,7 +2289,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2879
2289
|
translationMode,
|
|
2880
2290
|
openaiConfig,
|
|
2881
2291
|
releaseId,
|
|
2882
|
-
excludeFields = []
|
|
2292
|
+
excludeFields = [],
|
|
2293
|
+
replaceExisting = !1,
|
|
2294
|
+
mainSourceDocumentId
|
|
2883
2295
|
}) => {
|
|
2884
2296
|
const safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
|
|
2885
2297
|
excludeFields.length !== safeExcludeFields.length && devLog.warn("Language field was in excludeFields - removing it to prevent translation errors", {
|
|
@@ -2902,58 +2314,74 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2902
2314
|
filteredTitleValue: filteredDocument.title,
|
|
2903
2315
|
excludedValuesKeys: Object.keys(excludedValues),
|
|
2904
2316
|
excludedValues
|
|
2905
|
-
}), devLog.debug("Applying field transforms before translation", {
|
|
2906
|
-
documentId: sourceDocument._id,
|
|
2907
|
-
documentType: sourceDocument._type
|
|
2908
2317
|
});
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2318
|
+
let documentToTranslate, originalStructure = {};
|
|
2319
|
+
if (translationMode === "openai-only") {
|
|
2320
|
+
devLog.debug("Applying field transforms before translation (OpenAI mode)", {
|
|
2321
|
+
documentId: sourceDocument._id,
|
|
2322
|
+
documentType: sourceDocument._type
|
|
2323
|
+
});
|
|
2324
|
+
const transformResult = applyTransforms({
|
|
2325
|
+
document: filteredDocument,
|
|
2326
|
+
registry: globalTransformRegistry,
|
|
2327
|
+
schemaType: sourceDocument._type
|
|
2328
|
+
});
|
|
2329
|
+
documentToTranslate = transformResult.transformedDocument, originalStructure = transformResult.originalStructure;
|
|
2330
|
+
} else
|
|
2331
|
+
devLog.debug("Skipping field transforms (Sanity Agent mode)", {
|
|
2332
|
+
documentId: sourceDocument._id,
|
|
2333
|
+
documentType: sourceDocument._type
|
|
2334
|
+
}), documentToTranslate = filteredDocument;
|
|
2335
|
+
let translatedDoc, previousError = null;
|
|
2336
|
+
const performTranslation = async (errorContext) => {
|
|
2337
|
+
switch (translationMode) {
|
|
2338
|
+
case "sanity-only":
|
|
2339
|
+
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating", void 0, mainSourceDocumentId), await getSanityAgentTranslation({
|
|
2340
|
+
sourceDocument: documentToTranslate,
|
|
2341
|
+
targetLanguage,
|
|
2342
|
+
languageFieldName,
|
|
2343
|
+
schemaId,
|
|
2344
|
+
client,
|
|
2345
|
+
translationPrompts
|
|
2346
|
+
});
|
|
2347
|
+
case "openai-only": {
|
|
2348
|
+
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2349
|
+
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2350
|
+
return setJobStatus(
|
|
2351
|
+
dispatch,
|
|
2352
|
+
sourceDocument._id,
|
|
2353
|
+
targetLanguage,
|
|
2354
|
+
"translating_with_openai",
|
|
2355
|
+
void 0,
|
|
2356
|
+
mainSourceDocumentId
|
|
2357
|
+
), await getOpenAITranslation({
|
|
2358
|
+
dispatch,
|
|
2359
|
+
sourceDocument: documentToTranslate,
|
|
2360
|
+
targetLanguage,
|
|
2361
|
+
languageFieldName,
|
|
2362
|
+
translationPrompts,
|
|
2363
|
+
openaiConfig,
|
|
2364
|
+
previousError: errorContext && previousError ? previousError : void 0,
|
|
2365
|
+
errorContext
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
default:
|
|
2369
|
+
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2939
2370
|
}
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
default:
|
|
2943
|
-
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2944
|
-
}
|
|
2945
|
-
if (!translatedDoc)
|
|
2371
|
+
};
|
|
2372
|
+
if (translatedDoc = await performTranslation(), !translatedDoc)
|
|
2946
2373
|
return null;
|
|
2947
|
-
|
|
2374
|
+
let transformedResult;
|
|
2375
|
+
Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
|
|
2948
2376
|
documentId: sourceDocument._id,
|
|
2949
2377
|
documentType: sourceDocument._type
|
|
2950
|
-
})
|
|
2951
|
-
const transformedResult = restoreTransforms({
|
|
2378
|
+
}), transformedResult = restoreTransforms({
|
|
2952
2379
|
translatedDocument: translatedDoc,
|
|
2953
2380
|
originalStructure,
|
|
2954
2381
|
registry: globalTransformRegistry,
|
|
2955
2382
|
schemaType: sourceDocument._type
|
|
2956
|
-
})
|
|
2383
|
+
})) : transformedResult = translatedDoc;
|
|
2384
|
+
const excludedValuesToRestore = { ...excludedValues };
|
|
2957
2385
|
delete excludedValuesToRestore[languageFieldName], devLog.info("Before restoring excluded fields", {
|
|
2958
2386
|
documentId: sourceDocument._id,
|
|
2959
2387
|
excludedFieldCount: Object.keys(excludedValues).length,
|
|
@@ -2972,11 +2400,76 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2972
2400
|
...finalDoc,
|
|
2973
2401
|
_id: targetDocumentVersionId
|
|
2974
2402
|
};
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
}),
|
|
2403
|
+
replaceExisting && await client.getDocument(targetDocumentVersionId) && (devLog.info("Replacing existing version in release", {
|
|
2404
|
+
documentId: sourceDocument._id,
|
|
2405
|
+
targetDocumentVersionId,
|
|
2406
|
+
releaseId
|
|
2407
|
+
}), await client.delete(targetDocumentVersionId));
|
|
2408
|
+
try {
|
|
2409
|
+
await retryWithBackoff(
|
|
2410
|
+
async () => {
|
|
2411
|
+
await client.action({
|
|
2412
|
+
actionType: "sanity.action.document.version.create",
|
|
2413
|
+
publishedId: targetDocumentPublishedId,
|
|
2414
|
+
document: newDoc
|
|
2415
|
+
});
|
|
2416
|
+
},
|
|
2417
|
+
{
|
|
2418
|
+
maxAttempts: 2,
|
|
2419
|
+
initialDelayMs: 0,
|
|
2420
|
+
retryableErrors: isNetworkError,
|
|
2421
|
+
context: {
|
|
2422
|
+
documentId: sourceDocument._id,
|
|
2423
|
+
targetLanguage,
|
|
2424
|
+
operation: "version.create"
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
);
|
|
2428
|
+
} catch (error) {
|
|
2429
|
+
if (translationMode === "openai-only" && isSanityMutationError(error)) {
|
|
2430
|
+
devLog.warn("Mutation error after OpenAI translation, retrying full translation", {
|
|
2431
|
+
documentId: sourceDocument._id,
|
|
2432
|
+
targetLanguage,
|
|
2433
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2434
|
+
}), previousError = error instanceof Error ? error : new Error(String(error));
|
|
2435
|
+
const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure.`;
|
|
2436
|
+
if (translatedDoc = await performTranslation(errorContext), !translatedDoc)
|
|
2437
|
+
throw new Error("Translation retry failed: no document returned");
|
|
2438
|
+
let retryTransformedResult;
|
|
2439
|
+
Object.keys(originalStructure).length > 0 ? retryTransformedResult = restoreTransforms({
|
|
2440
|
+
translatedDocument: translatedDoc,
|
|
2441
|
+
originalStructure,
|
|
2442
|
+
registry: globalTransformRegistry,
|
|
2443
|
+
schemaType: sourceDocument._type
|
|
2444
|
+
}) : retryTransformedResult = translatedDoc;
|
|
2445
|
+
const retryNewDoc = {
|
|
2446
|
+
...restoreExcludedFields(retryTransformedResult, excludedValuesToRestore),
|
|
2447
|
+
_id: targetDocumentVersionId
|
|
2448
|
+
};
|
|
2449
|
+
return await retryWithBackoff(
|
|
2450
|
+
async () => {
|
|
2451
|
+
await client.action({
|
|
2452
|
+
actionType: "sanity.action.document.version.create",
|
|
2453
|
+
publishedId: targetDocumentPublishedId,
|
|
2454
|
+
document: retryNewDoc
|
|
2455
|
+
});
|
|
2456
|
+
},
|
|
2457
|
+
{
|
|
2458
|
+
maxAttempts: 2,
|
|
2459
|
+
initialDelayMs: 0,
|
|
2460
|
+
retryableErrors: isNetworkError,
|
|
2461
|
+
context: {
|
|
2462
|
+
documentId: sourceDocument._id,
|
|
2463
|
+
targetLanguage,
|
|
2464
|
+
operation: "version.create",
|
|
2465
|
+
isRetry: !0
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
), retryNewDoc;
|
|
2469
|
+
}
|
|
2470
|
+
throw error;
|
|
2471
|
+
}
|
|
2472
|
+
return newDoc;
|
|
2980
2473
|
}, translateDocument = async ({
|
|
2981
2474
|
dispatch,
|
|
2982
2475
|
client,
|
|
@@ -2984,84 +2477,66 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2984
2477
|
sourceLanguage,
|
|
2985
2478
|
targetLanguage,
|
|
2986
2479
|
languageFieldName,
|
|
2987
|
-
shouldOverwriteFreshTranslations,
|
|
2988
2480
|
schemaId,
|
|
2989
2481
|
translationPrompts,
|
|
2990
2482
|
translationMode,
|
|
2991
2483
|
openaiConfig,
|
|
2992
2484
|
releaseId,
|
|
2993
|
-
excludeFields
|
|
2485
|
+
excludeFields,
|
|
2486
|
+
replaceExisting,
|
|
2487
|
+
mainSourceDocumentId
|
|
2994
2488
|
}) => {
|
|
2995
2489
|
const logContext = {
|
|
2996
2490
|
documentId: sourceDocument._id,
|
|
2997
2491
|
targetLanguage,
|
|
2998
2492
|
schemaId,
|
|
2999
|
-
shouldOverwriteFreshTranslations,
|
|
3000
2493
|
translationMode
|
|
3001
2494
|
};
|
|
3002
|
-
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
|
|
2495
|
+
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation", void 0, mainSourceDocumentId);
|
|
3003
2496
|
try {
|
|
3004
2497
|
const translationMetadata = await getTranslationMetadata({
|
|
3005
2498
|
client,
|
|
3006
2499
|
sourceDocument,
|
|
3007
2500
|
sourceLanguage
|
|
3008
|
-
}),
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
2501
|
+
}), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
|
|
2502
|
+
dispatch,
|
|
2503
|
+
client,
|
|
2504
|
+
schemaId,
|
|
2505
|
+
sourceDocument,
|
|
2506
|
+
targetLanguage,
|
|
2507
|
+
existingTranslationId,
|
|
2508
|
+
languageFieldName,
|
|
2509
|
+
translationPrompts,
|
|
2510
|
+
translationMode,
|
|
2511
|
+
openaiConfig,
|
|
2512
|
+
releaseId,
|
|
2513
|
+
excludeFields,
|
|
2514
|
+
replaceExisting,
|
|
2515
|
+
mainSourceDocumentId
|
|
2516
|
+
});
|
|
2517
|
+
if (!newDocument) {
|
|
2518
|
+
devLog.error("Translation failed: no document returned", new Error("Translation returned null"), logContext), setJobStatus(
|
|
3019
2519
|
dispatch,
|
|
3020
|
-
|
|
3021
|
-
schemaId,
|
|
3022
|
-
sourceDocument,
|
|
2520
|
+
sourceDocument._id,
|
|
3023
2521
|
targetLanguage,
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
excludeFields
|
|
3031
|
-
}),
|
|
3032
|
-
{
|
|
3033
|
-
maxRetries: 3,
|
|
3034
|
-
initialDelayMs: 1e3,
|
|
3035
|
-
isRetryable: isRetryableError,
|
|
3036
|
-
onRetry: (attempt, error, delayMs) => {
|
|
3037
|
-
devLog.warn("Translation failed, scheduling retry", {
|
|
3038
|
-
...logContext,
|
|
3039
|
-
attempt,
|
|
3040
|
-
delayMs,
|
|
3041
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3042
|
-
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "retrying", {
|
|
3043
|
-
retryCount: attempt,
|
|
3044
|
-
errorMessage: error instanceof Error ? error.message : String(error)
|
|
3045
|
-
});
|
|
3046
|
-
}
|
|
3047
|
-
}
|
|
3048
|
-
);
|
|
3049
|
-
if (!translationResult.success || !translationResult.result) {
|
|
3050
|
-
const errorMessage = translationResult.error instanceof Error ? translationResult.error.message : String(translationResult.error || "Translation failed");
|
|
3051
|
-
devLog.error("Translation failed after retries", translationResult.error, {
|
|
3052
|
-
...logContext,
|
|
3053
|
-
attempts: translationResult.attempts
|
|
3054
|
-
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
3055
|
-
errorMessage,
|
|
3056
|
-
retryCount: translationResult.attempts - 1
|
|
3057
|
-
// -1 because attempts includes initial try
|
|
3058
|
-
}), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
2522
|
+
"error",
|
|
2523
|
+
{
|
|
2524
|
+
errorMessage: "Translation failed"
|
|
2525
|
+
},
|
|
2526
|
+
mainSourceDocumentId
|
|
2527
|
+
), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
3059
2528
|
return;
|
|
3060
2529
|
}
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
2530
|
+
return setJobStatus(
|
|
2531
|
+
dispatch,
|
|
2532
|
+
sourceDocument._id,
|
|
2533
|
+
targetLanguage,
|
|
2534
|
+
"updating_metadata",
|
|
2535
|
+
{
|
|
2536
|
+
targetId: newDocument._id
|
|
2537
|
+
},
|
|
2538
|
+
mainSourceDocumentId
|
|
2539
|
+
), await updateTranslationMetadata({
|
|
3065
2540
|
client,
|
|
3066
2541
|
sourceDocument,
|
|
3067
2542
|
sourceLanguage,
|
|
@@ -3069,17 +2544,31 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3069
2544
|
targetLanguage,
|
|
3070
2545
|
translationMetadata,
|
|
3071
2546
|
isNewTranslation: !existingTranslationId
|
|
3072
|
-
}), setJobStatus(
|
|
3073
|
-
|
|
3074
|
-
|
|
2547
|
+
}), setJobStatus(
|
|
2548
|
+
dispatch,
|
|
2549
|
+
sourceDocument._id,
|
|
2550
|
+
targetLanguage,
|
|
2551
|
+
"completed",
|
|
2552
|
+
{
|
|
2553
|
+
targetId: newDocument._id
|
|
2554
|
+
},
|
|
2555
|
+
mainSourceDocumentId
|
|
2556
|
+
), devLog.info("Translation completed successfully", {
|
|
3075
2557
|
...logContext,
|
|
3076
2558
|
newDocumentId: newDocument._id
|
|
3077
2559
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
3078
2560
|
} catch (error) {
|
|
3079
2561
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3080
|
-
devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(
|
|
3081
|
-
|
|
3082
|
-
|
|
2562
|
+
devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(
|
|
2563
|
+
dispatch,
|
|
2564
|
+
sourceDocument._id,
|
|
2565
|
+
targetLanguage,
|
|
2566
|
+
"error",
|
|
2567
|
+
{
|
|
2568
|
+
errorMessage
|
|
2569
|
+
},
|
|
2570
|
+
mainSourceDocumentId
|
|
2571
|
+
), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
3083
2572
|
return;
|
|
3084
2573
|
}
|
|
3085
2574
|
}, translationSemaphore = new Semaphore(2), translateDocumentSet = async ({
|
|
@@ -3088,13 +2577,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3088
2577
|
targetLanguages,
|
|
3089
2578
|
dispatch,
|
|
3090
2579
|
languageFieldName,
|
|
3091
|
-
shouldOverwriteFreshTranslations,
|
|
3092
2580
|
schemaId,
|
|
3093
2581
|
references = [],
|
|
3094
2582
|
maxConcurrentTranslationJobs,
|
|
3095
2583
|
translationMode,
|
|
3096
2584
|
openaiConfig,
|
|
3097
|
-
excludeFields
|
|
2585
|
+
excludeFields,
|
|
2586
|
+
releaseConfig
|
|
3098
2587
|
}) => {
|
|
3099
2588
|
const logContext = {
|
|
3100
2589
|
documentId: sourceDocument._id,
|
|
@@ -3103,28 +2592,48 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3103
2592
|
referencesCount: references.length,
|
|
3104
2593
|
maxConcurrentJobs: maxConcurrentTranslationJobs,
|
|
3105
2594
|
translationMode,
|
|
3106
|
-
|
|
2595
|
+
releaseMode: releaseConfig.mode
|
|
3107
2596
|
};
|
|
3108
|
-
let releaseId;
|
|
2597
|
+
let releaseId, createdNewRelease = !1;
|
|
3109
2598
|
try {
|
|
3110
2599
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
3111
2600
|
if (!sourceLanguage)
|
|
3112
2601
|
throw devLog.error("Source language not found"), new Error("Source language not found");
|
|
3113
|
-
maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
2602
|
+
if (maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
3114
2603
|
type: "SET_OVERALL_STATUS",
|
|
3115
2604
|
payload: "translating"
|
|
3116
|
-
})
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
}
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
2605
|
+
}), releaseConfig.mode === "existing" && releaseConfig.existingReleaseId)
|
|
2606
|
+
releaseId = releaseConfig.existingReleaseId, devLog.info("Using existing Content Release", {
|
|
2607
|
+
...logContext,
|
|
2608
|
+
releaseId
|
|
2609
|
+
});
|
|
2610
|
+
else {
|
|
2611
|
+
const releaseTitle = releaseConfig.newReleaseTitle, releaseDescription = `AI translation batch created on ${(/* @__PURE__ */ new Date()).toLocaleDateString()} at ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`;
|
|
2612
|
+
releaseId = (await retryWithBackoff(
|
|
2613
|
+
async () => await client.releases.create({
|
|
2614
|
+
metadata: {
|
|
2615
|
+
title: releaseTitle,
|
|
2616
|
+
description: releaseDescription,
|
|
2617
|
+
releaseType: "undecided"
|
|
2618
|
+
}
|
|
2619
|
+
}),
|
|
2620
|
+
{
|
|
2621
|
+
maxAttempts: 2,
|
|
2622
|
+
initialDelayMs: 0,
|
|
2623
|
+
retryableErrors: isNetworkError,
|
|
2624
|
+
context: {
|
|
2625
|
+
operation: "releases.create",
|
|
2626
|
+
releaseTitle,
|
|
2627
|
+
...logContext
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
)).releaseId, createdNewRelease = !0, devLog.info("Content Release created", {
|
|
2631
|
+
...logContext,
|
|
2632
|
+
releaseId,
|
|
2633
|
+
releaseTitle
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
registerActiveTranslation(sourceDocument._id, releaseId), updateActiveTranslationStatus(sourceDocument._id, "translating");
|
|
3128
2637
|
const sourceDocuments = [
|
|
3129
2638
|
{
|
|
3130
2639
|
_id: sourceDocument._id,
|
|
@@ -3139,7 +2648,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3139
2648
|
targetLanguages,
|
|
3140
2649
|
sourceDocument
|
|
3141
2650
|
});
|
|
3142
|
-
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
|
|
2651
|
+
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages, sourceDocument._id);
|
|
3143
2652
|
const translationTasks = sourceDocumentIds.flatMap(
|
|
3144
2653
|
(documentId) => targetLanguages.map(async (targetLanguage) => {
|
|
3145
2654
|
try {
|
|
@@ -3147,7 +2656,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3147
2656
|
const document = await client.withConfig({
|
|
3148
2657
|
perspective: "drafts"
|
|
3149
2658
|
}).getDocument(documentId);
|
|
3150
|
-
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document &&
|
|
2659
|
+
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId) {
|
|
3151
2660
|
const versionedTargetDocumentId = await translateDocument({
|
|
3152
2661
|
dispatch,
|
|
3153
2662
|
client,
|
|
@@ -3155,13 +2664,15 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3155
2664
|
sourceLanguage,
|
|
3156
2665
|
targetLanguage,
|
|
3157
2666
|
languageFieldName,
|
|
3158
|
-
shouldOverwriteFreshTranslations,
|
|
3159
2667
|
schemaId,
|
|
3160
2668
|
translationPrompts,
|
|
3161
2669
|
translationMode,
|
|
3162
2670
|
openaiConfig,
|
|
3163
|
-
releaseId
|
|
3164
|
-
excludeFields
|
|
2671
|
+
releaseId,
|
|
2672
|
+
excludeFields,
|
|
2673
|
+
replaceExisting: releaseConfig.replaceExisting,
|
|
2674
|
+
// Pass main source document ID for active translations tracking
|
|
2675
|
+
mainSourceDocumentId: sourceDocument._id
|
|
3165
2676
|
});
|
|
3166
2677
|
versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
|
|
3167
2678
|
}
|
|
@@ -3188,7 +2699,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3188
2699
|
}), dispatch({
|
|
3189
2700
|
type: "SET_OVERALL_STATUS",
|
|
3190
2701
|
payload: "linking_references"
|
|
3191
|
-
}), await Promise.all(
|
|
2702
|
+
}), updateActiveTranslationStatus(sourceDocument._id, "linking_references"), await Promise.all(
|
|
3192
2703
|
versionedTargetDocumentIds.map(async (documentId) => {
|
|
3193
2704
|
await localizeReferences({
|
|
3194
2705
|
client,
|
|
@@ -3199,18 +2710,65 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3199
2710
|
})
|
|
3200
2711
|
);
|
|
3201
2712
|
const hasAnyFailures = failed > 0;
|
|
3202
|
-
hasAnyFailures && succeeded === 0 ? (devLog.error("All translation tasks failed", void 0, logContext), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" })) : hasAnyFailures ? (devLog.warn("Translation completed with some failures", {
|
|
2713
|
+
hasAnyFailures && succeeded === 0 ? (devLog.error("All translation tasks failed", void 0, logContext), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id)) : hasAnyFailures ? (devLog.warn("Translation completed with some failures", {
|
|
3203
2714
|
...logContext,
|
|
3204
2715
|
succeeded,
|
|
3205
2716
|
failed
|
|
3206
|
-
}), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
|
|
2717
|
+
}), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), updateActiveTranslationStatus(sourceDocument._id, "completed"), cleanupActiveTranslation(sourceDocument._id)) : (dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), updateActiveTranslationStatus(sourceDocument._id, "completed"), cleanupActiveTranslation(sourceDocument._id)), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
|
|
3207
2718
|
} catch (error) {
|
|
3208
|
-
|
|
2719
|
+
if (devLog.error(
|
|
3209
2720
|
"translateDocumentSet failed with unexpected error",
|
|
3210
2721
|
error instanceof Error ? error : new Error(String(error)),
|
|
3211
2722
|
logContext
|
|
3212
|
-
),
|
|
2723
|
+
), releaseId && createdNewRelease)
|
|
2724
|
+
try {
|
|
2725
|
+
await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
|
|
2726
|
+
...logContext,
|
|
2727
|
+
releaseId
|
|
2728
|
+
});
|
|
2729
|
+
} catch (cleanupError) {
|
|
2730
|
+
devLog.error(
|
|
2731
|
+
"Failed to cleanup release",
|
|
2732
|
+
cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
|
|
2733
|
+
logContext
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id), error;
|
|
3213
2737
|
}
|
|
2738
|
+
}, ReleaseConflictWarning = ({ conflicts, onCancel, onReplace }) => {
|
|
2739
|
+
const uniqueDocuments = useMemo(() => new Set(conflicts.map((c) => c.sourceDocumentId)).size, [conflicts]), summaryText = useMemo(() => conflicts.length === 1 ? "1 translation" : uniqueDocuments === 1 ? `${conflicts.length} translations` : `${conflicts.length} translations across ${uniqueDocuments} documents`, [conflicts.length, uniqueDocuments]);
|
|
2740
|
+
return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2741
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
2742
|
+
/* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
|
|
2743
|
+
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "This release already contains translations for this content and target language(s)." })
|
|
2744
|
+
] }),
|
|
2745
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2746
|
+
"The selected release already has ",
|
|
2747
|
+
summaryText,
|
|
2748
|
+
" that will be overwritten if you continue:"
|
|
2749
|
+
] }),
|
|
2750
|
+
/* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsx(Box, { style: { maxHeight: "500px", overflowY: "auto" }, children: /* @__PURE__ */ jsx(Stack, { space: 2, children: conflicts.map((conflict) => /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
2751
|
+
conflict.sourceDocumentTitle || conflict.sourceDocumentId,
|
|
2752
|
+
conflict.sourceDocumentType && ` (${conflict.sourceDocumentType})`,
|
|
2753
|
+
" \u2192 ",
|
|
2754
|
+
conflict.targetLanguage
|
|
2755
|
+
] }, conflict.existingVersionId)) }) }) }),
|
|
2756
|
+
/* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2757
|
+
/* @__PURE__ */ jsx(Heading, { size: 0, children: "Choose an action:" }),
|
|
2758
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2759
|
+
/* @__PURE__ */ jsx("strong", { children: "Cancel" }),
|
|
2760
|
+
" \u2014 Go back and select a different release, or create a new one."
|
|
2761
|
+
] }),
|
|
2762
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2763
|
+
/* @__PURE__ */ jsx("strong", { children: "Replace" }),
|
|
2764
|
+
" \u2014 Delete the existing translations in this release and create new ones."
|
|
2765
|
+
] })
|
|
2766
|
+
] }),
|
|
2767
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", children: [
|
|
2768
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
|
|
2769
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "caution", text: "Replace Existing", onClick: onReplace })
|
|
2770
|
+
] })
|
|
2771
|
+
] }) });
|
|
3214
2772
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
3215
2773
|
const {
|
|
3216
2774
|
document,
|
|
@@ -3218,40 +2776,83 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3218
2776
|
overallStatus,
|
|
3219
2777
|
dispatch,
|
|
3220
2778
|
references,
|
|
2779
|
+
selectedReferenceIds,
|
|
3221
2780
|
selectedTargetLanguages,
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }),
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
2781
|
+
translationMode,
|
|
2782
|
+
releaseConfig
|
|
2783
|
+
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1), filteredReferences = references.filter((ref) => selectedReferenceIds.has(ref._id));
|
|
2784
|
+
useEffect(() => {
|
|
2785
|
+
releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
|
|
2786
|
+
}, [releaseConfig.mode, conflictContext]);
|
|
2787
|
+
const startTranslation = useCallback(
|
|
2788
|
+
(replaceExisting, overrideReleaseId) => {
|
|
2789
|
+
setConflictContext(null), translateDocumentSet({
|
|
2790
|
+
sourceDocument: document,
|
|
2791
|
+
targetLanguages: selectedTargetLanguages,
|
|
2792
|
+
client,
|
|
2793
|
+
dispatch,
|
|
2794
|
+
languageFieldName: config.languageFieldName,
|
|
2795
|
+
schemaId: config.schemaId,
|
|
2796
|
+
references: filteredReferences,
|
|
2797
|
+
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
2798
|
+
maxReferenceDepth: config.maxReferenceDepth,
|
|
2799
|
+
translationMode,
|
|
2800
|
+
openaiConfig: config.openai,
|
|
2801
|
+
excludeFields: config.excludeFields,
|
|
2802
|
+
releaseConfig: {
|
|
2803
|
+
...releaseConfig,
|
|
2804
|
+
replaceExisting,
|
|
2805
|
+
// Use the override release ID if provided (from conflict context),
|
|
2806
|
+
// otherwise use the current releaseConfig value
|
|
2807
|
+
existingReleaseId: overrideReleaseId ?? releaseConfig.existingReleaseId
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
},
|
|
2811
|
+
[
|
|
2812
|
+
document,
|
|
2813
|
+
selectedTargetLanguages,
|
|
3228
2814
|
client,
|
|
3229
2815
|
dispatch,
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
2816
|
+
config.languageFieldName,
|
|
2817
|
+
config.schemaId,
|
|
2818
|
+
config.maxConcurrentTranslationJobs,
|
|
2819
|
+
config.maxReferenceDepth,
|
|
2820
|
+
config.openai,
|
|
2821
|
+
config.excludeFields,
|
|
2822
|
+
filteredReferences,
|
|
3236
2823
|
translationMode,
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
2824
|
+
releaseConfig
|
|
2825
|
+
]
|
|
2826
|
+
), handleTranslate = useCallback(async () => {
|
|
2827
|
+
if (releaseConfig.mode === "new") {
|
|
2828
|
+
startTranslation(!1);
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
if (releaseConfig.mode === "existing" && releaseConfig.existingReleaseId) {
|
|
2832
|
+
const checkedReleaseId = releaseConfig.existingReleaseId;
|
|
2833
|
+
setIsCheckingConflicts(!0);
|
|
2834
|
+
const foundConflicts = await checkReleaseConflicts({
|
|
2835
|
+
client,
|
|
2836
|
+
releaseId: checkedReleaseId,
|
|
2837
|
+
sourceDocument: document,
|
|
2838
|
+
targetLanguages: selectedTargetLanguages,
|
|
2839
|
+
languageFieldName: config.languageFieldName,
|
|
2840
|
+
references: filteredReferences
|
|
2841
|
+
});
|
|
2842
|
+
if (setIsCheckingConflicts(!1), foundConflicts.length > 0) {
|
|
2843
|
+
setConflictContext({
|
|
2844
|
+
releaseId: checkedReleaseId,
|
|
2845
|
+
conflicts: foundConflicts
|
|
2846
|
+
});
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
startTranslation(!1);
|
|
2851
|
+
}, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, filteredReferences]), handleCancelConflict = useCallback(() => {
|
|
2852
|
+
setConflictContext(null);
|
|
2853
|
+
}, []), handleReplaceConflict = useCallback(() => {
|
|
2854
|
+
conflictContext && startTranslation(!0, conflictContext.releaseId);
|
|
2855
|
+
}, [startTranslation, conflictContext]), handleSelectAll = useCallback(() => {
|
|
3255
2856
|
dispatch({
|
|
3256
2857
|
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
3257
2858
|
payload: config.supportedLanguages.map((lang) => lang.id).filter((id) => id !== document.language)
|
|
@@ -3262,7 +2863,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3262
2863
|
payload: []
|
|
3263
2864
|
});
|
|
3264
2865
|
}, [dispatch]);
|
|
3265
|
-
return /* @__PURE__ */
|
|
2866
|
+
return conflictContext ? /* @__PURE__ */ jsx(Stack, { space: 4, children: /* @__PURE__ */ jsx(
|
|
2867
|
+
ReleaseConflictWarning,
|
|
2868
|
+
{
|
|
2869
|
+
conflicts: conflictContext.conflicts,
|
|
2870
|
+
onCancel: handleCancelConflict,
|
|
2871
|
+
onReplace: handleReplaceConflict
|
|
2872
|
+
}
|
|
2873
|
+
) }) : /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
|
|
3266
2874
|
/* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
|
|
3267
2875
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectAll, children: "Select All" }),
|
|
3268
2876
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectNone, children: "Select None" })
|
|
@@ -3276,16 +2884,41 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3276
2884
|
tone: "positive",
|
|
3277
2885
|
size: 2,
|
|
3278
2886
|
onClick: handleTranslate,
|
|
3279
|
-
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus),
|
|
3280
|
-
children: "Translate"
|
|
2887
|
+
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus) || releaseConfig.mode === "existing" && !releaseConfig.existingReleaseId || isCheckingConflicts,
|
|
2888
|
+
children: isCheckingConflicts ? "Checking..." : "Translate"
|
|
3281
2889
|
}
|
|
3282
2890
|
)
|
|
3283
2891
|
] })
|
|
3284
2892
|
] });
|
|
2893
|
+
}, TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
|
|
2894
|
+
const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
|
|
2895
|
+
params: {
|
|
2896
|
+
documentId: undraftId(documentId),
|
|
2897
|
+
translationMetadataType: TRANSLATION_METADATA_TYPE
|
|
2898
|
+
},
|
|
2899
|
+
initialValue: null
|
|
2900
|
+
});
|
|
2901
|
+
return useEffect(() => {
|
|
2902
|
+
if (loading) {
|
|
2903
|
+
dispatch({ type: "SET_OVERALL_STATUS", payload: "fetching_metadata" });
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2906
|
+
if (error) {
|
|
2907
|
+
dispatch({ type: "SET_OVERALL_STATUS", payload: "error" });
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
if (data) {
|
|
2911
|
+
dispatch({ type: "SET_TRANSLATION_METADATA", payload: data }), dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
|
|
2915
|
+
}, [data, loading, error, dispatch]), {
|
|
2916
|
+
data,
|
|
2917
|
+
loading,
|
|
2918
|
+
error
|
|
2919
|
+
};
|
|
3285
2920
|
}, LanguageCheckbox = ({ lang, isLoading }) => {
|
|
3286
|
-
const { document, dispatch, selectedTargetLanguages
|
|
3287
|
-
(history) => history.language === lang.id && history.sourceRev !== document._rev
|
|
3288
|
-
), handleChange = () => {
|
|
2921
|
+
const { document, dispatch, selectedTargetLanguages } = useTranslationContext(), isDisabled = lang.id === document.language, isChecked = selectedTargetLanguages.includes(lang.id), handleChange = () => {
|
|
3289
2922
|
if (isDisabled) return;
|
|
3290
2923
|
const updatedLanguages = isChecked ? selectedTargetLanguages.filter((langId) => langId !== lang.id) : [...selectedTargetLanguages, lang.id];
|
|
3291
2924
|
dispatch({
|
|
@@ -3303,16 +2936,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3303
2936
|
disabled: isDisabled,
|
|
3304
2937
|
tone: isChecked ? "primary" : "default",
|
|
3305
2938
|
muted: !0,
|
|
3306
|
-
children: /* @__PURE__ */
|
|
3307
|
-
/* @__PURE__ */
|
|
3308
|
-
|
|
3309
|
-
/* @__PURE__ */
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
] }),
|
|
3314
|
-
!isDisabled && hasTranslation && /* @__PURE__ */ jsx(Flex, { flex: 1, justify: "flex-end", children: /* @__PURE__ */ jsx(Badge, { tone: isStale ? "caution" : "positive", size: 3, muted: !0, children: isStale ? "Stale" : "Fresh" }) })
|
|
3315
|
-
] })
|
|
2939
|
+
children: /* @__PURE__ */ jsx(Flex, { align: "center", gap: 2, style: { cursor: "pointer" }, children: /* @__PURE__ */ jsxs(Inline, { space: 3, children: [
|
|
2940
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: isChecked, onChange: handleChange, label: lang.title, disabled: isDisabled }),
|
|
2941
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
2942
|
+
/* @__PURE__ */ jsx(Text, { children: lang.title }),
|
|
2943
|
+
isDisabled && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "The current language" })
|
|
2944
|
+
] })
|
|
2945
|
+
] }) })
|
|
3316
2946
|
}
|
|
3317
2947
|
);
|
|
3318
2948
|
}, LanguageSelectionPanel = () => {
|
|
@@ -3320,6 +2950,134 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3320
2950
|
documentId: document._id
|
|
3321
2951
|
});
|
|
3322
2952
|
return /* @__PURE__ */ jsx(Card, { padding: 4, muted: !0, border: !0, children: /* @__PURE__ */ jsx(Grid, { columns: [1, 2], gap: 3, children: config?.supportedLanguages.map((lang) => /* @__PURE__ */ jsx(LanguageCheckbox, { lang, isLoading: loading }, lang.id)) }) });
|
|
2953
|
+
}, ReleaseSelectionPanel = () => {
|
|
2954
|
+
const { dispatch, releaseConfig } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [releases, setReleases] = useState([]), [isLoading, setIsLoading] = useState(!1), [error, setError] = useState(null), [searchQuery, setSearchQuery] = useState("");
|
|
2955
|
+
useEffect(() => {
|
|
2956
|
+
(async () => {
|
|
2957
|
+
setIsLoading(!0), setError(null);
|
|
2958
|
+
try {
|
|
2959
|
+
const result = await client.fetch(
|
|
2960
|
+
'releases::all()[state == "active"] | order(_createdAt desc)'
|
|
2961
|
+
);
|
|
2962
|
+
setReleases(result), devLog.debug("Fetched active releases", {
|
|
2963
|
+
count: result.length,
|
|
2964
|
+
component: "ReleaseSelectionPanel"
|
|
2965
|
+
});
|
|
2966
|
+
} catch (err) {
|
|
2967
|
+
const errorMessage = err instanceof Error ? err.message : "Failed to fetch releases";
|
|
2968
|
+
setError(errorMessage), devLog.error("Failed to fetch releases", err instanceof Error ? err : new Error(errorMessage), {
|
|
2969
|
+
component: "ReleaseSelectionPanel"
|
|
2970
|
+
});
|
|
2971
|
+
} finally {
|
|
2972
|
+
setIsLoading(!1);
|
|
2973
|
+
}
|
|
2974
|
+
})();
|
|
2975
|
+
}, [client]);
|
|
2976
|
+
const handleModeChange = useCallback(
|
|
2977
|
+
(mode) => {
|
|
2978
|
+
dispatch({
|
|
2979
|
+
type: "SET_RELEASE_CONFIG",
|
|
2980
|
+
payload: { mode }
|
|
2981
|
+
});
|
|
2982
|
+
},
|
|
2983
|
+
[dispatch]
|
|
2984
|
+
), handleTitleChange = useCallback(
|
|
2985
|
+
(event) => {
|
|
2986
|
+
dispatch({
|
|
2987
|
+
type: "SET_RELEASE_CONFIG",
|
|
2988
|
+
payload: { newReleaseTitle: event.target.value }
|
|
2989
|
+
});
|
|
2990
|
+
},
|
|
2991
|
+
[dispatch]
|
|
2992
|
+
), handleSelectRelease = useCallback(
|
|
2993
|
+
(releaseId) => {
|
|
2994
|
+
dispatch({
|
|
2995
|
+
type: "SET_RELEASE_CONFIG",
|
|
2996
|
+
payload: { existingReleaseId: releaseId }
|
|
2997
|
+
});
|
|
2998
|
+
},
|
|
2999
|
+
[dispatch]
|
|
3000
|
+
), handleSearchChange = useCallback((event) => {
|
|
3001
|
+
setSearchQuery(event.target.value);
|
|
3002
|
+
}, []), filteredReleases = useMemo(() => {
|
|
3003
|
+
if (!searchQuery.trim())
|
|
3004
|
+
return releases;
|
|
3005
|
+
const query = searchQuery.toLowerCase();
|
|
3006
|
+
return releases.filter((release) => release.metadata.title.toLowerCase().includes(query));
|
|
3007
|
+
}, [releases, searchQuery]), formatDate = (dateString) => new Date(dateString).toLocaleDateString(void 0, {
|
|
3008
|
+
month: "short",
|
|
3009
|
+
day: "numeric",
|
|
3010
|
+
year: "numeric"
|
|
3011
|
+
});
|
|
3012
|
+
return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
3013
|
+
/* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3014
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, as: "label", style: { cursor: "pointer" }, onClick: () => handleModeChange("new"), children: [
|
|
3015
|
+
/* @__PURE__ */ jsx(Radio, { checked: releaseConfig.mode === "new", readOnly: !0 }),
|
|
3016
|
+
/* @__PURE__ */ jsx(Text, { weight: "medium", children: "Create new release" })
|
|
3017
|
+
] }),
|
|
3018
|
+
/* @__PURE__ */ jsxs(
|
|
3019
|
+
Flex,
|
|
3020
|
+
{
|
|
3021
|
+
align: "center",
|
|
3022
|
+
gap: 3,
|
|
3023
|
+
as: "label",
|
|
3024
|
+
style: { cursor: "pointer" },
|
|
3025
|
+
onClick: () => handleModeChange("existing"),
|
|
3026
|
+
children: [
|
|
3027
|
+
/* @__PURE__ */ jsx(Radio, { checked: releaseConfig.mode === "existing", readOnly: !0 }),
|
|
3028
|
+
/* @__PURE__ */ jsx(Text, { weight: "medium", children: "Add to existing release" })
|
|
3029
|
+
]
|
|
3030
|
+
}
|
|
3031
|
+
)
|
|
3032
|
+
] }),
|
|
3033
|
+
releaseConfig.mode === "new" && /* @__PURE__ */ jsx(
|
|
3034
|
+
TextInput,
|
|
3035
|
+
{
|
|
3036
|
+
value: releaseConfig.newReleaseTitle,
|
|
3037
|
+
onChange: handleTitleChange,
|
|
3038
|
+
placeholder: "Release title..."
|
|
3039
|
+
}
|
|
3040
|
+
),
|
|
3041
|
+
releaseConfig.mode === "existing" && /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3042
|
+
isLoading && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
3043
|
+
/* @__PURE__ */ jsx(Spinner, { muted: !0 }),
|
|
3044
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Loading..." })
|
|
3045
|
+
] }),
|
|
3046
|
+
error && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: error }),
|
|
3047
|
+
!isLoading && !error && releases.length === 0 && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "No active releases found." }),
|
|
3048
|
+
!isLoading && !error && releases.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3049
|
+
/* @__PURE__ */ jsx(
|
|
3050
|
+
TextInput,
|
|
3051
|
+
{
|
|
3052
|
+
icon: SearchIcon,
|
|
3053
|
+
value: searchQuery,
|
|
3054
|
+
onChange: handleSearchChange,
|
|
3055
|
+
placeholder: "Search releases..."
|
|
3056
|
+
}
|
|
3057
|
+
),
|
|
3058
|
+
/* @__PURE__ */ jsx(Box, { style: { maxHeight: "200px", overflowY: "auto" }, children: /* @__PURE__ */ jsx(Stack, { space: 1, children: filteredReleases.length === 0 ? /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "No releases match your search." }) : filteredReleases.map((release) => {
|
|
3059
|
+
const releaseName = release._id.replace("_.releases.", ""), isSelected = releaseConfig.existingReleaseId === releaseName;
|
|
3060
|
+
return /* @__PURE__ */ jsx(
|
|
3061
|
+
Card,
|
|
3062
|
+
{
|
|
3063
|
+
padding: 3,
|
|
3064
|
+
radius: 2,
|
|
3065
|
+
tone: isSelected ? "primary" : "default",
|
|
3066
|
+
style: { cursor: "pointer" },
|
|
3067
|
+
onClick: () => handleSelectRelease(releaseName),
|
|
3068
|
+
border: !0,
|
|
3069
|
+
muted: isSelected,
|
|
3070
|
+
children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
3071
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: release.metadata.title }),
|
|
3072
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: formatDate(release._createdAt) })
|
|
3073
|
+
] })
|
|
3074
|
+
},
|
|
3075
|
+
release._id
|
|
3076
|
+
);
|
|
3077
|
+
}) }) })
|
|
3078
|
+
] })
|
|
3079
|
+
] })
|
|
3080
|
+
] }) });
|
|
3323
3081
|
}, TranslationModePanel = () => {
|
|
3324
3082
|
const { config, translationMode, openaiValidated, dispatch } = useTranslationContext();
|
|
3325
3083
|
if (!config?.openai?.enabled || !config?.openai?.apiKey)
|
|
@@ -3338,7 +3096,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3338
3096
|
return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3339
3097
|
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3340
3098
|
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "Translation Mode" }),
|
|
3341
|
-
/* @__PURE__ */ jsx(Badge, { tone: "positive", children: "
|
|
3099
|
+
/* @__PURE__ */ jsx(Badge, { tone: "positive", children: "OpenAI Available" })
|
|
3342
3100
|
] }),
|
|
3343
3101
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3344
3102
|
/* @__PURE__ */ jsx(
|
|
@@ -3368,32 +3126,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3368
3126
|
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "openai-only", readOnly: !0 }),
|
|
3369
3127
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3370
3128
|
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3371
|
-
/* @__PURE__ */ jsx(Text, { children: "
|
|
3129
|
+
/* @__PURE__ */ jsx(Text, { children: "OpenAI Only" }),
|
|
3372
3130
|
/* @__PURE__ */ jsx(Badge, { tone: "caution", children: "Beta" })
|
|
3373
3131
|
] }),
|
|
3374
3132
|
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Full style guide capacity with 1M token context window." })
|
|
3375
3133
|
] })
|
|
3376
3134
|
] })
|
|
3377
3135
|
}
|
|
3378
|
-
),
|
|
3379
|
-
/* @__PURE__ */ jsx(
|
|
3380
|
-
Button,
|
|
3381
|
-
{
|
|
3382
|
-
type: "button",
|
|
3383
|
-
mode: translationMode === "sanity-with-review" ? "ghost" : "bleed",
|
|
3384
|
-
tone: translationMode === "sanity-with-review" ? "primary" : "default",
|
|
3385
|
-
onClick: () => handleModeChange("sanity-with-review"),
|
|
3386
|
-
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3387
|
-
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "sanity-with-review", readOnly: !0 }),
|
|
3388
|
-
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3389
|
-
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3390
|
-
/* @__PURE__ */ jsx(Text, { children: "Sanity + GPT-5 Review" }),
|
|
3391
|
-
/* @__PURE__ */ jsx(Badge, { tone: "primary", children: "Highest Quality" })
|
|
3392
|
-
] }),
|
|
3393
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Sanity translation refined by GPT-5. Takes longer but best results." })
|
|
3394
|
-
] })
|
|
3395
|
-
] })
|
|
3396
|
-
}
|
|
3397
3136
|
)
|
|
3398
3137
|
] }),
|
|
3399
3138
|
translationMode !== "sanity-only" && /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 3, border: !0, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
@@ -3483,8 +3222,104 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3483
3222
|
});
|
|
3484
3223
|
}
|
|
3485
3224
|
return result;
|
|
3225
|
+
}, ReferenceSelectionPanel = () => {
|
|
3226
|
+
const { references, selectedReferenceIds, dispatch } = useTranslationContext(), [typeFilter, setTypeFilter] = useState("all"), documentTypes = useMemo(() => {
|
|
3227
|
+
const types = new Set(references.map((ref) => ref._type));
|
|
3228
|
+
return Array.from(types).sort();
|
|
3229
|
+
}, [references]), filteredReferences = useMemo(() => typeFilter === "all" ? references : references.filter((ref) => ref._type === typeFilter), [references, typeFilter]), selectedCount = useMemo(() => filteredReferences.filter((ref) => selectedReferenceIds.has(ref._id)).length, [filteredReferences, selectedReferenceIds]), handleToggleReference = (referenceId) => {
|
|
3230
|
+
const newSelection = new Set(selectedReferenceIds);
|
|
3231
|
+
newSelection.has(referenceId) ? newSelection.delete(referenceId) : newSelection.add(referenceId), dispatch({
|
|
3232
|
+
type: "SET_SELECTED_REFERENCE_IDS",
|
|
3233
|
+
payload: newSelection
|
|
3234
|
+
});
|
|
3235
|
+
}, handleSelectAll = () => {
|
|
3236
|
+
const newSelection = new Set(selectedReferenceIds);
|
|
3237
|
+
filteredReferences.forEach((ref) => {
|
|
3238
|
+
newSelection.add(ref._id);
|
|
3239
|
+
}), dispatch({
|
|
3240
|
+
type: "SET_SELECTED_REFERENCE_IDS",
|
|
3241
|
+
payload: newSelection
|
|
3242
|
+
});
|
|
3243
|
+
}, handleSelectNone = () => {
|
|
3244
|
+
const newSelection = new Set(selectedReferenceIds);
|
|
3245
|
+
filteredReferences.forEach((ref) => {
|
|
3246
|
+
newSelection.delete(ref._id);
|
|
3247
|
+
}), dispatch({
|
|
3248
|
+
type: "SET_SELECTED_REFERENCE_IDS",
|
|
3249
|
+
payload: newSelection
|
|
3250
|
+
});
|
|
3251
|
+
};
|
|
3252
|
+
return references.length === 0 ? null : /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3253
|
+
/* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", gap: 2, children: [
|
|
3254
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
3255
|
+
/* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "Select References" }),
|
|
3256
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3257
|
+
"(",
|
|
3258
|
+
selectedCount,
|
|
3259
|
+
" of ",
|
|
3260
|
+
filteredReferences.length,
|
|
3261
|
+
" selected)"
|
|
3262
|
+
] })
|
|
3263
|
+
] }),
|
|
3264
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
3265
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 1, onClick: handleSelectAll, text: "Select All" }),
|
|
3266
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 1, onClick: handleSelectNone, text: "Select None" })
|
|
3267
|
+
] })
|
|
3268
|
+
] }),
|
|
3269
|
+
documentTypes.length > 1 && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
3270
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Filter by type:" }),
|
|
3271
|
+
/* @__PURE__ */ jsxs(
|
|
3272
|
+
Select,
|
|
3273
|
+
{
|
|
3274
|
+
value: typeFilter,
|
|
3275
|
+
onChange: (event) => setTypeFilter(event.target.value),
|
|
3276
|
+
style: { minWidth: "150px" },
|
|
3277
|
+
children: [
|
|
3278
|
+
/* @__PURE__ */ jsxs("option", { value: "all", children: [
|
|
3279
|
+
"All types (",
|
|
3280
|
+
references.length,
|
|
3281
|
+
")"
|
|
3282
|
+
] }),
|
|
3283
|
+
documentTypes.map((type) => {
|
|
3284
|
+
const count = references.filter((ref) => ref._type === type).length;
|
|
3285
|
+
return /* @__PURE__ */ jsxs("option", { value: type, children: [
|
|
3286
|
+
type,
|
|
3287
|
+
" (",
|
|
3288
|
+
count,
|
|
3289
|
+
")"
|
|
3290
|
+
] }, type);
|
|
3291
|
+
})
|
|
3292
|
+
]
|
|
3293
|
+
}
|
|
3294
|
+
)
|
|
3295
|
+
] }),
|
|
3296
|
+
/* @__PURE__ */ jsx(Box, { style: { maxHeight: "400px", overflowY: "auto" }, children: /* @__PURE__ */ jsx(Stack, { space: 2, children: filteredReferences.length === 0 ? /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "No references match the selected filter." }) : filteredReferences.map((reference) => {
|
|
3297
|
+
const isSelected = selectedReferenceIds.has(reference._id), displayTitle = reference.title || reference._id;
|
|
3298
|
+
return /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
|
|
3299
|
+
/* @__PURE__ */ jsx(
|
|
3300
|
+
Checkbox,
|
|
3301
|
+
{
|
|
3302
|
+
checked: isSelected,
|
|
3303
|
+
onChange: () => handleToggleReference(reference._id)
|
|
3304
|
+
}
|
|
3305
|
+
),
|
|
3306
|
+
/* @__PURE__ */ jsxs(Flex, { flex: 1, align: "center", gap: 2, children: [
|
|
3307
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: displayTitle }),
|
|
3308
|
+
/* @__PURE__ */ jsx(Badge, { tone: "primary", fontSize: 0, children: reference._type })
|
|
3309
|
+
] })
|
|
3310
|
+
] }) }, reference._id);
|
|
3311
|
+
}) }) })
|
|
3312
|
+
] }) });
|
|
3486
3313
|
}, ReferenceConfigPanel = () => {
|
|
3487
|
-
const {
|
|
3314
|
+
const {
|
|
3315
|
+
dispatch,
|
|
3316
|
+
document,
|
|
3317
|
+
shouldTranslateReferences,
|
|
3318
|
+
maxReferenceDepth,
|
|
3319
|
+
config,
|
|
3320
|
+
references,
|
|
3321
|
+
selectedReferenceIds
|
|
3322
|
+
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [isLoadingReferences, setIsLoadingReferences] = useState(!1), [showSelectionPanel, setShowSelectionPanel] = useState(!1), maxDepth = config.maxReferenceDepth || 3;
|
|
3488
3323
|
useEffect(() => {
|
|
3489
3324
|
(async () => {
|
|
3490
3325
|
if (setIsLoadingReferences(!0), !shouldTranslateReferences) {
|
|
@@ -3554,43 +3389,26 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3554
3389
|
)
|
|
3555
3390
|
] }) : null
|
|
3556
3391
|
] }),
|
|
3557
|
-
shouldTranslateReferences ? /* @__PURE__ */
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
tone: shouldOverwriteFreshTranslations ? "primary" : "neutral",
|
|
3576
|
-
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3577
|
-
/* @__PURE__ */ jsx(
|
|
3578
|
-
Checkbox,
|
|
3579
|
-
{
|
|
3580
|
-
checked: shouldOverwriteFreshTranslations,
|
|
3581
|
-
onChange: handleShouldOverwriteFreshTranslationsChange,
|
|
3582
|
-
label: shouldOverwriteFreshTranslations ? "Disable" : "Enable"
|
|
3583
|
-
}
|
|
3584
|
-
),
|
|
3585
|
-
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3586
|
-
/* @__PURE__ */ jsx(Text, { children: "Overwrite Fresh Translations" }),
|
|
3587
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "All existing translations will be overwritten even if they are fresh." })
|
|
3588
|
-
] })
|
|
3589
|
-
] })
|
|
3590
|
-
}
|
|
3591
|
-
) })
|
|
3392
|
+
shouldTranslateReferences ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3393
|
+
/* @__PURE__ */ jsxs(Flex, { padding: 2, justify: "space-between", align: "center", children: [
|
|
3394
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: isLoadingReferences ? "Loading references..." : references.length === 0 ? "No translatable references found." : (() => {
|
|
3395
|
+
const selectedCount = references.filter((ref) => selectedReferenceIds.has(ref._id)).length, totalCount = references.length;
|
|
3396
|
+
return selectedCount === totalCount ? `${totalCount} reference${totalCount === 1 ? "" : "s"} will be translated along with this document.` : `${selectedCount} of ${totalCount} reference${totalCount === 1 ? "" : "s"} will be translated along with this document.`;
|
|
3397
|
+
})() }),
|
|
3398
|
+
!isLoadingReferences && references.length > 0 && /* @__PURE__ */ jsx(
|
|
3399
|
+
Button,
|
|
3400
|
+
{
|
|
3401
|
+
mode: "ghost",
|
|
3402
|
+
size: 1,
|
|
3403
|
+
text: showSelectionPanel ? "Hide selection" : "Select specific references...",
|
|
3404
|
+
onClick: () => setShowSelectionPanel(!showSelectionPanel)
|
|
3405
|
+
}
|
|
3406
|
+
)
|
|
3407
|
+
] }),
|
|
3408
|
+
!isLoadingReferences && references.length > 0 && showSelectionPanel && /* @__PURE__ */ jsx(ReferenceSelectionPanel, {})
|
|
3409
|
+
] }) : null
|
|
3592
3410
|
] });
|
|
3593
|
-
}, TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
3411
|
+
}, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
3594
3412
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3595
3413
|
/* @__PURE__ */ jsx(Label, { children: "Languages" }),
|
|
3596
3414
|
/* @__PURE__ */ jsx(LanguageSelectionPanel, {})
|
|
@@ -3599,16 +3417,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3599
3417
|
/* @__PURE__ */ jsx(Label, { children: "Translation Method" }),
|
|
3600
3418
|
/* @__PURE__ */ jsx(TranslationModePanel, {})
|
|
3601
3419
|
] }),
|
|
3420
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3421
|
+
/* @__PURE__ */ jsx(Label, { children: "Content Release" }),
|
|
3422
|
+
/* @__PURE__ */ jsx(ReleaseSelectionPanel, {})
|
|
3423
|
+
] }),
|
|
3602
3424
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3603
3425
|
/* @__PURE__ */ jsx(Label, { children: "Settings" }),
|
|
3604
3426
|
/* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsx(WriteSettingsPanel, {}) })
|
|
3605
3427
|
] }),
|
|
3606
3428
|
/* @__PURE__ */ jsx(ActionButtonBar, { setIsDialogOpen })
|
|
3607
3429
|
] }), TERMINAL_STATUSES = ["completed", "skipped", "error"], TranslationStatusPanel = () => {
|
|
3608
|
-
const { overallStatus, translationJobs, translationMode } = useTranslationContext(), headline = useMemo(
|
|
3609
|
-
()
|
|
3610
|
-
|
|
3611
|
-
), jobs = useMemo(() => Array.from(translationJobs.values()), [translationJobs]), openaiStats = useMemo(() => {
|
|
3430
|
+
const { overallStatus, translationJobs, translationMode, dispatch, document } = useTranslationContext(), headline = useMemo(() => overallStatus === "completed" ? "Translation completed" : overallStatus === "error" ? "Translation failed" : "Translation in progress", [overallStatus]), jobs = useMemo(() => Array.from(translationJobs.values()), [translationJobs]), isTerminalState = overallStatus === "completed" || overallStatus === "error", handleReset = useCallback(() => {
|
|
3431
|
+
forceCleanupActiveTranslation(document._id), dispatch({ type: "RESET_TRANSLATION_STATE" });
|
|
3432
|
+
}, [dispatch, document._id]), openaiStats = useMemo(() => {
|
|
3612
3433
|
if (translationMode === "sanity-only") return null;
|
|
3613
3434
|
const completedJobs = jobs.filter((j) => j.status === "completed"), totalTokens = completedJobs.reduce((sum, job) => sum + (job.tokensUsed || 0), 0), totalCost = completedJobs.reduce((sum, job) => sum + (job.estimatedCost || 0), 0);
|
|
3614
3435
|
return { totalTokens, totalCost, jobsWithUsage: completedJobs.filter((j) => j.tokensUsed).length };
|
|
@@ -3642,17 +3463,12 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3642
3463
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3643
3464
|
"Error: ",
|
|
3644
3465
|
job.errorMessage
|
|
3645
|
-
] }) }) : null,
|
|
3646
|
-
job.status === "retrying" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3647
|
-
"Retrying after: ",
|
|
3648
|
-
job.errorMessage
|
|
3649
3466
|
] }) }) : null
|
|
3650
3467
|
] })
|
|
3651
3468
|
] }),
|
|
3652
3469
|
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
3653
3470
|
job.status === "skipped" ? /* @__PURE__ */ jsx(SkippedBadge, {}) : null,
|
|
3654
|
-
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {
|
|
3655
|
-
job.status === "retrying" ? /* @__PURE__ */ jsx(RetryingBadge, { retryCount: job.retryCount }) : null,
|
|
3471
|
+
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {}) : null,
|
|
3656
3472
|
job.targetId ? /* @__PURE__ */ jsx(
|
|
3657
3473
|
TranslationOpenerButton,
|
|
3658
3474
|
{
|
|
@@ -3669,18 +3485,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3669
3485
|
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Usage Summary" }),
|
|
3670
3486
|
/* @__PURE__ */ jsxs(Inline, { space: 4, children: [
|
|
3671
3487
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3672
|
-
"
|
|
3488
|
+
"Total Tokens: ",
|
|
3673
3489
|
/* @__PURE__ */ jsx("strong", { children: openaiStats.totalTokens.toLocaleString() })
|
|
3674
3490
|
] }),
|
|
3675
3491
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3676
|
-
"
|
|
3492
|
+
"Total Cost: ",
|
|
3677
3493
|
/* @__PURE__ */ jsxs("strong", { children: [
|
|
3678
3494
|
"$",
|
|
3679
3495
|
openaiStats.totalCost.toFixed(4)
|
|
3680
3496
|
] })
|
|
3681
3497
|
] })
|
|
3682
3498
|
] })
|
|
3683
|
-
] }) })
|
|
3499
|
+
] }) }),
|
|
3500
|
+
isTerminalState && /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "primary", text: "Translate More", icon: ResetIcon, onClick: handleReset }) })
|
|
3684
3501
|
] });
|
|
3685
3502
|
}, ProgressBar = ({ progress, max = 100 }) => {
|
|
3686
3503
|
const percentage = Math.min(progress / max * 100, 100);
|
|
@@ -3732,35 +3549,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3732
3549
|
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." }) }),
|
|
3733
3550
|
children: /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: "Skipped" })
|
|
3734
3551
|
}
|
|
3735
|
-
), ErrorBadge = (
|
|
3736
|
-
Tooltip,
|
|
3737
|
-
{
|
|
3738
|
-
portal: !0,
|
|
3739
|
-
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." }) }),
|
|
3740
|
-
children: /* @__PURE__ */ jsxs(Badge, { tone: "critical", children: [
|
|
3741
|
-
"Failed",
|
|
3742
|
-
retryCount && retryCount > 0 ? ` (${retryCount} retries)` : ""
|
|
3743
|
-
] })
|
|
3744
|
-
}
|
|
3745
|
-
), RetryingBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
|
|
3552
|
+
), ErrorBadge = () => /* @__PURE__ */ jsx(
|
|
3746
3553
|
Tooltip,
|
|
3747
3554
|
{
|
|
3748
3555
|
portal: !0,
|
|
3749
|
-
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "
|
|
3750
|
-
children: /* @__PURE__ */
|
|
3751
|
-
"Retry ",
|
|
3752
|
-
retryCount || 1
|
|
3753
|
-
] })
|
|
3556
|
+
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "This translation failed. Check the error message for details." }) }),
|
|
3557
|
+
children: /* @__PURE__ */ jsx(Badge, { tone: "critical", children: "Failed" })
|
|
3754
3558
|
}
|
|
3755
3559
|
), getStatusLabel = (status) => ({
|
|
3756
3560
|
idle: "Idle",
|
|
3757
3561
|
queued: "Queued...",
|
|
3758
3562
|
preparing_translation: "Preparing translation...",
|
|
3759
3563
|
translating: "Translating with Sanity Agent...",
|
|
3760
|
-
translating_with_openai: "Translating with
|
|
3761
|
-
reviewing_with_openai: "Reviewing translation with GPT-5...",
|
|
3564
|
+
translating_with_openai: "Translating with OpenAI...",
|
|
3762
3565
|
updating_metadata: "Saving metadata...",
|
|
3763
|
-
retrying: "Retrying...",
|
|
3764
3566
|
error: "Error",
|
|
3765
3567
|
skipped: "Skipped",
|
|
3766
3568
|
completed: "Completed"
|
|
@@ -3768,14 +3570,30 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3768
3570
|
"translating",
|
|
3769
3571
|
"linking_references",
|
|
3770
3572
|
"completed"
|
|
3771
|
-
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
|
|
3573
|
+
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen, onStatusChange }) => {
|
|
3772
3574
|
const { overallStatus, document } = useTranslationContext();
|
|
3773
|
-
return
|
|
3575
|
+
return useEffect(() => {
|
|
3576
|
+
onStatusChange?.(overallStatus);
|
|
3577
|
+
}, [overallStatus, onStatusChange]), STATES_IN_WHICH_TO_SHOW_CONFIG.includes(overallStatus) ? /* @__PURE__ */ jsx(TranslationConfigView, { setIsDialogOpen }) : STATES_IN_WHICH_TO_SHOW_TRANSLATION_PROGRESS.includes(overallStatus) ? /* @__PURE__ */ jsx(TranslationStatusPanel, {}) : STATES_IN_WHICH_TO_SHOW_ERROR.includes(overallStatus) ? /* @__PURE__ */ jsx("div", { children: "Error occurred" }) : (devLog.warn("Unhandled translation dialog state", {
|
|
3774
3578
|
documentId: document._id,
|
|
3775
3579
|
overallStatus
|
|
3776
3580
|
}), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
|
|
3777
|
-
}, getTranslateAction = (config) => ({ draft, published }) => {
|
|
3778
|
-
const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1)
|
|
3581
|
+
}, IN_PROGRESS_STATUSES = ["translating", "linking_references"], getButtonLabel = (status) => status === "completed" ? "Translation Complete" : status && IN_PROGRESS_STATUSES.includes(status) ? "Translating..." : "Translate", getTranslateAction = (config) => ({ draft, published }) => {
|
|
3582
|
+
const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1), [currentStatus, setCurrentStatus] = useState(null), isTranslating = currentStatus !== null && IN_PROGRESS_STATUSES.includes(currentStatus), handleStatusChange = useCallback((status) => {
|
|
3583
|
+
setCurrentStatus(status);
|
|
3584
|
+
}, []), handleClose = useCallback(() => {
|
|
3585
|
+
if (isTranslating) {
|
|
3586
|
+
if (!window.confirm(
|
|
3587
|
+
"Translation is in progress. Closing this dialog will not stop the translation, but refreshing the page will cancel the translation. You can reopen the Translate dialog to see updated progress."
|
|
3588
|
+
))
|
|
3589
|
+
return;
|
|
3590
|
+
devLog.warn("User closed dialog during active translation", {
|
|
3591
|
+
documentId: document?._id,
|
|
3592
|
+
component: "TranslateAction"
|
|
3593
|
+
});
|
|
3594
|
+
}
|
|
3595
|
+
setIsDialogOpen(!1);
|
|
3596
|
+
}, [isTranslating, document?._id]);
|
|
3779
3597
|
return devLog.debug("TranslateAction initialized", {
|
|
3780
3598
|
documentId: document?._id,
|
|
3781
3599
|
documentType: document?._type
|
|
@@ -3783,7 +3601,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3783
3601
|
documentType: document?._type,
|
|
3784
3602
|
translatableTypes: config.translatableSchemaTypes
|
|
3785
3603
|
}), null) : config ? {
|
|
3786
|
-
label:
|
|
3604
|
+
label: getButtonLabel(currentStatus),
|
|
3787
3605
|
icon: TranslateIcon,
|
|
3788
3606
|
onHandle: () => {
|
|
3789
3607
|
devLog.info("Translation dialog opened", {
|
|
@@ -3794,9 +3612,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3794
3612
|
},
|
|
3795
3613
|
dialog: isDialogOpen && {
|
|
3796
3614
|
type: "dialog",
|
|
3797
|
-
onClose:
|
|
3615
|
+
onClose: handleClose,
|
|
3798
3616
|
header: "Translate Document",
|
|
3799
|
-
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
|
|
3617
|
+
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen, onStatusChange: handleStatusChange }) })
|
|
3800
3618
|
}
|
|
3801
3619
|
} : (devLog.error("Missing config in TranslateAction"), null);
|
|
3802
3620
|
}, translationGlossaryTerm = defineType({
|
|
@@ -3912,7 +3730,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3912
3730
|
defineField({
|
|
3913
3731
|
name: "changeHistory",
|
|
3914
3732
|
title: "Change History",
|
|
3915
|
-
description: "History of changes made
|
|
3733
|
+
description: "History of changes made to translation settings.",
|
|
3916
3734
|
type: "array",
|
|
3917
3735
|
readOnly: !0,
|
|
3918
3736
|
group: "history",
|
|
@@ -4164,10 +3982,14 @@ function serializeVTTCaptionText(captions) {
|
|
|
4164
3982
|
firstCaption: captions.length > 0 ? captions[0] : null
|
|
4165
3983
|
}), !Array.isArray(captions) || captions.length === 0)
|
|
4166
3984
|
return "";
|
|
4167
|
-
const result = captions.map((caption, index) =>
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
3985
|
+
const result = captions.map((caption, index) => {
|
|
3986
|
+
devLog.debug(`Processing caption ${index}:`, {
|
|
3987
|
+
text: caption.text,
|
|
3988
|
+
hasText: typeof caption.text == "string"
|
|
3989
|
+
});
|
|
3990
|
+
const escapedText = (caption.text || "").replace(/\n/g, "\\n");
|
|
3991
|
+
return `[${index}] ${escapedText}`;
|
|
3992
|
+
}).join(`
|
|
4171
3993
|
`);
|
|
4172
3994
|
return devLog.debug("serializeVTTCaptionText - output:", {
|
|
4173
3995
|
resultLength: result.length,
|
|
@@ -4184,29 +4006,46 @@ function deserializeVTTCaptionText(translationText, originalMetadata) {
|
|
|
4184
4006
|
return [];
|
|
4185
4007
|
const result = [], lines = translationText.split(`
|
|
4186
4008
|
`), lineRegex = /^\[(\d+)\]\s*(.*)$/;
|
|
4009
|
+
let currentIndex = null, currentTextParts = [];
|
|
4187
4010
|
for (const line of lines) {
|
|
4188
4011
|
const match = line.match(lineRegex);
|
|
4189
|
-
if (
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4012
|
+
if (match) {
|
|
4013
|
+
if (currentIndex !== null) {
|
|
4014
|
+
const text = currentTextParts.join(`
|
|
4015
|
+
`).replace(/\\n/g, `
|
|
4016
|
+
`), metadata = originalMetadata.find((m) => m._originalIndex === currentIndex);
|
|
4017
|
+
if (metadata) {
|
|
4018
|
+
const { _originalIndex, ...metadataWithoutIndex } = metadata;
|
|
4019
|
+
result.push({
|
|
4020
|
+
...metadataWithoutIndex,
|
|
4021
|
+
text
|
|
4022
|
+
});
|
|
4023
|
+
} else
|
|
4024
|
+
devLog.warn("Missing metadata for caption", { index: currentIndex }), result.push({
|
|
4025
|
+
_type: "vtt.cue",
|
|
4026
|
+
_key: `caption-${currentIndex}`,
|
|
4027
|
+
text,
|
|
4028
|
+
startTime: 0,
|
|
4029
|
+
endTime: 0
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
currentIndex = Number.parseInt(match[1], 10), currentTextParts = [match[2]];
|
|
4033
|
+
} else currentIndex !== null ? currentTextParts.push(line) : devLog.warn("Failed to parse caption line", { line });
|
|
4034
|
+
}
|
|
4035
|
+
if (currentIndex !== null) {
|
|
4036
|
+
const text = currentTextParts.join(`
|
|
4037
|
+
`).replace(/\\n/g, `
|
|
4038
|
+
`), metadata = originalMetadata.find((m) => m._originalIndex === currentIndex);
|
|
4199
4039
|
if (metadata) {
|
|
4200
|
-
devLog.debug(`Found matching metadata for index ${index}`);
|
|
4201
4040
|
const { _originalIndex, ...metadataWithoutIndex } = metadata;
|
|
4202
4041
|
result.push({
|
|
4203
4042
|
...metadataWithoutIndex,
|
|
4204
4043
|
text
|
|
4205
4044
|
});
|
|
4206
4045
|
} else
|
|
4207
|
-
devLog.warn("Missing metadata for caption", { index }), result.push({
|
|
4046
|
+
devLog.warn("Missing metadata for caption", { index: currentIndex }), result.push({
|
|
4208
4047
|
_type: "vtt.cue",
|
|
4209
|
-
_key: `caption-${
|
|
4048
|
+
_key: `caption-${currentIndex}`,
|
|
4210
4049
|
text,
|
|
4211
4050
|
startTime: 0,
|
|
4212
4051
|
endTime: 0
|
|
@@ -4217,12 +4056,299 @@ function deserializeVTTCaptionText(translationText, originalMetadata) {
|
|
|
4217
4056
|
resultExample: result.length > 0 ? result[0] : null
|
|
4218
4057
|
}), result;
|
|
4219
4058
|
}
|
|
4059
|
+
const LOCALE_TO_ISO = {
|
|
4060
|
+
// English variants
|
|
4061
|
+
"en-us": "eng",
|
|
4062
|
+
"en-US": "eng",
|
|
4063
|
+
"en-gb": "eng",
|
|
4064
|
+
"en-GB": "eng",
|
|
4065
|
+
"en-ca": "eng",
|
|
4066
|
+
"en-CA": "eng",
|
|
4067
|
+
"en-au": "eng",
|
|
4068
|
+
"en-AU": "eng",
|
|
4069
|
+
// Spanish variants
|
|
4070
|
+
es: "spa",
|
|
4071
|
+
"es-es": "spa",
|
|
4072
|
+
"es-ES": "spa",
|
|
4073
|
+
"es-mx": "spa",
|
|
4074
|
+
"es-MX": "spa",
|
|
4075
|
+
// French variants
|
|
4076
|
+
fr: "fra",
|
|
4077
|
+
"fr-fr": "fra",
|
|
4078
|
+
"fr-FR": "fra",
|
|
4079
|
+
"fr-ca": "fra",
|
|
4080
|
+
"fr-CA": "fra",
|
|
4081
|
+
// German variants
|
|
4082
|
+
de: "ger",
|
|
4083
|
+
"de-de": "ger",
|
|
4084
|
+
"de-DE": "ger",
|
|
4085
|
+
"de-at": "ger",
|
|
4086
|
+
"de-AT": "ger",
|
|
4087
|
+
// Italian variants
|
|
4088
|
+
it: "ita",
|
|
4089
|
+
"it-it": "ita",
|
|
4090
|
+
"it-IT": "ita",
|
|
4091
|
+
// Portuguese variants
|
|
4092
|
+
pt: "por",
|
|
4093
|
+
"pt-br": "por",
|
|
4094
|
+
"pt-BR": "por",
|
|
4095
|
+
"pt-pt": "por",
|
|
4096
|
+
"pt-PT": "por",
|
|
4097
|
+
// Russian variants
|
|
4098
|
+
ru: "rus",
|
|
4099
|
+
"ru-ru": "rus",
|
|
4100
|
+
"ru-RU": "rus",
|
|
4101
|
+
// Japanese variants
|
|
4102
|
+
ja: "jpn",
|
|
4103
|
+
"ja-jp": "jpn",
|
|
4104
|
+
"ja-JP": "jpn",
|
|
4105
|
+
// Korean variants
|
|
4106
|
+
ko: "kor",
|
|
4107
|
+
"ko-kr": "kor",
|
|
4108
|
+
"ko-KR": "kor",
|
|
4109
|
+
// Chinese variants
|
|
4110
|
+
zh: "chi",
|
|
4111
|
+
"zh-cn": "chi",
|
|
4112
|
+
"zh-CN": "chi",
|
|
4113
|
+
"zh-tw": "chi",
|
|
4114
|
+
"zh-TW": "chi",
|
|
4115
|
+
"zh-hk": "chi",
|
|
4116
|
+
"zh-HK": "chi"
|
|
4117
|
+
}, normalizeToIsoLanguageCode = (code) => {
|
|
4118
|
+
const normalizedCode = code.toLowerCase();
|
|
4119
|
+
return LOCALE_TO_ISO[normalizedCode];
|
|
4120
|
+
}, wistiaCaptionTransform = {
|
|
4121
|
+
matcher: {
|
|
4122
|
+
fieldName: "wistiaCaptions"
|
|
4123
|
+
},
|
|
4124
|
+
toTranslatable: (wistiaCaptions, context) => {
|
|
4125
|
+
if (devLog.info("Wistia captions toTranslatable - input:", {
|
|
4126
|
+
fieldPath: context.fieldPath,
|
|
4127
|
+
hasCaptionContent: !!wistiaCaptions?.captionContent,
|
|
4128
|
+
captionContentLength: wistiaCaptions?.captionContent?.length ?? 0,
|
|
4129
|
+
isoLanguage: wistiaCaptions?.isoLanguage
|
|
4130
|
+
}), !wistiaCaptions || !wistiaCaptions.captionContent)
|
|
4131
|
+
return devLog.debug("Wistia caption field is missing or has no content, returning empty transform", {
|
|
4132
|
+
fieldPath: context.fieldPath,
|
|
4133
|
+
value: wistiaCaptions
|
|
4134
|
+
}), {
|
|
4135
|
+
_type: "translatable.wistia.captions",
|
|
4136
|
+
translationText: "",
|
|
4137
|
+
_originalMetadata: {
|
|
4138
|
+
captionFileName: wistiaCaptions?.captionFileName,
|
|
4139
|
+
isoLanguage: wistiaCaptions?.isoLanguage,
|
|
4140
|
+
captionUploadStatus: wistiaCaptions?.captionUploadStatus,
|
|
4141
|
+
captionUploadMessage: wistiaCaptions?.captionUploadMessage,
|
|
4142
|
+
_originalIndex: 0
|
|
4143
|
+
},
|
|
4144
|
+
_srtCues: []
|
|
4145
|
+
};
|
|
4146
|
+
devLog.info("Applying Wistia caption transform", {
|
|
4147
|
+
fieldPath: context.fieldPath,
|
|
4148
|
+
captionContentPreview: wistiaCaptions.captionContent.substring(0, 200)
|
|
4149
|
+
});
|
|
4150
|
+
const srtCues = parseSRTContent(wistiaCaptions.captionContent);
|
|
4151
|
+
devLog.debug("Parsed SRT cues:", {
|
|
4152
|
+
fieldPath: context.fieldPath,
|
|
4153
|
+
cueCount: srtCues.length,
|
|
4154
|
+
firstCue: srtCues.length > 0 ? srtCues[0] : null
|
|
4155
|
+
});
|
|
4156
|
+
const translationText = serializeWistiaCaptionText(srtCues), originalMetadata = {
|
|
4157
|
+
captionFileName: wistiaCaptions.captionFileName,
|
|
4158
|
+
isoLanguage: wistiaCaptions.isoLanguage,
|
|
4159
|
+
captionUploadStatus: wistiaCaptions.captionUploadStatus,
|
|
4160
|
+
captionUploadMessage: wistiaCaptions.captionUploadMessage,
|
|
4161
|
+
_originalIndex: 0
|
|
4162
|
+
}, srtCuesMetadata = srtCues.map((cue) => ({
|
|
4163
|
+
index: cue.index,
|
|
4164
|
+
startTime: cue.startTime,
|
|
4165
|
+
endTime: cue.endTime
|
|
4166
|
+
}));
|
|
4167
|
+
return devLog.info("Wistia captions serialized:", {
|
|
4168
|
+
fieldPath: context.fieldPath,
|
|
4169
|
+
translationTextLength: translationText.length,
|
|
4170
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
|
|
4171
|
+
cueCount: srtCues.length
|
|
4172
|
+
}), {
|
|
4173
|
+
_type: "translatable.wistia.captions",
|
|
4174
|
+
translationText,
|
|
4175
|
+
_originalMetadata: originalMetadata,
|
|
4176
|
+
_srtCues: srtCuesMetadata
|
|
4177
|
+
};
|
|
4178
|
+
},
|
|
4179
|
+
fromTranslatable: (translatedValue, originalValue, context) => {
|
|
4180
|
+
if (devLog.info("Wistia captions fromTranslatable - input:", {
|
|
4181
|
+
fieldPath: context.fieldPath,
|
|
4182
|
+
valueType: typeof translatedValue,
|
|
4183
|
+
isCorrectType: translatedValue._type === "translatable.wistia.captions",
|
|
4184
|
+
hasText: !!translatedValue.translationText,
|
|
4185
|
+
hasMetadata: !!translatedValue._originalMetadata,
|
|
4186
|
+
hasCues: Array.isArray(translatedValue._srtCues)
|
|
4187
|
+
}), !translatedValue || translatedValue._type !== "translatable.wistia.captions" || !translatedValue._originalMetadata || !Array.isArray(translatedValue._srtCues))
|
|
4188
|
+
return devLog.debug("Value is not a translatable Wistia caption object, returning original", {
|
|
4189
|
+
fieldPath: context.fieldPath,
|
|
4190
|
+
valueType: typeof translatedValue,
|
|
4191
|
+
translatedValue
|
|
4192
|
+
}), originalValue || {};
|
|
4193
|
+
devLog.info("Processing translated Wistia captions", {
|
|
4194
|
+
fieldPath: context.fieldPath,
|
|
4195
|
+
cueCount: translatedValue._srtCues.length
|
|
4196
|
+
});
|
|
4197
|
+
const { translationText, _originalMetadata, _srtCues } = translatedValue;
|
|
4198
|
+
devLog.info("Wistia captions translationText:", {
|
|
4199
|
+
fieldPath: context.fieldPath,
|
|
4200
|
+
translationTextLength: translationText.length,
|
|
4201
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
|
|
4202
|
+
});
|
|
4203
|
+
const reconstructedCaptionContent = deserializeWistiaCaptionText(translationText, _srtCues), documentLanguage = getDocumentLanguage(context.document);
|
|
4204
|
+
let updatedIsoLanguage = _originalMetadata.isoLanguage;
|
|
4205
|
+
if (documentLanguage) {
|
|
4206
|
+
const isoCode = normalizeToIsoLanguageCode(documentLanguage);
|
|
4207
|
+
isoCode ? (updatedIsoLanguage = isoCode, devLog.debug("Updated isoLanguage based on document language", {
|
|
4208
|
+
fieldPath: context.fieldPath,
|
|
4209
|
+
documentLanguage,
|
|
4210
|
+
isoCode
|
|
4211
|
+
})) : devLog.debug("Could not convert document language to ISO code, preserving original", {
|
|
4212
|
+
fieldPath: context.fieldPath,
|
|
4213
|
+
documentLanguage,
|
|
4214
|
+
originalIsoLanguage: _originalMetadata.isoLanguage
|
|
4215
|
+
});
|
|
4216
|
+
} else
|
|
4217
|
+
devLog.debug("Document language field not found, preserving original isoLanguage", {
|
|
4218
|
+
fieldPath: context.fieldPath,
|
|
4219
|
+
originalIsoLanguage: _originalMetadata.isoLanguage
|
|
4220
|
+
});
|
|
4221
|
+
const result = {
|
|
4222
|
+
captionContent: reconstructedCaptionContent,
|
|
4223
|
+
captionFileName: _originalMetadata.captionFileName,
|
|
4224
|
+
isoLanguage: updatedIsoLanguage,
|
|
4225
|
+
captionUploadStatus: _originalMetadata.captionUploadStatus,
|
|
4226
|
+
captionUploadMessage: _originalMetadata.captionUploadMessage
|
|
4227
|
+
};
|
|
4228
|
+
return devLog.info("Wistia captions final result:", {
|
|
4229
|
+
fieldPath: context.fieldPath,
|
|
4230
|
+
hasCaptionContent: !!result.captionContent,
|
|
4231
|
+
captionContentLength: result.captionContent?.length ?? 0,
|
|
4232
|
+
isoLanguage: result.isoLanguage
|
|
4233
|
+
}), result;
|
|
4234
|
+
}
|
|
4235
|
+
};
|
|
4236
|
+
function parseSRTContent(content) {
|
|
4237
|
+
if (devLog.debug("parseSRTContent - input:", {
|
|
4238
|
+
contentLength: content.length,
|
|
4239
|
+
contentPreview: content.substring(0, 100) + (content.length > 100 ? "..." : "")
|
|
4240
|
+
}), !content || content.trim() === "")
|
|
4241
|
+
return [];
|
|
4242
|
+
let normalizedContent = content;
|
|
4243
|
+
content.includes("\\n") && !content.includes(`
|
|
4244
|
+
`) && (normalizedContent = content.replace(/\\n/g, `
|
|
4245
|
+
`), devLog.debug("Unescaped newlines in SRT content", {
|
|
4246
|
+
originalLength: content.length,
|
|
4247
|
+
normalizedLength: normalizedContent.length
|
|
4248
|
+
}));
|
|
4249
|
+
const cues = [], cueBlocks = normalizedContent.split(/\n\s*\n/);
|
|
4250
|
+
for (const block of cueBlocks) {
|
|
4251
|
+
const lines = block.trim().split(`
|
|
4252
|
+
`);
|
|
4253
|
+
if (lines.length < 3)
|
|
4254
|
+
continue;
|
|
4255
|
+
const cueNumber = Number.parseInt(lines[0].trim(), 10);
|
|
4256
|
+
if (Number.isNaN(cueNumber)) {
|
|
4257
|
+
devLog.warn("Invalid SRT cue number", { line: lines[0] });
|
|
4258
|
+
continue;
|
|
4259
|
+
}
|
|
4260
|
+
const timestampLine = lines[1].trim(), timestampMatch = timestampLine.match(/^(\d{2}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}[,.]\d{3})$/);
|
|
4261
|
+
if (!timestampMatch) {
|
|
4262
|
+
devLog.warn("Invalid SRT timestamp format", { line: timestampLine });
|
|
4263
|
+
continue;
|
|
4264
|
+
}
|
|
4265
|
+
const startTime = timestampMatch[1].replace(".", ","), endTime = timestampMatch[2].replace(".", ","), text = lines.slice(2).join(`
|
|
4266
|
+
`).trim();
|
|
4267
|
+
text && cues.push({
|
|
4268
|
+
index: cueNumber,
|
|
4269
|
+
startTime,
|
|
4270
|
+
endTime,
|
|
4271
|
+
text
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
return devLog.debug("parseSRTContent - output:", {
|
|
4275
|
+
cueCount: cues.length,
|
|
4276
|
+
firstCue: cues.length > 0 ? cues[0] : null
|
|
4277
|
+
}), cues;
|
|
4278
|
+
}
|
|
4279
|
+
function serializeWistiaCaptionText(cues) {
|
|
4280
|
+
if (devLog.debug("serializeWistiaCaptionText - input:", {
|
|
4281
|
+
cuesCount: cues.length,
|
|
4282
|
+
firstCue: cues.length > 0 ? cues[0] : null
|
|
4283
|
+
}), !Array.isArray(cues) || cues.length === 0)
|
|
4284
|
+
return "";
|
|
4285
|
+
const result = cues.map((cue) => {
|
|
4286
|
+
devLog.debug(`Processing cue ${cue.index}:`, {
|
|
4287
|
+
text: cue.text,
|
|
4288
|
+
hasText: typeof cue.text == "string"
|
|
4289
|
+
});
|
|
4290
|
+
const escapedText = cue.text.replace(/\n/g, "\\n");
|
|
4291
|
+
return `[${cue.index}] ${escapedText}`;
|
|
4292
|
+
}).join(`
|
|
4293
|
+
`);
|
|
4294
|
+
return devLog.debug("serializeWistiaCaptionText - output:", {
|
|
4295
|
+
resultLength: result.length,
|
|
4296
|
+
resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
|
|
4297
|
+
}), result;
|
|
4298
|
+
}
|
|
4299
|
+
function deserializeWistiaCaptionText(translationText, srtCuesMetadata) {
|
|
4300
|
+
if (devLog.debug("deserializeWistiaCaptionText - input:", {
|
|
4301
|
+
translationTextLength: translationText.length,
|
|
4302
|
+
translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
|
|
4303
|
+
metadataCount: srtCuesMetadata.length,
|
|
4304
|
+
metadataExample: srtCuesMetadata.length > 0 ? srtCuesMetadata[0] : null
|
|
4305
|
+
}), !translationText || translationText.trim() === "")
|
|
4306
|
+
return "";
|
|
4307
|
+
const result = [], lines = translationText.split(`
|
|
4308
|
+
`), lineRegex = /^\[(\d+)\]\s*(.*)$/, metadataByIndex = /* @__PURE__ */ new Map();
|
|
4309
|
+
for (const metadata of srtCuesMetadata)
|
|
4310
|
+
metadataByIndex.set(metadata.index, { startTime: metadata.startTime, endTime: metadata.endTime });
|
|
4311
|
+
let currentIndex = null, currentTextParts = [];
|
|
4312
|
+
for (const line of lines) {
|
|
4313
|
+
const match = line.match(lineRegex);
|
|
4314
|
+
if (match) {
|
|
4315
|
+
if (currentIndex !== null) {
|
|
4316
|
+
const text = currentTextParts.join(`
|
|
4317
|
+
`).replace(/\\n/g, `
|
|
4318
|
+
`), metadata = metadataByIndex.get(currentIndex);
|
|
4319
|
+
metadata ? (result.push(`${currentIndex}`), result.push(`${metadata.startTime} --> ${metadata.endTime}`), result.push(text), result.push("")) : devLog.warn("Missing metadata for caption", { index: currentIndex });
|
|
4320
|
+
}
|
|
4321
|
+
currentIndex = Number.parseInt(match[1], 10), currentTextParts = [match[2]];
|
|
4322
|
+
} else currentIndex !== null ? currentTextParts.push(line) : devLog.warn("Failed to parse caption line", { line });
|
|
4323
|
+
}
|
|
4324
|
+
if (currentIndex !== null) {
|
|
4325
|
+
const text = currentTextParts.join(`
|
|
4326
|
+
`).replace(/\\n/g, `
|
|
4327
|
+
`), metadata = metadataByIndex.get(currentIndex);
|
|
4328
|
+
metadata ? (result.push(`${currentIndex}`), result.push(`${metadata.startTime} --> ${metadata.endTime}`), result.push(text), result.push("")) : devLog.warn("Missing metadata for caption", { index: currentIndex });
|
|
4329
|
+
}
|
|
4330
|
+
result.length > 0 && result[result.length - 1] === "" && result.pop();
|
|
4331
|
+
const reconstructed = result.join(`
|
|
4332
|
+
`);
|
|
4333
|
+
return devLog.debug("deserializeWistiaCaptionText - output:", {
|
|
4334
|
+
resultLength: reconstructed.length,
|
|
4335
|
+
resultPreview: reconstructed.substring(0, 200) + (reconstructed.length > 200 ? "..." : "")
|
|
4336
|
+
}), reconstructed;
|
|
4337
|
+
}
|
|
4338
|
+
function getDocumentLanguage(document) {
|
|
4339
|
+
if (!document || typeof document != "object")
|
|
4340
|
+
return;
|
|
4341
|
+
const languageFieldNames = ["language", "lang", "locale"];
|
|
4342
|
+
for (const fieldName of languageFieldNames)
|
|
4343
|
+
if (fieldName in document && typeof document[fieldName] == "string")
|
|
4344
|
+
return document[fieldName];
|
|
4345
|
+
}
|
|
4220
4346
|
const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
|
|
4221
4347
|
globalTransformRegistry.register(transform);
|
|
4222
4348
|
})), {
|
|
4223
4349
|
name: "sanity-plugin-ai-translation",
|
|
4224
4350
|
document: {
|
|
4225
|
-
actions: [getTranslateAction(config)
|
|
4351
|
+
actions: [getTranslateAction(config)]
|
|
4226
4352
|
},
|
|
4227
4353
|
schema: {
|
|
4228
4354
|
types: [translationSettings, translationGlossaryTerm]
|
|
@@ -4236,6 +4362,7 @@ export {
|
|
|
4236
4362
|
restoreExcludedFields,
|
|
4237
4363
|
richTextTransform,
|
|
4238
4364
|
translationHistorySchema,
|
|
4239
|
-
vttCaptionTransform
|
|
4365
|
+
vttCaptionTransform,
|
|
4366
|
+
wistiaCaptionTransform
|
|
4240
4367
|
};
|
|
4241
4368
|
//# sourceMappingURL=index.js.map
|