@braze/sanity-plugin-ai-translation 0.2.37 → 0.2.51
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 +16 -54
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1099 -1512
- 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, 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,63 @@ 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
|
+
}, 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 = `${job._id}_${job.language}`, existingJob = entry.state.translationJobs.get(jobKey), mergedJob = existingJob ? { ...existingJob, ...job } : job;
|
|
84
|
+
entry.state.translationJobs.set(jobKey, mergedJob), notifySubscribers(entry);
|
|
85
|
+
}
|
|
86
|
+
}, getActiveTranslation = (documentId) => activeTranslations.get(documentId)?.state ?? null, subscribeToActiveTranslation = (documentId, callback) => {
|
|
87
|
+
const entry = activeTranslations.get(documentId);
|
|
88
|
+
return entry ? (entry.subscribers.add(callback), callback(entry.state), () => {
|
|
89
|
+
entry.subscribers.delete(callback);
|
|
90
|
+
}) : () => {
|
|
91
|
+
};
|
|
92
|
+
}, cleanupActiveTranslation = (documentId, delayMs = 6e4) => {
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
const entry = activeTranslations.get(documentId);
|
|
95
|
+
entry && ["completed", "error"].includes(entry.state.overallStatus) && activeTranslations.delete(documentId);
|
|
96
|
+
}, delayMs);
|
|
97
|
+
}, notifySubscribers = (entry) => {
|
|
98
|
+
for (const subscriber of entry.subscribers)
|
|
99
|
+
try {
|
|
100
|
+
subscriber(entry.state);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Error in active translation subscriber:", error);
|
|
103
|
+
}
|
|
104
|
+
}, forceCleanupActiveTranslation = (documentId) => {
|
|
105
|
+
activeTranslations.delete(documentId);
|
|
65
106
|
}, TranslationContext = createContext({
|
|
66
107
|
document: null,
|
|
67
108
|
config: null,
|
|
68
109
|
overallStatus: "idle",
|
|
69
110
|
selectedTargetLanguages: [],
|
|
70
111
|
translationJobs: /* @__PURE__ */ new Map(),
|
|
71
|
-
shouldOverwriteFreshTranslations: !1,
|
|
72
112
|
shouldTranslateReferences: !1,
|
|
73
113
|
maxReferenceDepth: 1,
|
|
74
114
|
references: [],
|
|
75
115
|
translationMetadata: null,
|
|
76
116
|
translationMode: "sanity-only",
|
|
77
117
|
openaiValidated: !1,
|
|
118
|
+
releaseConfig: {
|
|
119
|
+
mode: "new",
|
|
120
|
+
newReleaseTitle: ""
|
|
121
|
+
},
|
|
78
122
|
dispatch: () => {
|
|
79
123
|
}
|
|
80
124
|
}), translationReducer = (context, action) => {
|
|
@@ -85,7 +129,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
85
129
|
};
|
|
86
130
|
switch (devLog.debug("Translation action dispatched", {
|
|
87
131
|
...logContext,
|
|
88
|
-
payload: action.payload
|
|
132
|
+
payload: "payload" in action ? action.payload : void 0
|
|
89
133
|
}), action.type) {
|
|
90
134
|
case "SET_OVERALL_STATUS":
|
|
91
135
|
return { ...context, overallStatus: action.payload };
|
|
@@ -116,11 +160,6 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
116
160
|
...context,
|
|
117
161
|
maxReferenceDepth: action.payload
|
|
118
162
|
};
|
|
119
|
-
case "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS":
|
|
120
|
-
return {
|
|
121
|
-
...context,
|
|
122
|
-
shouldOverwriteFreshTranslations: action.payload
|
|
123
|
-
};
|
|
124
163
|
case "SET_REFERENCES":
|
|
125
164
|
return {
|
|
126
165
|
...context,
|
|
@@ -133,31 +172,82 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
133
172
|
translationJobs: newTranslationJobs
|
|
134
173
|
};
|
|
135
174
|
}
|
|
175
|
+
case "SET_RELEASE_CONFIG":
|
|
176
|
+
return {
|
|
177
|
+
...context,
|
|
178
|
+
releaseConfig: {
|
|
179
|
+
...context.releaseConfig,
|
|
180
|
+
...action.payload
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
case "RESET_TRANSLATION_STATE":
|
|
184
|
+
return devLog.info("Resetting translation state for new translation", {
|
|
185
|
+
...logContext,
|
|
186
|
+
previousStatus: context.overallStatus,
|
|
187
|
+
previousJobCount: context.translationJobs.size
|
|
188
|
+
}), {
|
|
189
|
+
...context,
|
|
190
|
+
overallStatus: "idle",
|
|
191
|
+
translationJobs: /* @__PURE__ */ new Map(),
|
|
192
|
+
// Clear any release conflicts from previous run
|
|
193
|
+
releaseConfig: {
|
|
194
|
+
...context.releaseConfig,
|
|
195
|
+
conflicts: void 0,
|
|
196
|
+
replaceExisting: void 0
|
|
197
|
+
}
|
|
198
|
+
};
|
|
136
199
|
default:
|
|
137
200
|
return context;
|
|
138
201
|
}
|
|
139
|
-
}, TranslationContextProvider = ({ document, config, children }) => {
|
|
202
|
+
}, generateDefaultReleaseTitle = (document) => `[AI Translation] ${document.title || document.name || document._id.split("-").slice(0, 2).join("-")}`, TranslationContextProvider = ({ document, config, children }) => {
|
|
140
203
|
devLog.debug("TranslationContextProvider initialized", {
|
|
141
204
|
documentId: document._id,
|
|
142
205
|
documentType: document._type,
|
|
143
206
|
supportedLanguages: config.supportedLanguages?.map((l) => l.id),
|
|
144
207
|
component: "TranslationContextProvider"
|
|
145
208
|
});
|
|
146
|
-
const initialContext = {
|
|
209
|
+
const defaultReleaseTitle = generateDefaultReleaseTitle(document), activeTranslation = getActiveTranslation(document._id), initialContext = {
|
|
147
210
|
document,
|
|
148
211
|
config,
|
|
149
|
-
|
|
212
|
+
// If there's an active translation, restore its state
|
|
213
|
+
overallStatus: activeTranslation?.overallStatus ?? "idle",
|
|
150
214
|
translationMetadata: null,
|
|
151
215
|
selectedTargetLanguages: [],
|
|
152
|
-
shouldOverwriteFreshTranslations: !1,
|
|
153
216
|
shouldTranslateReferences: !1,
|
|
154
217
|
maxReferenceDepth: 1,
|
|
155
218
|
references: [],
|
|
156
|
-
|
|
219
|
+
// Restore jobs from active translation if available
|
|
220
|
+
translationJobs: activeTranslation?.translationJobs ?? /* @__PURE__ */ new Map(),
|
|
157
221
|
translationMode: config?.openai?.defaultMode || "sanity-only",
|
|
158
|
-
openaiValidated: !1
|
|
159
|
-
|
|
222
|
+
openaiValidated: !1,
|
|
223
|
+
releaseConfig: {
|
|
224
|
+
mode: "new",
|
|
225
|
+
newReleaseTitle: defaultReleaseTitle
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
activeTranslation && devLog.info("Reconnecting to active translation", {
|
|
229
|
+
documentId: document._id,
|
|
230
|
+
status: activeTranslation.overallStatus,
|
|
231
|
+
jobCount: activeTranslation.translationJobs.size,
|
|
232
|
+
component: "TranslationContextProvider"
|
|
233
|
+
});
|
|
234
|
+
const [context, dispatch] = useReducer(translationReducer, initialContext);
|
|
160
235
|
return useEffect(() => {
|
|
236
|
+
const handleActiveTranslationUpdate = (state) => {
|
|
237
|
+
dispatch({
|
|
238
|
+
type: "SET_OVERALL_STATUS",
|
|
239
|
+
payload: state.overallStatus
|
|
240
|
+
});
|
|
241
|
+
for (const job of state.translationJobs.values())
|
|
242
|
+
dispatch({
|
|
243
|
+
type: "SET_TRANSLATION_JOB_STATUS",
|
|
244
|
+
payload: job
|
|
245
|
+
});
|
|
246
|
+
}, unsubscribe = subscribeToActiveTranslation(document._id, handleActiveTranslationUpdate);
|
|
247
|
+
return () => {
|
|
248
|
+
unsubscribe();
|
|
249
|
+
};
|
|
250
|
+
}, [document._id]), useEffect(() => {
|
|
161
251
|
(async () => {
|
|
162
252
|
if (config?.openai?.enabled && config?.openai?.apiKey)
|
|
163
253
|
try {
|
|
@@ -196,1044 +286,238 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
196
286
|
"useTranslationContext must be used within a TranslationContextProvider with document and config props"
|
|
197
287
|
);
|
|
198
288
|
return context;
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
289
|
+
}, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
290
|
+
(translation) => translation._key === targetLanguage
|
|
291
|
+
)?.value._ref || null, undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_TYPE = "translation.metadata", getTranslationMetadata = async ({
|
|
292
|
+
client,
|
|
293
|
+
sourceDocument,
|
|
294
|
+
sourceLanguage
|
|
295
|
+
}) => {
|
|
296
|
+
try {
|
|
297
|
+
return await client.fetch(
|
|
298
|
+
"*[_type == $tmType && references($sourceDocumentId)][0]",
|
|
299
|
+
{
|
|
300
|
+
tmType: TRANSLATION_METADATA_TYPE,
|
|
301
|
+
sourceDocumentId: undraftId(sourceDocument._id)
|
|
302
|
+
}
|
|
303
|
+
) || await client.create({
|
|
304
|
+
_type: TRANSLATION_METADATA_TYPE,
|
|
305
|
+
translations: [
|
|
306
|
+
{
|
|
307
|
+
_key: sourceLanguage,
|
|
308
|
+
_type: "internationalizedArrayReferenceValue",
|
|
309
|
+
value: {
|
|
310
|
+
_type: "reference",
|
|
311
|
+
_ref: undraftId(sourceDocument._id),
|
|
312
|
+
_weak: !0,
|
|
313
|
+
_strengthenOnPublish: {
|
|
314
|
+
_type: sourceDocument._type,
|
|
315
|
+
_weak: !1
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
translationHistory: []
|
|
321
|
+
});
|
|
322
|
+
} catch (error) {
|
|
323
|
+
throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
|
|
324
|
+
}
|
|
325
|
+
}, checkDocumentConflicts = async ({
|
|
326
|
+
client,
|
|
327
|
+
releaseId,
|
|
328
|
+
documentId,
|
|
329
|
+
documentTitle,
|
|
330
|
+
documentType,
|
|
331
|
+
targetLanguages,
|
|
332
|
+
languageFieldName
|
|
333
|
+
}) => {
|
|
334
|
+
const conflicts = [], document = await client.withConfig({ perspective: "drafts" }).getDocument(documentId);
|
|
335
|
+
if (!document)
|
|
336
|
+
return conflicts;
|
|
337
|
+
const sourceLanguage = document[languageFieldName], translationMetadata = await getTranslationMetadata({
|
|
338
|
+
client,
|
|
339
|
+
sourceDocument: document,
|
|
340
|
+
sourceLanguage
|
|
206
341
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
342
|
+
for (const targetLanguage of targetLanguages) {
|
|
343
|
+
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage);
|
|
344
|
+
if (existingTranslationId) {
|
|
345
|
+
const versionedId = `versions.${releaseId}.${existingTranslationId}`;
|
|
346
|
+
await client.getDocument(versionedId) && (conflicts.push({
|
|
347
|
+
sourceDocumentId: documentId,
|
|
348
|
+
sourceDocumentTitle: documentTitle || document.title || document.name,
|
|
349
|
+
sourceDocumentType: documentType || document._type,
|
|
350
|
+
targetLanguage,
|
|
351
|
+
existingVersionId: versionedId
|
|
352
|
+
}), devLog.debug("Found existing version in release", {
|
|
353
|
+
releaseId,
|
|
354
|
+
sourceDocumentId: documentId,
|
|
355
|
+
targetLanguage,
|
|
356
|
+
versionedId
|
|
357
|
+
}));
|
|
219
358
|
}
|
|
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
359
|
}
|
|
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({
|
|
360
|
+
return conflicts;
|
|
361
|
+
}, checkReleaseConflicts = async ({
|
|
367
362
|
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) {
|
|
363
|
+
releaseId,
|
|
364
|
+
sourceDocument,
|
|
365
|
+
targetLanguages,
|
|
366
|
+
languageFieldName,
|
|
367
|
+
references = []
|
|
368
|
+
}) => {
|
|
369
|
+
const allConflicts = [];
|
|
562
370
|
try {
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
371
|
+
const documentsToCheck = [
|
|
372
|
+
{
|
|
373
|
+
_id: sourceDocument._id,
|
|
374
|
+
_type: sourceDocument._type,
|
|
375
|
+
title: sourceDocument.title || sourceDocument.name
|
|
376
|
+
},
|
|
377
|
+
...references.map((ref) => ({
|
|
378
|
+
_id: ref._id,
|
|
379
|
+
_type: ref._type,
|
|
380
|
+
title: ref.title
|
|
381
|
+
}))
|
|
382
|
+
], conflictChecks = await Promise.all(
|
|
383
|
+
documentsToCheck.map(
|
|
384
|
+
(doc) => checkDocumentConflicts({
|
|
385
|
+
client,
|
|
386
|
+
releaseId,
|
|
387
|
+
documentId: doc._id,
|
|
388
|
+
documentTitle: doc.title,
|
|
389
|
+
documentType: doc._type,
|
|
390
|
+
targetLanguages,
|
|
391
|
+
languageFieldName
|
|
392
|
+
})
|
|
393
|
+
)
|
|
394
|
+
);
|
|
395
|
+
for (const conflicts of conflictChecks)
|
|
396
|
+
allConflicts.push(...conflicts);
|
|
397
|
+
devLog.debug("Release conflict check complete", {
|
|
398
|
+
releaseId,
|
|
399
|
+
documentsChecked: documentsToCheck.length,
|
|
400
|
+
conflictsFound: allConflicts.length
|
|
566
401
|
});
|
|
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
402
|
} catch (error) {
|
|
573
|
-
|
|
403
|
+
devLog.error("Error checking release conflicts", error instanceof Error ? error : new Error(String(error)), {
|
|
404
|
+
releaseId,
|
|
405
|
+
sourceDocumentId: sourceDocument._id,
|
|
406
|
+
targetLanguages,
|
|
407
|
+
referencesCount: references.length
|
|
408
|
+
});
|
|
574
409
|
}
|
|
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");
|
|
410
|
+
return allConflicts;
|
|
411
|
+
};
|
|
412
|
+
function isNetworkError(error) {
|
|
413
|
+
if (!error) return !1;
|
|
414
|
+
if (error instanceof TypeError) {
|
|
415
|
+
const message = error.message.toLowerCase();
|
|
416
|
+
return message.includes("fetch") || message.includes("network") || message.includes("connection") || message.includes("failed to fetch") || message.includes("networkerror");
|
|
600
417
|
}
|
|
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));
|
|
418
|
+
if (error && typeof error == "object" && "code" in error) {
|
|
419
|
+
const code = error.code;
|
|
420
|
+
return ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "ECONNREFUSED", "EHOSTUNREACH"].includes(code);
|
|
618
421
|
}
|
|
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));
|
|
422
|
+
if (error && typeof error == "object" && "response" in error) {
|
|
423
|
+
const response = error.response;
|
|
424
|
+
if (response?.status)
|
|
425
|
+
return response.status >= 500 || response.status === 429;
|
|
630
426
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
suggestions.filter((s) => s.approved)
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLanguage, globalPrompts, localePrompts) {
|
|
639
|
-
if (!targetLanguage)
|
|
640
|
-
throw devLog.error("targetLanguage is undefined or empty!", {
|
|
641
|
-
targetLanguage,
|
|
642
|
-
globalPrompts: globalPrompts.length,
|
|
643
|
-
localePrompts: localePrompts.length
|
|
644
|
-
}), new Error("targetLanguage is required but was undefined");
|
|
645
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0], currentSettings = await client.fetch("*[_id == $settingsId][0]{ globalPrompt, localePrompts }", { settingsId });
|
|
646
|
-
if (globalPrompts.length > 0) {
|
|
647
|
-
const currentGlobalPrompt = currentSettings?.globalPrompt || "", updates = globalPrompts.map((s) => s.description.trim()).filter(Boolean), newGlobalPrompt = [
|
|
648
|
-
currentGlobalPrompt,
|
|
649
|
-
currentGlobalPrompt ? `
|
|
650
|
-
` : "",
|
|
651
|
-
`## User Feedback (${timestamp})`,
|
|
652
|
-
...updates.map((update) => `- ${update}`)
|
|
653
|
-
].filter(Boolean).join(`
|
|
654
|
-
`);
|
|
655
|
-
await client.patch(settingsId).set({ globalPrompt: newGlobalPrompt }).commit();
|
|
427
|
+
if (error instanceof Error) {
|
|
428
|
+
const message = error.message;
|
|
429
|
+
if (/\b(50[0-9]|429)\b/.test(message))
|
|
430
|
+
return !0;
|
|
656
431
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
432
|
+
return !1;
|
|
433
|
+
}
|
|
434
|
+
function isSanityMutationError(error) {
|
|
435
|
+
if (!error) return !1;
|
|
436
|
+
if (error instanceof Error) {
|
|
437
|
+
const message = error.message.toLowerCase();
|
|
438
|
+
return [
|
|
439
|
+
"validation",
|
|
440
|
+
"schema",
|
|
441
|
+
"invalid document",
|
|
442
|
+
"invalid field",
|
|
443
|
+
"type mismatch",
|
|
444
|
+
"reference",
|
|
445
|
+
"patch",
|
|
446
|
+
"mutation",
|
|
447
|
+
"document structure",
|
|
448
|
+
"required field",
|
|
449
|
+
"field type",
|
|
450
|
+
"sanity"
|
|
451
|
+
].some((pattern) => message.includes(pattern));
|
|
452
|
+
}
|
|
453
|
+
if (error && typeof error == "object") {
|
|
454
|
+
const errorObj = error;
|
|
455
|
+
if (("statusCode" in errorObj || "response" in errorObj || "details" in errorObj) && "statusCode" in errorObj) {
|
|
456
|
+
const statusCode = errorObj.statusCode;
|
|
457
|
+
if (statusCode >= 400 && statusCode < 500 && statusCode !== 429)
|
|
458
|
+
return !0;
|
|
679
459
|
}
|
|
680
460
|
}
|
|
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 }));
|
|
461
|
+
return !1;
|
|
697
462
|
}
|
|
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
|
-
});
|
|
463
|
+
const DEFAULT_RETRY_OPTIONS = {
|
|
464
|
+
maxAttempts: 2,
|
|
465
|
+
initialDelayMs: 0,
|
|
466
|
+
maxDelayMs: 5e3,
|
|
467
|
+
backoffMultiplier: 2,
|
|
468
|
+
retryableErrors: isNetworkError,
|
|
469
|
+
logRetries: !0
|
|
470
|
+
};
|
|
471
|
+
function calculateDelay(attempt, options) {
|
|
472
|
+
const delay = options.initialDelayMs * Math.pow(options.backoffMultiplier, attempt);
|
|
473
|
+
return Math.min(delay, options.maxDelayMs);
|
|
752
474
|
}
|
|
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();
|
|
475
|
+
function sleep(ms) {
|
|
476
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
763
477
|
}
|
|
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
|
|
478
|
+
async function retryWithBackoff(operation, options = {}) {
|
|
479
|
+
const opts = {
|
|
480
|
+
...DEFAULT_RETRY_OPTIONS,
|
|
481
|
+
...options,
|
|
482
|
+
retryableErrors: options.retryableErrors || DEFAULT_RETRY_OPTIONS.retryableErrors
|
|
792
483
|
};
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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);
|
|
862
|
-
};
|
|
863
|
-
return suggestions.length === 0 ? /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
864
|
-
/* @__PURE__ */ jsx(
|
|
865
|
-
ProgressPanel,
|
|
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("");
|
|
484
|
+
let lastError;
|
|
485
|
+
const maxAttempts = opts.maxAttempts + 1;
|
|
486
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++)
|
|
1024
487
|
try {
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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");
|
|
1065
|
-
try {
|
|
1066
|
-
const translationHistory2 = translationMetadata?.translationHistory?.find(
|
|
1067
|
-
(history) => history.language === document[config.languageFieldName]
|
|
1068
|
-
);
|
|
1069
|
-
if (!translationHistory2)
|
|
1070
|
-
throw new Error("Translation history not found");
|
|
1071
|
-
await applySuggestions({
|
|
1072
|
-
client,
|
|
1073
|
-
schemaId: config.schemaId,
|
|
1074
|
-
sourceLanguage: translationHistory2.sourceLanguage || "en-us",
|
|
1075
|
-
targetLanguage: translationHistory2.language,
|
|
1076
|
-
suggestions,
|
|
1077
|
-
onProgress: ({ stage, message }) => {
|
|
1078
|
-
setProcessingState(stage), setChangeSummary(message);
|
|
1079
|
-
}
|
|
1080
|
-
}), setProcessingState("completed");
|
|
1081
|
-
} catch (err) {
|
|
1082
|
-
console.error("Error applying suggestions:", err), setError(err instanceof Error ? err.message : "An unexpected error occurred"), setProcessingState("error");
|
|
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,
|
|
488
|
+
const result = await operation();
|
|
489
|
+
return attempt > 1 && opts.logRetries && devLog.info(`Operation succeeded after ${attempt} attempts`, {
|
|
490
|
+
...opts.context,
|
|
491
|
+
attempt,
|
|
492
|
+
totalAttempts: attempt
|
|
493
|
+
}), result;
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (lastError = error, !opts.retryableErrors(error))
|
|
496
|
+
throw opts.logRetries && devLog.warn("Error is not retryable, aborting retries", {
|
|
497
|
+
...opts.context,
|
|
498
|
+
attempt,
|
|
499
|
+
error: error instanceof Error ? error.message : String(error)
|
|
500
|
+
}), error;
|
|
501
|
+
if (attempt >= maxAttempts)
|
|
502
|
+
throw opts.logRetries && devLog.error(
|
|
503
|
+
`Operation failed after ${maxAttempts} attempts`,
|
|
504
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1167
505
|
{
|
|
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
|
|
506
|
+
...opts.context,
|
|
507
|
+
totalAttempts: maxAttempts
|
|
1174
508
|
}
|
|
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 }) })
|
|
509
|
+
), error;
|
|
510
|
+
const delay = calculateDelay(attempt - 1, opts);
|
|
511
|
+
opts.logRetries && devLog.warn(`Operation failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {
|
|
512
|
+
...opts.context,
|
|
513
|
+
attempt,
|
|
514
|
+
totalAttempts: maxAttempts,
|
|
515
|
+
delayMs: delay,
|
|
516
|
+
error: error instanceof Error ? error.message : String(error)
|
|
517
|
+
}), delay > 0 && await sleep(delay);
|
|
1234
518
|
}
|
|
1235
|
-
|
|
1236
|
-
}
|
|
519
|
+
throw lastError;
|
|
520
|
+
}
|
|
1237
521
|
class Semaphore {
|
|
1238
522
|
permits;
|
|
1239
523
|
queue = [];
|
|
@@ -1584,13 +868,27 @@ const fetchTranslationPrompts = async ({
|
|
|
1584
868
|
}
|
|
1585
869
|
}), hasChanges = !0);
|
|
1586
870
|
}
|
|
1587
|
-
hasChanges && (await
|
|
871
|
+
hasChanges && (await retryWithBackoff(
|
|
872
|
+
async () => {
|
|
873
|
+
await patch.commit();
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
maxAttempts: 2,
|
|
877
|
+
initialDelayMs: 0,
|
|
878
|
+
retryableErrors: isNetworkError,
|
|
879
|
+
context: {
|
|
880
|
+
operation: "localizeReferences",
|
|
881
|
+
documentId,
|
|
882
|
+
referenceCount: refs.length
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
), devLog.info("Updated references in batch", {
|
|
1588
886
|
documentId,
|
|
1589
887
|
count: refs.length,
|
|
1590
888
|
translatedCount: translationMap.size,
|
|
1591
889
|
component: "updateReferences"
|
|
1592
890
|
}));
|
|
1593
|
-
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages) => {
|
|
891
|
+
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages, mainSourceDocumentId) => {
|
|
1594
892
|
for (const sourceDocument of sourceDocuments)
|
|
1595
893
|
for (const lang of targetLanguages) {
|
|
1596
894
|
const payload = {
|
|
@@ -1603,146 +901,8 @@ const fetchTranslationPrompts = async ({
|
|
|
1603
901
|
dispatch({
|
|
1604
902
|
type: "SET_TRANSLATION_JOB_STATUS",
|
|
1605
903
|
payload
|
|
1606
|
-
});
|
|
904
|
+
}), mainSourceDocumentId && updateActiveTranslationJob(mainSourceDocumentId, payload);
|
|
1607
905
|
}
|
|
1608
|
-
}, DEFAULT_OPTIONS = {
|
|
1609
|
-
maxRetries: 3,
|
|
1610
|
-
initialDelayMs: 1e3,
|
|
1611
|
-
maxDelayMs: 3e4,
|
|
1612
|
-
backoffMultiplier: 2,
|
|
1613
|
-
jitter: !0
|
|
1614
|
-
};
|
|
1615
|
-
function isRetryableError(error) {
|
|
1616
|
-
if (error instanceof Error) {
|
|
1617
|
-
const message = error.message.toLowerCase(), name = error.name.toLowerCase(), nonRetryablePatterns = [
|
|
1618
|
-
"schema not found",
|
|
1619
|
-
"invalid schema",
|
|
1620
|
-
"not found",
|
|
1621
|
-
"unauthorized",
|
|
1622
|
-
"forbidden",
|
|
1623
|
-
"invalid api key",
|
|
1624
|
-
"missing api key",
|
|
1625
|
-
"validation failed",
|
|
1626
|
-
"invalid document",
|
|
1627
|
-
"source language not found",
|
|
1628
|
-
"invalid configuration",
|
|
1629
|
-
"type mismatch"
|
|
1630
|
-
];
|
|
1631
|
-
for (const pattern of nonRetryablePatterns)
|
|
1632
|
-
if (message.includes(pattern) || name.includes(pattern))
|
|
1633
|
-
return !1;
|
|
1634
|
-
const retryablePatterns = [
|
|
1635
|
-
"failed to fetch",
|
|
1636
|
-
"network",
|
|
1637
|
-
"timeout",
|
|
1638
|
-
"econnreset",
|
|
1639
|
-
"econnrefused",
|
|
1640
|
-
"enotfound",
|
|
1641
|
-
"socket",
|
|
1642
|
-
"rate limit",
|
|
1643
|
-
"too many requests",
|
|
1644
|
-
"429",
|
|
1645
|
-
"500",
|
|
1646
|
-
"502",
|
|
1647
|
-
"503",
|
|
1648
|
-
"504",
|
|
1649
|
-
"internal server error",
|
|
1650
|
-
"bad gateway",
|
|
1651
|
-
"service unavailable",
|
|
1652
|
-
"gateway timeout",
|
|
1653
|
-
"temporarily unavailable",
|
|
1654
|
-
"try again"
|
|
1655
|
-
];
|
|
1656
|
-
for (const pattern of retryablePatterns)
|
|
1657
|
-
if (message.includes(pattern) || name.includes(pattern))
|
|
1658
|
-
return !0;
|
|
1659
|
-
}
|
|
1660
|
-
if (typeof error == "object" && error !== null) {
|
|
1661
|
-
const statusCode = error.statusCode || error.status;
|
|
1662
|
-
if (statusCode) {
|
|
1663
|
-
if (statusCode === 429 || statusCode >= 500 && statusCode < 600)
|
|
1664
|
-
return !0;
|
|
1665
|
-
if (statusCode >= 400 && statusCode < 500)
|
|
1666
|
-
return !1;
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
return !0;
|
|
1670
|
-
}
|
|
1671
|
-
function calculateBackoffDelay(attempt, options) {
|
|
1672
|
-
const { initialDelayMs, maxDelayMs, backoffMultiplier, jitter } = options;
|
|
1673
|
-
let delay = initialDelayMs * backoffMultiplier ** attempt;
|
|
1674
|
-
if (delay = Math.min(delay, maxDelayMs), jitter) {
|
|
1675
|
-
const jitterFactor = 0.75 + Math.random() * 0.5;
|
|
1676
|
-
delay = Math.round(delay * jitterFactor);
|
|
1677
|
-
}
|
|
1678
|
-
return delay;
|
|
1679
|
-
}
|
|
1680
|
-
function sleep(ms) {
|
|
1681
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1682
|
-
}
|
|
1683
|
-
async function withRetry(fn, options = {}) {
|
|
1684
|
-
const opts = { ...DEFAULT_OPTIONS, ...options }, shouldRetry = opts.isRetryable ?? isRetryableError;
|
|
1685
|
-
let lastError, attempts = 0;
|
|
1686
|
-
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
1687
|
-
attempts = attempt + 1;
|
|
1688
|
-
try {
|
|
1689
|
-
return { success: !0, result: await fn(), attempts };
|
|
1690
|
-
} catch (error) {
|
|
1691
|
-
if (lastError = error, attempt >= opts.maxRetries) {
|
|
1692
|
-
devLog.debug("Max retries exhausted", { attempt: attempts, error });
|
|
1693
|
-
break;
|
|
1694
|
-
}
|
|
1695
|
-
if (!shouldRetry(error)) {
|
|
1696
|
-
devLog.debug("Error is not retryable", { attempt: attempts, error });
|
|
1697
|
-
break;
|
|
1698
|
-
}
|
|
1699
|
-
const delay = calculateBackoffDelay(attempt, opts);
|
|
1700
|
-
devLog.debug("Scheduling retry", {
|
|
1701
|
-
attempt: attempts,
|
|
1702
|
-
nextAttempt: attempt + 2,
|
|
1703
|
-
delayMs: delay,
|
|
1704
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1705
|
-
}), opts.onRetry && opts.onRetry(attempt + 1, error, delay), await sleep(delay);
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
return { success: !1, error: lastError, attempts };
|
|
1709
|
-
}
|
|
1710
|
-
const getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
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
906
|
}, updateTranslationMetadata = async ({
|
|
1747
907
|
client,
|
|
1748
908
|
targetLanguage,
|
|
@@ -1776,11 +936,71 @@ const getExistingTranslationId = (translationMetadata, targetLanguage) => transl
|
|
|
1776
936
|
targetRev: newDocument._rev,
|
|
1777
937
|
targetSnapshot: JSON.stringify(newDocument)
|
|
1778
938
|
}, translationPatch = client.patch(translationMetadata._id).setIfMissing({ translations: [] });
|
|
1779
|
-
isNewTranslation ? await
|
|
939
|
+
isNewTranslation ? await retryWithBackoff(
|
|
940
|
+
async () => {
|
|
941
|
+
await translationPatch.insert("after", "translations[-1]", [refToNewTranslation]).commit();
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
maxAttempts: 2,
|
|
945
|
+
initialDelayMs: 0,
|
|
946
|
+
retryableErrors: isNetworkError,
|
|
947
|
+
context: {
|
|
948
|
+
operation: "updateTranslationMetadata.translations",
|
|
949
|
+
sourceDocumentId: sourceDocument._id,
|
|
950
|
+
targetLanguage,
|
|
951
|
+
isNewTranslation: !0
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
) : await retryWithBackoff(
|
|
955
|
+
async () => {
|
|
956
|
+
await translationPatch.insert("replace", `translations[_key == "${targetLanguage}"]`, [refToNewTranslation]).commit();
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
maxAttempts: 2,
|
|
960
|
+
initialDelayMs: 0,
|
|
961
|
+
retryableErrors: isNetworkError,
|
|
962
|
+
context: {
|
|
963
|
+
operation: "updateTranslationMetadata.translations",
|
|
964
|
+
sourceDocumentId: sourceDocument._id,
|
|
965
|
+
targetLanguage,
|
|
966
|
+
isNewTranslation: !1
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
);
|
|
1780
970
|
const historyPatch = client.patch(translationMetadata._id).setIfMissing({ translationHistory: [] });
|
|
1781
971
|
translationMetadata.translationHistory?.some(
|
|
1782
972
|
(history) => history.language === targetLanguage
|
|
1783
|
-
) ? await
|
|
973
|
+
) ? await retryWithBackoff(
|
|
974
|
+
async () => {
|
|
975
|
+
await historyPatch.insert("replace", `translationHistory[_key == "${targetLanguage}"]`, [newTranslationHistory]).commit();
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
maxAttempts: 2,
|
|
979
|
+
initialDelayMs: 0,
|
|
980
|
+
retryableErrors: isNetworkError,
|
|
981
|
+
context: {
|
|
982
|
+
operation: "updateTranslationMetadata.history",
|
|
983
|
+
sourceDocumentId: sourceDocument._id,
|
|
984
|
+
targetLanguage,
|
|
985
|
+
isNewHistory: !1
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
) : await retryWithBackoff(
|
|
989
|
+
async () => {
|
|
990
|
+
await historyPatch.insert("after", "translationHistory[-1]", [newTranslationHistory]).commit();
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
maxAttempts: 2,
|
|
994
|
+
initialDelayMs: 0,
|
|
995
|
+
retryableErrors: isNetworkError,
|
|
996
|
+
context: {
|
|
997
|
+
operation: "updateTranslationMetadata.history",
|
|
998
|
+
sourceDocumentId: sourceDocument._id,
|
|
999
|
+
targetLanguage,
|
|
1000
|
+
isNewHistory: !0
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
);
|
|
1784
1004
|
} catch (error) {
|
|
1785
1005
|
throw devLog.error("Error updating translation metadata", error instanceof Error ? error : new Error(String(error)), {
|
|
1786
1006
|
sourceDocumentId: sourceDocument._id,
|
|
@@ -1789,15 +1009,16 @@ const getExistingTranslationId = (translationMetadata, targetLanguage) => transl
|
|
|
1789
1009
|
component: "updateTranslationMetadata"
|
|
1790
1010
|
}), error;
|
|
1791
1011
|
}
|
|
1792
|
-
}, setJobStatus = (dispatch, _id, language, status, payload) => {
|
|
1012
|
+
}, setJobStatus = (dispatch, _id, language, status, payload, sourceDocumentId) => {
|
|
1013
|
+
const job = defaults(payload, {
|
|
1014
|
+
_id,
|
|
1015
|
+
language,
|
|
1016
|
+
status
|
|
1017
|
+
});
|
|
1793
1018
|
dispatch({
|
|
1794
1019
|
type: "SET_TRANSLATION_JOB_STATUS",
|
|
1795
|
-
payload:
|
|
1796
|
-
|
|
1797
|
-
language,
|
|
1798
|
-
status
|
|
1799
|
-
})
|
|
1800
|
-
});
|
|
1020
|
+
payload: job
|
|
1021
|
+
}), sourceDocumentId && updateActiveTranslationJob(sourceDocumentId, job);
|
|
1801
1022
|
};
|
|
1802
1023
|
function shouldExcludeField(fieldPath, excludeFields) {
|
|
1803
1024
|
for (const pattern of excludeFields) {
|
|
@@ -2123,14 +1344,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2123
1344
|
}), devLog.debug("OpenAI GPT-5 Full Prompt", {
|
|
2124
1345
|
messages: request.messages
|
|
2125
1346
|
});
|
|
2126
|
-
const response = await
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
1347
|
+
const response = await retryWithBackoff(
|
|
1348
|
+
async () => {
|
|
1349
|
+
const fetchResponse = await fetch("https://api.openai.com/v1/responses", {
|
|
1350
|
+
method: "POST",
|
|
1351
|
+
headers: {
|
|
1352
|
+
"Content-Type": "application/json",
|
|
1353
|
+
Authorization: `Bearer ${apiKey}`
|
|
1354
|
+
},
|
|
1355
|
+
body: JSON.stringify(requestBody)
|
|
1356
|
+
});
|
|
1357
|
+
if (!fetchResponse.ok) {
|
|
1358
|
+
const errorMessage = `GPT-5 API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
|
|
1359
|
+
throw errorObj.response = { status: fetchResponse.status }, errorObj;
|
|
1360
|
+
}
|
|
1361
|
+
return fetchResponse;
|
|
2131
1362
|
},
|
|
2132
|
-
|
|
2133
|
-
|
|
1363
|
+
{
|
|
1364
|
+
maxAttempts: 2,
|
|
1365
|
+
initialDelayMs: 0,
|
|
1366
|
+
retryableErrors: isNetworkError,
|
|
1367
|
+
context: { model: requestBody.model, endpoint: "responses" }
|
|
1368
|
+
}
|
|
1369
|
+
);
|
|
2134
1370
|
if (!response.ok) {
|
|
2135
1371
|
const error = await response.json();
|
|
2136
1372
|
throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
|
|
@@ -2169,14 +1405,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2169
1405
|
}), devLog.debug("OpenAI Chat Completions Full Prompt", {
|
|
2170
1406
|
messages: request.messages
|
|
2171
1407
|
});
|
|
2172
|
-
const response = await
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
1408
|
+
const response = await retryWithBackoff(
|
|
1409
|
+
async () => {
|
|
1410
|
+
const fetchResponse = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1411
|
+
method: "POST",
|
|
1412
|
+
headers: {
|
|
1413
|
+
"Content-Type": "application/json",
|
|
1414
|
+
Authorization: `Bearer ${apiKey}`
|
|
1415
|
+
},
|
|
1416
|
+
body: JSON.stringify(requestBody)
|
|
1417
|
+
});
|
|
1418
|
+
if (!fetchResponse.ok) {
|
|
1419
|
+
const errorMessage = `OpenAI API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
|
|
1420
|
+
throw errorObj.response = { status: fetchResponse.status }, errorObj;
|
|
1421
|
+
}
|
|
1422
|
+
return fetchResponse;
|
|
2177
1423
|
},
|
|
2178
|
-
|
|
2179
|
-
|
|
1424
|
+
{
|
|
1425
|
+
maxAttempts: 2,
|
|
1426
|
+
initialDelayMs: 0,
|
|
1427
|
+
retryableErrors: isNetworkError,
|
|
1428
|
+
context: { model, endpoint: "chat/completions" }
|
|
1429
|
+
}
|
|
1430
|
+
);
|
|
2180
1431
|
if (!response.ok) {
|
|
2181
1432
|
const error = await response.json();
|
|
2182
1433
|
throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
|
|
@@ -2331,7 +1582,21 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2331
1582
|
parent && parent.children.push(node);
|
|
2332
1583
|
} else
|
|
2333
1584
|
root.push(node);
|
|
2334
|
-
else
|
|
1585
|
+
else if (attributes.type === "string" && !content) {
|
|
1586
|
+
const contentLines = [];
|
|
1587
|
+
for (i++; i < lines.length; ) {
|
|
1588
|
+
const currentLine = lines[i];
|
|
1589
|
+
if (currentLine.trim() === "</field>")
|
|
1590
|
+
break;
|
|
1591
|
+
contentLines.push(currentLine), i++;
|
|
1592
|
+
}
|
|
1593
|
+
if (node.text = contentLines.join(`
|
|
1594
|
+
`).trim(), stack.length > 0) {
|
|
1595
|
+
const parent = stack[stack.length - 1];
|
|
1596
|
+
parent && parent.children.push(node);
|
|
1597
|
+
} else
|
|
1598
|
+
root.push(node);
|
|
1599
|
+
} else
|
|
2335
1600
|
stack.push(node);
|
|
2336
1601
|
continue;
|
|
2337
1602
|
}
|
|
@@ -2592,11 +1857,12 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2592
1857
|
}, getOpenAITranslation = async ({
|
|
2593
1858
|
dispatch,
|
|
2594
1859
|
sourceDocument,
|
|
2595
|
-
originalSourceDocument,
|
|
2596
1860
|
languageFieldName,
|
|
2597
1861
|
translationPrompts,
|
|
2598
1862
|
targetLanguage,
|
|
2599
|
-
openaiConfig
|
|
1863
|
+
openaiConfig,
|
|
1864
|
+
previousError,
|
|
1865
|
+
errorContext
|
|
2600
1866
|
}) => {
|
|
2601
1867
|
const sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
2602
1868
|
sourceId: sourceDocument._id,
|
|
@@ -2614,7 +1880,8 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2614
1880
|
sourceText,
|
|
2615
1881
|
sourceLanguage,
|
|
2616
1882
|
targetLanguage,
|
|
2617
|
-
translationPrompts
|
|
1883
|
+
translationPrompts,
|
|
1884
|
+
errorContext
|
|
2618
1885
|
}), response = await callGPT5(openaiConfig.apiKey, {
|
|
2619
1886
|
messages: [
|
|
2620
1887
|
{
|
|
@@ -2633,9 +1900,9 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2633
1900
|
responseLength: response.content.length,
|
|
2634
1901
|
responsePreview: response.content.substring(0, 500)
|
|
2635
1902
|
});
|
|
2636
|
-
const
|
|
1903
|
+
const result = deserializeDocument(sourceDocument, response.content);
|
|
2637
1904
|
result[languageFieldName] = targetLanguage;
|
|
2638
|
-
const validation = validateStructureIntegrity(
|
|
1905
|
+
const validation = validateStructureIntegrity(sourceDocument, result);
|
|
2639
1906
|
if (!validation.valid) {
|
|
2640
1907
|
devLog.error("Structure validation failed", {
|
|
2641
1908
|
...logContext,
|
|
@@ -2672,14 +1939,21 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2672
1939
|
sourceText,
|
|
2673
1940
|
sourceLanguage,
|
|
2674
1941
|
targetLanguage,
|
|
2675
|
-
translationPrompts
|
|
1942
|
+
translationPrompts,
|
|
1943
|
+
errorContext
|
|
2676
1944
|
}) => {
|
|
2677
1945
|
const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
|
|
2678
1946
|
return `# TRANSLATION TASK
|
|
2679
1947
|
|
|
2680
1948
|
Translate this ${sourceLanguage} document into ${targetLanguage}.
|
|
2681
1949
|
|
|
2682
|
-
|
|
1950
|
+
${errorContext ? `## \u26A0\uFE0F IMPORTANT: Previous Attempt Error
|
|
1951
|
+
|
|
1952
|
+
${errorContext}
|
|
1953
|
+
|
|
1954
|
+
Please carefully review the error above and ensure your translation fixes the issue.
|
|
1955
|
+
|
|
1956
|
+
` : ""}## Guidelines
|
|
2683
1957
|
|
|
2684
1958
|
${globalPrompt ? `### General Guidelines
|
|
2685
1959
|
${globalPrompt}
|
|
@@ -2853,21 +2127,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2853
2127
|
languageFieldName,
|
|
2854
2128
|
targetLanguage
|
|
2855
2129
|
});
|
|
2856
|
-
return await
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2130
|
+
return await retryWithBackoff(
|
|
2131
|
+
async () => await client.agent.action.translate({
|
|
2132
|
+
schemaId,
|
|
2133
|
+
toLanguage,
|
|
2134
|
+
fromLanguage,
|
|
2135
|
+
documentId: sourceDocument._id,
|
|
2136
|
+
noWrite: !0,
|
|
2137
|
+
// Do not write to the document, just return the translated content
|
|
2138
|
+
languageFieldPath: [languageFieldName],
|
|
2139
|
+
styleGuide,
|
|
2140
|
+
protectedPhrases: translationPrompts.protectedPhrases,
|
|
2141
|
+
conditionalPaths: {
|
|
2142
|
+
defaultHidden: !1,
|
|
2143
|
+
defaultReadOnly: !1
|
|
2144
|
+
}
|
|
2145
|
+
}),
|
|
2146
|
+
{
|
|
2147
|
+
maxAttempts: 2,
|
|
2148
|
+
initialDelayMs: 0,
|
|
2149
|
+
retryableErrors: isNetworkError,
|
|
2150
|
+
context: {
|
|
2151
|
+
operation: "agent.action.translate",
|
|
2152
|
+
documentId: sourceDocument._id,
|
|
2153
|
+
targetLanguage,
|
|
2154
|
+
schemaId
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
);
|
|
2871
2158
|
}, handleTranslation = async ({
|
|
2872
2159
|
dispatch,
|
|
2873
2160
|
client,
|
|
@@ -2880,84 +2167,108 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2880
2167
|
translationMode,
|
|
2881
2168
|
openaiConfig,
|
|
2882
2169
|
releaseId,
|
|
2883
|
-
excludeFields = []
|
|
2170
|
+
excludeFields = [],
|
|
2171
|
+
replaceExisting = !1,
|
|
2172
|
+
mainSourceDocumentId
|
|
2884
2173
|
}) => {
|
|
2885
|
-
|
|
2174
|
+
const safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
|
|
2175
|
+
excludeFields.length !== safeExcludeFields.length && devLog.warn("Language field was in excludeFields - removing it to prevent translation errors", {
|
|
2176
|
+
documentId: sourceDocument._id,
|
|
2177
|
+
languageFieldName,
|
|
2178
|
+
originalExcludeFields: excludeFields,
|
|
2179
|
+
safeExcludeFields
|
|
2180
|
+
}), devLog.info("Filtering excluded fields before translation", {
|
|
2886
2181
|
documentId: sourceDocument._id,
|
|
2887
2182
|
documentType: sourceDocument._type,
|
|
2888
|
-
excludeFields,
|
|
2889
|
-
excludeFieldsLength:
|
|
2183
|
+
excludeFields: safeExcludeFields,
|
|
2184
|
+
excludeFieldsLength: safeExcludeFields?.length ?? 0,
|
|
2890
2185
|
sourceHasTitle: "title" in sourceDocument,
|
|
2891
2186
|
sourceTitleValue: sourceDocument.title
|
|
2892
2187
|
});
|
|
2893
|
-
const { filteredDocument, excludedValues } = filterExcludedFields(sourceDocument,
|
|
2188
|
+
const { filteredDocument, excludedValues } = filterExcludedFields(sourceDocument, safeExcludeFields);
|
|
2894
2189
|
devLog.info("After filtering excluded fields", {
|
|
2895
2190
|
documentId: sourceDocument._id,
|
|
2896
2191
|
filteredHasTitle: "title" in filteredDocument,
|
|
2897
2192
|
filteredTitleValue: filteredDocument.title,
|
|
2898
2193
|
excludedValuesKeys: Object.keys(excludedValues),
|
|
2899
2194
|
excludedValues
|
|
2900
|
-
}), devLog.debug("Applying field transforms before translation", {
|
|
2901
|
-
documentId: sourceDocument._id,
|
|
2902
|
-
documentType: sourceDocument._type
|
|
2903
|
-
});
|
|
2904
|
-
const { transformedDocument, originalStructure } = applyTransforms({
|
|
2905
|
-
document: filteredDocument,
|
|
2906
|
-
registry: globalTransformRegistry,
|
|
2907
|
-
schemaType: sourceDocument._type
|
|
2908
2195
|
});
|
|
2909
|
-
let
|
|
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
|
-
|
|
2196
|
+
let documentToTranslate, originalStructure = {};
|
|
2197
|
+
if (translationMode === "openai-only") {
|
|
2198
|
+
devLog.debug("Applying field transforms before translation (OpenAI mode)", {
|
|
2199
|
+
documentId: sourceDocument._id,
|
|
2200
|
+
documentType: sourceDocument._type
|
|
2201
|
+
});
|
|
2202
|
+
const transformResult = applyTransforms({
|
|
2203
|
+
document: filteredDocument,
|
|
2204
|
+
registry: globalTransformRegistry,
|
|
2205
|
+
schemaType: sourceDocument._type
|
|
2206
|
+
});
|
|
2207
|
+
documentToTranslate = transformResult.transformedDocument, originalStructure = transformResult.originalStructure;
|
|
2208
|
+
} else
|
|
2209
|
+
devLog.debug("Skipping field transforms (Sanity Agent mode)", {
|
|
2210
|
+
documentId: sourceDocument._id,
|
|
2211
|
+
documentType: sourceDocument._type
|
|
2212
|
+
}), documentToTranslate = filteredDocument;
|
|
2213
|
+
let translatedDoc, previousError = null;
|
|
2214
|
+
const performTranslation = async (errorContext) => {
|
|
2215
|
+
switch (translationMode) {
|
|
2216
|
+
case "sanity-only":
|
|
2217
|
+
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating", void 0, mainSourceDocumentId), await getSanityAgentTranslation({
|
|
2218
|
+
sourceDocument: documentToTranslate,
|
|
2219
|
+
targetLanguage,
|
|
2220
|
+
languageFieldName,
|
|
2221
|
+
schemaId,
|
|
2222
|
+
client,
|
|
2223
|
+
translationPrompts
|
|
2224
|
+
});
|
|
2225
|
+
case "openai-only": {
|
|
2226
|
+
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2227
|
+
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2228
|
+
return setJobStatus(
|
|
2229
|
+
dispatch,
|
|
2230
|
+
sourceDocument._id,
|
|
2231
|
+
targetLanguage,
|
|
2232
|
+
"translating_with_openai",
|
|
2233
|
+
void 0,
|
|
2234
|
+
mainSourceDocumentId
|
|
2235
|
+
), await getOpenAITranslation({
|
|
2236
|
+
dispatch,
|
|
2237
|
+
sourceDocument: documentToTranslate,
|
|
2238
|
+
targetLanguage,
|
|
2239
|
+
languageFieldName,
|
|
2240
|
+
translationPrompts,
|
|
2241
|
+
openaiConfig,
|
|
2242
|
+
previousError: errorContext && previousError ? previousError : void 0,
|
|
2243
|
+
errorContext
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
default:
|
|
2247
|
+
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2935
2248
|
}
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
default:
|
|
2939
|
-
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2940
|
-
}
|
|
2941
|
-
if (!translatedDoc)
|
|
2249
|
+
};
|
|
2250
|
+
if (translatedDoc = await performTranslation(), !translatedDoc)
|
|
2942
2251
|
return null;
|
|
2943
|
-
|
|
2252
|
+
let transformedResult;
|
|
2253
|
+
Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
|
|
2944
2254
|
documentId: sourceDocument._id,
|
|
2945
2255
|
documentType: sourceDocument._type
|
|
2946
|
-
})
|
|
2947
|
-
const transformedResult = restoreTransforms({
|
|
2256
|
+
}), transformedResult = restoreTransforms({
|
|
2948
2257
|
translatedDocument: translatedDoc,
|
|
2949
2258
|
originalStructure,
|
|
2950
2259
|
registry: globalTransformRegistry,
|
|
2951
2260
|
schemaType: sourceDocument._type
|
|
2952
|
-
});
|
|
2953
|
-
|
|
2261
|
+
})) : transformedResult = translatedDoc;
|
|
2262
|
+
const excludedValuesToRestore = { ...excludedValues };
|
|
2263
|
+
delete excludedValuesToRestore[languageFieldName], devLog.info("Before restoring excluded fields", {
|
|
2954
2264
|
documentId: sourceDocument._id,
|
|
2955
2265
|
excludedFieldCount: Object.keys(excludedValues).length,
|
|
2956
2266
|
excludedValuesKeys: Object.keys(excludedValues),
|
|
2267
|
+
languageFieldExcluded: languageFieldName in excludedValues,
|
|
2957
2268
|
transformedResultHasTitle: "title" in transformedResult,
|
|
2958
2269
|
transformedResultTitle: transformedResult.title
|
|
2959
2270
|
});
|
|
2960
|
-
const finalDoc = restoreExcludedFields(transformedResult,
|
|
2271
|
+
const finalDoc = restoreExcludedFields(transformedResult, excludedValuesToRestore);
|
|
2961
2272
|
devLog.info("After restoring excluded fields", {
|
|
2962
2273
|
documentId: sourceDocument._id,
|
|
2963
2274
|
finalDocHasTitle: "title" in finalDoc,
|
|
@@ -2967,11 +2278,76 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2967
2278
|
...finalDoc,
|
|
2968
2279
|
_id: targetDocumentVersionId
|
|
2969
2280
|
};
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
}),
|
|
2281
|
+
replaceExisting && await client.getDocument(targetDocumentVersionId) && (devLog.info("Replacing existing version in release", {
|
|
2282
|
+
documentId: sourceDocument._id,
|
|
2283
|
+
targetDocumentVersionId,
|
|
2284
|
+
releaseId
|
|
2285
|
+
}), await client.delete(targetDocumentVersionId));
|
|
2286
|
+
try {
|
|
2287
|
+
await retryWithBackoff(
|
|
2288
|
+
async () => {
|
|
2289
|
+
await client.action({
|
|
2290
|
+
actionType: "sanity.action.document.version.create",
|
|
2291
|
+
publishedId: targetDocumentPublishedId,
|
|
2292
|
+
document: newDoc
|
|
2293
|
+
});
|
|
2294
|
+
},
|
|
2295
|
+
{
|
|
2296
|
+
maxAttempts: 2,
|
|
2297
|
+
initialDelayMs: 0,
|
|
2298
|
+
retryableErrors: isNetworkError,
|
|
2299
|
+
context: {
|
|
2300
|
+
documentId: sourceDocument._id,
|
|
2301
|
+
targetLanguage,
|
|
2302
|
+
operation: "version.create"
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
);
|
|
2306
|
+
} catch (error) {
|
|
2307
|
+
if (translationMode === "openai-only" && isSanityMutationError(error)) {
|
|
2308
|
+
devLog.warn("Mutation error after OpenAI translation, retrying full translation", {
|
|
2309
|
+
documentId: sourceDocument._id,
|
|
2310
|
+
targetLanguage,
|
|
2311
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2312
|
+
}), previousError = error instanceof Error ? error : new Error(String(error));
|
|
2313
|
+
const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure.`;
|
|
2314
|
+
if (translatedDoc = await performTranslation(errorContext), !translatedDoc)
|
|
2315
|
+
throw new Error("Translation retry failed: no document returned");
|
|
2316
|
+
let retryTransformedResult;
|
|
2317
|
+
Object.keys(originalStructure).length > 0 ? retryTransformedResult = restoreTransforms({
|
|
2318
|
+
translatedDocument: translatedDoc,
|
|
2319
|
+
originalStructure,
|
|
2320
|
+
registry: globalTransformRegistry,
|
|
2321
|
+
schemaType: sourceDocument._type
|
|
2322
|
+
}) : retryTransformedResult = translatedDoc;
|
|
2323
|
+
const retryNewDoc = {
|
|
2324
|
+
...restoreExcludedFields(retryTransformedResult, excludedValuesToRestore),
|
|
2325
|
+
_id: targetDocumentVersionId
|
|
2326
|
+
};
|
|
2327
|
+
return await retryWithBackoff(
|
|
2328
|
+
async () => {
|
|
2329
|
+
await client.action({
|
|
2330
|
+
actionType: "sanity.action.document.version.create",
|
|
2331
|
+
publishedId: targetDocumentPublishedId,
|
|
2332
|
+
document: retryNewDoc
|
|
2333
|
+
});
|
|
2334
|
+
},
|
|
2335
|
+
{
|
|
2336
|
+
maxAttempts: 2,
|
|
2337
|
+
initialDelayMs: 0,
|
|
2338
|
+
retryableErrors: isNetworkError,
|
|
2339
|
+
context: {
|
|
2340
|
+
documentId: sourceDocument._id,
|
|
2341
|
+
targetLanguage,
|
|
2342
|
+
operation: "version.create",
|
|
2343
|
+
isRetry: !0
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
), retryNewDoc;
|
|
2347
|
+
}
|
|
2348
|
+
throw error;
|
|
2349
|
+
}
|
|
2350
|
+
return newDoc;
|
|
2975
2351
|
}, translateDocument = async ({
|
|
2976
2352
|
dispatch,
|
|
2977
2353
|
client,
|
|
@@ -2979,84 +2355,66 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2979
2355
|
sourceLanguage,
|
|
2980
2356
|
targetLanguage,
|
|
2981
2357
|
languageFieldName,
|
|
2982
|
-
shouldOverwriteFreshTranslations,
|
|
2983
2358
|
schemaId,
|
|
2984
2359
|
translationPrompts,
|
|
2985
2360
|
translationMode,
|
|
2986
2361
|
openaiConfig,
|
|
2987
2362
|
releaseId,
|
|
2988
|
-
excludeFields
|
|
2363
|
+
excludeFields,
|
|
2364
|
+
replaceExisting,
|
|
2365
|
+
mainSourceDocumentId
|
|
2989
2366
|
}) => {
|
|
2990
2367
|
const logContext = {
|
|
2991
2368
|
documentId: sourceDocument._id,
|
|
2992
2369
|
targetLanguage,
|
|
2993
2370
|
schemaId,
|
|
2994
|
-
shouldOverwriteFreshTranslations,
|
|
2995
2371
|
translationMode
|
|
2996
2372
|
};
|
|
2997
|
-
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
|
|
2373
|
+
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation", void 0, mainSourceDocumentId);
|
|
2998
2374
|
try {
|
|
2999
2375
|
const translationMetadata = await getTranslationMetadata({
|
|
3000
2376
|
client,
|
|
3001
2377
|
sourceDocument,
|
|
3002
2378
|
sourceLanguage
|
|
3003
|
-
}),
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
2379
|
+
}), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
|
|
2380
|
+
dispatch,
|
|
2381
|
+
client,
|
|
2382
|
+
schemaId,
|
|
2383
|
+
sourceDocument,
|
|
2384
|
+
targetLanguage,
|
|
2385
|
+
existingTranslationId,
|
|
2386
|
+
languageFieldName,
|
|
2387
|
+
translationPrompts,
|
|
2388
|
+
translationMode,
|
|
2389
|
+
openaiConfig,
|
|
2390
|
+
releaseId,
|
|
2391
|
+
excludeFields,
|
|
2392
|
+
replaceExisting,
|
|
2393
|
+
mainSourceDocumentId
|
|
2394
|
+
});
|
|
2395
|
+
if (!newDocument) {
|
|
2396
|
+
devLog.error("Translation failed: no document returned", new Error("Translation returned null"), logContext), setJobStatus(
|
|
3014
2397
|
dispatch,
|
|
3015
|
-
|
|
3016
|
-
schemaId,
|
|
3017
|
-
sourceDocument,
|
|
2398
|
+
sourceDocument._id,
|
|
3018
2399
|
targetLanguage,
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
excludeFields
|
|
3026
|
-
}),
|
|
3027
|
-
{
|
|
3028
|
-
maxRetries: 3,
|
|
3029
|
-
initialDelayMs: 1e3,
|
|
3030
|
-
isRetryable: isRetryableError,
|
|
3031
|
-
onRetry: (attempt, error, delayMs) => {
|
|
3032
|
-
devLog.warn("Translation failed, scheduling retry", {
|
|
3033
|
-
...logContext,
|
|
3034
|
-
attempt,
|
|
3035
|
-
delayMs,
|
|
3036
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3037
|
-
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "retrying", {
|
|
3038
|
-
retryCount: attempt,
|
|
3039
|
-
errorMessage: error instanceof Error ? error.message : String(error)
|
|
3040
|
-
});
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
);
|
|
3044
|
-
if (!translationResult.success || !translationResult.result) {
|
|
3045
|
-
const errorMessage = translationResult.error instanceof Error ? translationResult.error.message : String(translationResult.error || "Translation failed");
|
|
3046
|
-
devLog.error("Translation failed after retries", translationResult.error, {
|
|
3047
|
-
...logContext,
|
|
3048
|
-
attempts: translationResult.attempts
|
|
3049
|
-
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
3050
|
-
errorMessage,
|
|
3051
|
-
retryCount: translationResult.attempts - 1
|
|
3052
|
-
// -1 because attempts includes initial try
|
|
3053
|
-
}), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
2400
|
+
"error",
|
|
2401
|
+
{
|
|
2402
|
+
errorMessage: "Translation failed"
|
|
2403
|
+
},
|
|
2404
|
+
mainSourceDocumentId
|
|
2405
|
+
), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
3054
2406
|
return;
|
|
3055
2407
|
}
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
2408
|
+
return setJobStatus(
|
|
2409
|
+
dispatch,
|
|
2410
|
+
sourceDocument._id,
|
|
2411
|
+
targetLanguage,
|
|
2412
|
+
"updating_metadata",
|
|
2413
|
+
{
|
|
2414
|
+
targetId: newDocument._id
|
|
2415
|
+
},
|
|
2416
|
+
mainSourceDocumentId
|
|
2417
|
+
), await updateTranslationMetadata({
|
|
3060
2418
|
client,
|
|
3061
2419
|
sourceDocument,
|
|
3062
2420
|
sourceLanguage,
|
|
@@ -3064,17 +2422,31 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3064
2422
|
targetLanguage,
|
|
3065
2423
|
translationMetadata,
|
|
3066
2424
|
isNewTranslation: !existingTranslationId
|
|
3067
|
-
}), setJobStatus(
|
|
3068
|
-
|
|
3069
|
-
|
|
2425
|
+
}), setJobStatus(
|
|
2426
|
+
dispatch,
|
|
2427
|
+
sourceDocument._id,
|
|
2428
|
+
targetLanguage,
|
|
2429
|
+
"completed",
|
|
2430
|
+
{
|
|
2431
|
+
targetId: newDocument._id
|
|
2432
|
+
},
|
|
2433
|
+
mainSourceDocumentId
|
|
2434
|
+
), devLog.info("Translation completed successfully", {
|
|
3070
2435
|
...logContext,
|
|
3071
2436
|
newDocumentId: newDocument._id
|
|
3072
2437
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
3073
2438
|
} catch (error) {
|
|
3074
2439
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3075
|
-
devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(
|
|
3076
|
-
|
|
3077
|
-
|
|
2440
|
+
devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(
|
|
2441
|
+
dispatch,
|
|
2442
|
+
sourceDocument._id,
|
|
2443
|
+
targetLanguage,
|
|
2444
|
+
"error",
|
|
2445
|
+
{
|
|
2446
|
+
errorMessage
|
|
2447
|
+
},
|
|
2448
|
+
mainSourceDocumentId
|
|
2449
|
+
), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
3078
2450
|
return;
|
|
3079
2451
|
}
|
|
3080
2452
|
}, translationSemaphore = new Semaphore(2), translateDocumentSet = async ({
|
|
@@ -3083,13 +2455,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3083
2455
|
targetLanguages,
|
|
3084
2456
|
dispatch,
|
|
3085
2457
|
languageFieldName,
|
|
3086
|
-
shouldOverwriteFreshTranslations,
|
|
3087
2458
|
schemaId,
|
|
3088
2459
|
references = [],
|
|
3089
2460
|
maxConcurrentTranslationJobs,
|
|
3090
2461
|
translationMode,
|
|
3091
2462
|
openaiConfig,
|
|
3092
|
-
excludeFields
|
|
2463
|
+
excludeFields,
|
|
2464
|
+
releaseConfig
|
|
3093
2465
|
}) => {
|
|
3094
2466
|
const logContext = {
|
|
3095
2467
|
documentId: sourceDocument._id,
|
|
@@ -3098,28 +2470,48 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3098
2470
|
referencesCount: references.length,
|
|
3099
2471
|
maxConcurrentJobs: maxConcurrentTranslationJobs,
|
|
3100
2472
|
translationMode,
|
|
3101
|
-
|
|
2473
|
+
releaseMode: releaseConfig.mode
|
|
3102
2474
|
};
|
|
3103
|
-
let releaseId;
|
|
2475
|
+
let releaseId, createdNewRelease = !1;
|
|
3104
2476
|
try {
|
|
3105
2477
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
3106
2478
|
if (!sourceLanguage)
|
|
3107
2479
|
throw devLog.error("Source language not found"), new Error("Source language not found");
|
|
3108
|
-
maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
2480
|
+
if (maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
3109
2481
|
type: "SET_OVERALL_STATUS",
|
|
3110
2482
|
payload: "translating"
|
|
3111
|
-
})
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
2483
|
+
}), releaseConfig.mode === "existing" && releaseConfig.existingReleaseId)
|
|
2484
|
+
releaseId = releaseConfig.existingReleaseId, devLog.info("Using existing Content Release", {
|
|
2485
|
+
...logContext,
|
|
2486
|
+
releaseId
|
|
2487
|
+
});
|
|
2488
|
+
else {
|
|
2489
|
+
const releaseTitle = releaseConfig.newReleaseTitle, releaseDescription = `AI translation batch created on ${(/* @__PURE__ */ new Date()).toLocaleDateString()} at ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`;
|
|
2490
|
+
releaseId = (await retryWithBackoff(
|
|
2491
|
+
async () => await client.releases.create({
|
|
2492
|
+
metadata: {
|
|
2493
|
+
title: releaseTitle,
|
|
2494
|
+
description: releaseDescription,
|
|
2495
|
+
releaseType: "undecided"
|
|
2496
|
+
}
|
|
2497
|
+
}),
|
|
2498
|
+
{
|
|
2499
|
+
maxAttempts: 2,
|
|
2500
|
+
initialDelayMs: 0,
|
|
2501
|
+
retryableErrors: isNetworkError,
|
|
2502
|
+
context: {
|
|
2503
|
+
operation: "releases.create",
|
|
2504
|
+
releaseTitle,
|
|
2505
|
+
...logContext
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
)).releaseId, createdNewRelease = !0, devLog.info("Content Release created", {
|
|
2509
|
+
...logContext,
|
|
2510
|
+
releaseId,
|
|
2511
|
+
releaseTitle
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
registerActiveTranslation(sourceDocument._id, releaseId), updateActiveTranslationStatus(sourceDocument._id, "translating");
|
|
3123
2515
|
const sourceDocuments = [
|
|
3124
2516
|
{
|
|
3125
2517
|
_id: sourceDocument._id,
|
|
@@ -3134,7 +2526,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3134
2526
|
targetLanguages,
|
|
3135
2527
|
sourceDocument
|
|
3136
2528
|
});
|
|
3137
|
-
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
|
|
2529
|
+
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages, sourceDocument._id);
|
|
3138
2530
|
const translationTasks = sourceDocumentIds.flatMap(
|
|
3139
2531
|
(documentId) => targetLanguages.map(async (targetLanguage) => {
|
|
3140
2532
|
try {
|
|
@@ -3142,7 +2534,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3142
2534
|
const document = await client.withConfig({
|
|
3143
2535
|
perspective: "drafts"
|
|
3144
2536
|
}).getDocument(documentId);
|
|
3145
|
-
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document &&
|
|
2537
|
+
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId) {
|
|
3146
2538
|
const versionedTargetDocumentId = await translateDocument({
|
|
3147
2539
|
dispatch,
|
|
3148
2540
|
client,
|
|
@@ -3150,13 +2542,15 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3150
2542
|
sourceLanguage,
|
|
3151
2543
|
targetLanguage,
|
|
3152
2544
|
languageFieldName,
|
|
3153
|
-
shouldOverwriteFreshTranslations,
|
|
3154
2545
|
schemaId,
|
|
3155
2546
|
translationPrompts,
|
|
3156
2547
|
translationMode,
|
|
3157
2548
|
openaiConfig,
|
|
3158
|
-
releaseId
|
|
3159
|
-
excludeFields
|
|
2549
|
+
releaseId,
|
|
2550
|
+
excludeFields,
|
|
2551
|
+
replaceExisting: releaseConfig.replaceExisting,
|
|
2552
|
+
// Pass main source document ID for active translations tracking
|
|
2553
|
+
mainSourceDocumentId: sourceDocument._id
|
|
3160
2554
|
});
|
|
3161
2555
|
versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
|
|
3162
2556
|
}
|
|
@@ -3183,7 +2577,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3183
2577
|
}), dispatch({
|
|
3184
2578
|
type: "SET_OVERALL_STATUS",
|
|
3185
2579
|
payload: "linking_references"
|
|
3186
|
-
}), await Promise.all(
|
|
2580
|
+
}), updateActiveTranslationStatus(sourceDocument._id, "linking_references"), await Promise.all(
|
|
3187
2581
|
versionedTargetDocumentIds.map(async (documentId) => {
|
|
3188
2582
|
await localizeReferences({
|
|
3189
2583
|
client,
|
|
@@ -3194,18 +2588,65 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3194
2588
|
})
|
|
3195
2589
|
);
|
|
3196
2590
|
const hasAnyFailures = failed > 0;
|
|
3197
|
-
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", {
|
|
2591
|
+
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", {
|
|
3198
2592
|
...logContext,
|
|
3199
2593
|
succeeded,
|
|
3200
2594
|
failed
|
|
3201
|
-
}), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
|
|
2595
|
+
}), 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();
|
|
3202
2596
|
} catch (error) {
|
|
3203
|
-
|
|
2597
|
+
if (devLog.error(
|
|
3204
2598
|
"translateDocumentSet failed with unexpected error",
|
|
3205
2599
|
error instanceof Error ? error : new Error(String(error)),
|
|
3206
2600
|
logContext
|
|
3207
|
-
),
|
|
3208
|
-
|
|
2601
|
+
), releaseId && createdNewRelease)
|
|
2602
|
+
try {
|
|
2603
|
+
await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
|
|
2604
|
+
...logContext,
|
|
2605
|
+
releaseId
|
|
2606
|
+
});
|
|
2607
|
+
} catch (cleanupError) {
|
|
2608
|
+
devLog.error(
|
|
2609
|
+
"Failed to cleanup release",
|
|
2610
|
+
cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
|
|
2611
|
+
logContext
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id), error;
|
|
2615
|
+
}
|
|
2616
|
+
}, ReleaseConflictWarning = ({ conflicts, onCancel, onReplace }) => {
|
|
2617
|
+
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]);
|
|
2618
|
+
return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2619
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
2620
|
+
/* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
|
|
2621
|
+
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "This release already contains translations for this content and target language(s)." })
|
|
2622
|
+
] }),
|
|
2623
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2624
|
+
"The selected release already has ",
|
|
2625
|
+
summaryText,
|
|
2626
|
+
" that will be overwritten if you continue:"
|
|
2627
|
+
] }),
|
|
2628
|
+
/* @__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: [
|
|
2629
|
+
conflict.sourceDocumentTitle || conflict.sourceDocumentId,
|
|
2630
|
+
conflict.sourceDocumentType && ` (${conflict.sourceDocumentType})`,
|
|
2631
|
+
" \u2192 ",
|
|
2632
|
+
conflict.targetLanguage
|
|
2633
|
+
] }, conflict.existingVersionId)) }) }) }),
|
|
2634
|
+
/* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2635
|
+
/* @__PURE__ */ jsx(Heading, { size: 0, children: "Choose an action:" }),
|
|
2636
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2637
|
+
/* @__PURE__ */ jsx("strong", { children: "Cancel" }),
|
|
2638
|
+
" \u2014 Go back and select a different release, or create a new one."
|
|
2639
|
+
] }),
|
|
2640
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2641
|
+
/* @__PURE__ */ jsx("strong", { children: "Replace" }),
|
|
2642
|
+
" \u2014 Delete the existing translations in this release and create new ones."
|
|
2643
|
+
] })
|
|
2644
|
+
] }),
|
|
2645
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", children: [
|
|
2646
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
|
|
2647
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "caution", text: "Replace Existing", onClick: onReplace })
|
|
2648
|
+
] })
|
|
2649
|
+
] }) });
|
|
3209
2650
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
3210
2651
|
const {
|
|
3211
2652
|
document,
|
|
@@ -3214,39 +2655,81 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3214
2655
|
dispatch,
|
|
3215
2656
|
references,
|
|
3216
2657
|
selectedTargetLanguages,
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }),
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
2658
|
+
translationMode,
|
|
2659
|
+
releaseConfig
|
|
2660
|
+
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1);
|
|
2661
|
+
useEffect(() => {
|
|
2662
|
+
releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
|
|
2663
|
+
}, [releaseConfig.mode, conflictContext]);
|
|
2664
|
+
const startTranslation = useCallback(
|
|
2665
|
+
(replaceExisting, overrideReleaseId) => {
|
|
2666
|
+
setConflictContext(null), translateDocumentSet({
|
|
2667
|
+
sourceDocument: document,
|
|
2668
|
+
targetLanguages: selectedTargetLanguages,
|
|
2669
|
+
client,
|
|
2670
|
+
dispatch,
|
|
2671
|
+
languageFieldName: config.languageFieldName,
|
|
2672
|
+
schemaId: config.schemaId,
|
|
2673
|
+
references,
|
|
2674
|
+
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
2675
|
+
maxReferenceDepth: config.maxReferenceDepth,
|
|
2676
|
+
translationMode,
|
|
2677
|
+
openaiConfig: config.openai,
|
|
2678
|
+
excludeFields: config.excludeFields,
|
|
2679
|
+
releaseConfig: {
|
|
2680
|
+
...releaseConfig,
|
|
2681
|
+
replaceExisting,
|
|
2682
|
+
// Use the override release ID if provided (from conflict context),
|
|
2683
|
+
// otherwise use the current releaseConfig value
|
|
2684
|
+
existingReleaseId: overrideReleaseId ?? releaseConfig.existingReleaseId
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
},
|
|
2688
|
+
[
|
|
2689
|
+
document,
|
|
2690
|
+
selectedTargetLanguages,
|
|
3223
2691
|
client,
|
|
3224
2692
|
dispatch,
|
|
3225
|
-
|
|
3226
|
-
|
|
2693
|
+
config.languageFieldName,
|
|
2694
|
+
config.schemaId,
|
|
2695
|
+
config.maxConcurrentTranslationJobs,
|
|
2696
|
+
config.maxReferenceDepth,
|
|
2697
|
+
config.openai,
|
|
2698
|
+
config.excludeFields,
|
|
3227
2699
|
references,
|
|
3228
|
-
shouldOverwriteFreshTranslations,
|
|
3229
|
-
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
3230
|
-
maxReferenceDepth: config.maxReferenceDepth,
|
|
3231
2700
|
translationMode,
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
2701
|
+
releaseConfig
|
|
2702
|
+
]
|
|
2703
|
+
), handleTranslate = useCallback(async () => {
|
|
2704
|
+
if (releaseConfig.mode === "new") {
|
|
2705
|
+
startTranslation(!1);
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
if (releaseConfig.mode === "existing" && releaseConfig.existingReleaseId) {
|
|
2709
|
+
const checkedReleaseId = releaseConfig.existingReleaseId;
|
|
2710
|
+
setIsCheckingConflicts(!0);
|
|
2711
|
+
const foundConflicts = await checkReleaseConflicts({
|
|
2712
|
+
client,
|
|
2713
|
+
releaseId: checkedReleaseId,
|
|
2714
|
+
sourceDocument: document,
|
|
2715
|
+
targetLanguages: selectedTargetLanguages,
|
|
2716
|
+
languageFieldName: config.languageFieldName,
|
|
2717
|
+
references
|
|
2718
|
+
});
|
|
2719
|
+
if (setIsCheckingConflicts(!1), foundConflicts.length > 0) {
|
|
2720
|
+
setConflictContext({
|
|
2721
|
+
releaseId: checkedReleaseId,
|
|
2722
|
+
conflicts: foundConflicts
|
|
2723
|
+
});
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
startTranslation(!1);
|
|
2728
|
+
}, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, references]), handleCancelConflict = useCallback(() => {
|
|
2729
|
+
setConflictContext(null);
|
|
2730
|
+
}, []), handleReplaceConflict = useCallback(() => {
|
|
2731
|
+
conflictContext && startTranslation(!0, conflictContext.releaseId);
|
|
2732
|
+
}, [startTranslation, conflictContext]), handleSelectAll = useCallback(() => {
|
|
3250
2733
|
dispatch({
|
|
3251
2734
|
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
3252
2735
|
payload: config.supportedLanguages.map((lang) => lang.id).filter((id) => id !== document.language)
|
|
@@ -3257,7 +2740,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3257
2740
|
payload: []
|
|
3258
2741
|
});
|
|
3259
2742
|
}, [dispatch]);
|
|
3260
|
-
return /* @__PURE__ */
|
|
2743
|
+
return conflictContext ? /* @__PURE__ */ jsx(Stack, { space: 4, children: /* @__PURE__ */ jsx(
|
|
2744
|
+
ReleaseConflictWarning,
|
|
2745
|
+
{
|
|
2746
|
+
conflicts: conflictContext.conflicts,
|
|
2747
|
+
onCancel: handleCancelConflict,
|
|
2748
|
+
onReplace: handleReplaceConflict
|
|
2749
|
+
}
|
|
2750
|
+
) }) : /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
|
|
3261
2751
|
/* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
|
|
3262
2752
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectAll, children: "Select All" }),
|
|
3263
2753
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectNone, children: "Select None" })
|
|
@@ -3271,16 +2761,41 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3271
2761
|
tone: "positive",
|
|
3272
2762
|
size: 2,
|
|
3273
2763
|
onClick: handleTranslate,
|
|
3274
|
-
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus),
|
|
3275
|
-
children: "Translate"
|
|
2764
|
+
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus) || releaseConfig.mode === "existing" && !releaseConfig.existingReleaseId || isCheckingConflicts,
|
|
2765
|
+
children: isCheckingConflicts ? "Checking..." : "Translate"
|
|
3276
2766
|
}
|
|
3277
2767
|
)
|
|
3278
2768
|
] })
|
|
3279
2769
|
] });
|
|
2770
|
+
}, TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
|
|
2771
|
+
const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
|
|
2772
|
+
params: {
|
|
2773
|
+
documentId: undraftId(documentId),
|
|
2774
|
+
translationMetadataType: TRANSLATION_METADATA_TYPE
|
|
2775
|
+
},
|
|
2776
|
+
initialValue: null
|
|
2777
|
+
});
|
|
2778
|
+
return useEffect(() => {
|
|
2779
|
+
if (loading) {
|
|
2780
|
+
dispatch({ type: "SET_OVERALL_STATUS", payload: "fetching_metadata" });
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
if (error) {
|
|
2784
|
+
dispatch({ type: "SET_OVERALL_STATUS", payload: "error" });
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
if (data) {
|
|
2788
|
+
dispatch({ type: "SET_TRANSLATION_METADATA", payload: data }), dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
|
|
2792
|
+
}, [data, loading, error, dispatch]), {
|
|
2793
|
+
data,
|
|
2794
|
+
loading,
|
|
2795
|
+
error
|
|
2796
|
+
};
|
|
3280
2797
|
}, LanguageCheckbox = ({ lang, isLoading }) => {
|
|
3281
|
-
const { document, dispatch, selectedTargetLanguages
|
|
3282
|
-
(history) => history.language === lang.id && history.sourceRev !== document._rev
|
|
3283
|
-
), handleChange = () => {
|
|
2798
|
+
const { document, dispatch, selectedTargetLanguages } = useTranslationContext(), isDisabled = lang.id === document.language, isChecked = selectedTargetLanguages.includes(lang.id), handleChange = () => {
|
|
3284
2799
|
if (isDisabled) return;
|
|
3285
2800
|
const updatedLanguages = isChecked ? selectedTargetLanguages.filter((langId) => langId !== lang.id) : [...selectedTargetLanguages, lang.id];
|
|
3286
2801
|
dispatch({
|
|
@@ -3298,16 +2813,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3298
2813
|
disabled: isDisabled,
|
|
3299
2814
|
tone: isChecked ? "primary" : "default",
|
|
3300
2815
|
muted: !0,
|
|
3301
|
-
children: /* @__PURE__ */
|
|
3302
|
-
/* @__PURE__ */
|
|
3303
|
-
|
|
3304
|
-
/* @__PURE__ */
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
] }),
|
|
3309
|
-
!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" }) })
|
|
3310
|
-
] })
|
|
2816
|
+
children: /* @__PURE__ */ jsx(Flex, { align: "center", gap: 2, style: { cursor: "pointer" }, children: /* @__PURE__ */ jsxs(Inline, { space: 3, children: [
|
|
2817
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: isChecked, onChange: handleChange, label: lang.title, disabled: isDisabled }),
|
|
2818
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
2819
|
+
/* @__PURE__ */ jsx(Text, { children: lang.title }),
|
|
2820
|
+
isDisabled && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "The current language" })
|
|
2821
|
+
] })
|
|
2822
|
+
] }) })
|
|
3311
2823
|
}
|
|
3312
2824
|
);
|
|
3313
2825
|
}, LanguageSelectionPanel = () => {
|
|
@@ -3315,6 +2827,134 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3315
2827
|
documentId: document._id
|
|
3316
2828
|
});
|
|
3317
2829
|
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)) }) });
|
|
2830
|
+
}, ReleaseSelectionPanel = () => {
|
|
2831
|
+
const { dispatch, releaseConfig } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [releases, setReleases] = useState([]), [isLoading, setIsLoading] = useState(!1), [error, setError] = useState(null), [searchQuery, setSearchQuery] = useState("");
|
|
2832
|
+
useEffect(() => {
|
|
2833
|
+
(async () => {
|
|
2834
|
+
setIsLoading(!0), setError(null);
|
|
2835
|
+
try {
|
|
2836
|
+
const result = await client.fetch(
|
|
2837
|
+
'releases::all()[state == "active"] | order(_createdAt desc)'
|
|
2838
|
+
);
|
|
2839
|
+
setReleases(result), devLog.debug("Fetched active releases", {
|
|
2840
|
+
count: result.length,
|
|
2841
|
+
component: "ReleaseSelectionPanel"
|
|
2842
|
+
});
|
|
2843
|
+
} catch (err) {
|
|
2844
|
+
const errorMessage = err instanceof Error ? err.message : "Failed to fetch releases";
|
|
2845
|
+
setError(errorMessage), devLog.error("Failed to fetch releases", err instanceof Error ? err : new Error(errorMessage), {
|
|
2846
|
+
component: "ReleaseSelectionPanel"
|
|
2847
|
+
});
|
|
2848
|
+
} finally {
|
|
2849
|
+
setIsLoading(!1);
|
|
2850
|
+
}
|
|
2851
|
+
})();
|
|
2852
|
+
}, [client]);
|
|
2853
|
+
const handleModeChange = useCallback(
|
|
2854
|
+
(mode) => {
|
|
2855
|
+
dispatch({
|
|
2856
|
+
type: "SET_RELEASE_CONFIG",
|
|
2857
|
+
payload: { mode }
|
|
2858
|
+
});
|
|
2859
|
+
},
|
|
2860
|
+
[dispatch]
|
|
2861
|
+
), handleTitleChange = useCallback(
|
|
2862
|
+
(event) => {
|
|
2863
|
+
dispatch({
|
|
2864
|
+
type: "SET_RELEASE_CONFIG",
|
|
2865
|
+
payload: { newReleaseTitle: event.target.value }
|
|
2866
|
+
});
|
|
2867
|
+
},
|
|
2868
|
+
[dispatch]
|
|
2869
|
+
), handleSelectRelease = useCallback(
|
|
2870
|
+
(releaseId) => {
|
|
2871
|
+
dispatch({
|
|
2872
|
+
type: "SET_RELEASE_CONFIG",
|
|
2873
|
+
payload: { existingReleaseId: releaseId }
|
|
2874
|
+
});
|
|
2875
|
+
},
|
|
2876
|
+
[dispatch]
|
|
2877
|
+
), handleSearchChange = useCallback((event) => {
|
|
2878
|
+
setSearchQuery(event.target.value);
|
|
2879
|
+
}, []), filteredReleases = useMemo(() => {
|
|
2880
|
+
if (!searchQuery.trim())
|
|
2881
|
+
return releases;
|
|
2882
|
+
const query = searchQuery.toLowerCase();
|
|
2883
|
+
return releases.filter((release) => release.metadata.title.toLowerCase().includes(query));
|
|
2884
|
+
}, [releases, searchQuery]), formatDate = (dateString) => new Date(dateString).toLocaleDateString(void 0, {
|
|
2885
|
+
month: "short",
|
|
2886
|
+
day: "numeric",
|
|
2887
|
+
year: "numeric"
|
|
2888
|
+
});
|
|
2889
|
+
return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2890
|
+
/* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
2891
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, as: "label", style: { cursor: "pointer" }, onClick: () => handleModeChange("new"), children: [
|
|
2892
|
+
/* @__PURE__ */ jsx(Radio, { checked: releaseConfig.mode === "new", readOnly: !0 }),
|
|
2893
|
+
/* @__PURE__ */ jsx(Text, { weight: "medium", children: "Create new release" })
|
|
2894
|
+
] }),
|
|
2895
|
+
/* @__PURE__ */ jsxs(
|
|
2896
|
+
Flex,
|
|
2897
|
+
{
|
|
2898
|
+
align: "center",
|
|
2899
|
+
gap: 3,
|
|
2900
|
+
as: "label",
|
|
2901
|
+
style: { cursor: "pointer" },
|
|
2902
|
+
onClick: () => handleModeChange("existing"),
|
|
2903
|
+
children: [
|
|
2904
|
+
/* @__PURE__ */ jsx(Radio, { checked: releaseConfig.mode === "existing", readOnly: !0 }),
|
|
2905
|
+
/* @__PURE__ */ jsx(Text, { weight: "medium", children: "Add to existing release" })
|
|
2906
|
+
]
|
|
2907
|
+
}
|
|
2908
|
+
)
|
|
2909
|
+
] }),
|
|
2910
|
+
releaseConfig.mode === "new" && /* @__PURE__ */ jsx(
|
|
2911
|
+
TextInput,
|
|
2912
|
+
{
|
|
2913
|
+
value: releaseConfig.newReleaseTitle,
|
|
2914
|
+
onChange: handleTitleChange,
|
|
2915
|
+
placeholder: "Release title..."
|
|
2916
|
+
}
|
|
2917
|
+
),
|
|
2918
|
+
releaseConfig.mode === "existing" && /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
2919
|
+
isLoading && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
2920
|
+
/* @__PURE__ */ jsx(Spinner, { muted: !0 }),
|
|
2921
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Loading..." })
|
|
2922
|
+
] }),
|
|
2923
|
+
error && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: error }),
|
|
2924
|
+
!isLoading && !error && releases.length === 0 && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "No active releases found." }),
|
|
2925
|
+
!isLoading && !error && releases.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2926
|
+
/* @__PURE__ */ jsx(
|
|
2927
|
+
TextInput,
|
|
2928
|
+
{
|
|
2929
|
+
icon: SearchIcon,
|
|
2930
|
+
value: searchQuery,
|
|
2931
|
+
onChange: handleSearchChange,
|
|
2932
|
+
placeholder: "Search releases..."
|
|
2933
|
+
}
|
|
2934
|
+
),
|
|
2935
|
+
/* @__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) => {
|
|
2936
|
+
const releaseName = release._id.replace("_.releases.", ""), isSelected = releaseConfig.existingReleaseId === releaseName;
|
|
2937
|
+
return /* @__PURE__ */ jsx(
|
|
2938
|
+
Card,
|
|
2939
|
+
{
|
|
2940
|
+
padding: 3,
|
|
2941
|
+
radius: 2,
|
|
2942
|
+
tone: isSelected ? "primary" : "default",
|
|
2943
|
+
style: { cursor: "pointer" },
|
|
2944
|
+
onClick: () => handleSelectRelease(releaseName),
|
|
2945
|
+
border: !0,
|
|
2946
|
+
muted: isSelected,
|
|
2947
|
+
children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
2948
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: release.metadata.title }),
|
|
2949
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: formatDate(release._createdAt) })
|
|
2950
|
+
] })
|
|
2951
|
+
},
|
|
2952
|
+
release._id
|
|
2953
|
+
);
|
|
2954
|
+
}) }) })
|
|
2955
|
+
] })
|
|
2956
|
+
] })
|
|
2957
|
+
] }) });
|
|
3318
2958
|
}, TranslationModePanel = () => {
|
|
3319
2959
|
const { config, translationMode, openaiValidated, dispatch } = useTranslationContext();
|
|
3320
2960
|
if (!config?.openai?.enabled || !config?.openai?.apiKey)
|
|
@@ -3333,7 +2973,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3333
2973
|
return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3334
2974
|
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3335
2975
|
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "Translation Mode" }),
|
|
3336
|
-
/* @__PURE__ */ jsx(Badge, { tone: "positive", children: "
|
|
2976
|
+
/* @__PURE__ */ jsx(Badge, { tone: "positive", children: "OpenAI Available" })
|
|
3337
2977
|
] }),
|
|
3338
2978
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3339
2979
|
/* @__PURE__ */ jsx(
|
|
@@ -3363,32 +3003,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3363
3003
|
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "openai-only", readOnly: !0 }),
|
|
3364
3004
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3365
3005
|
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3366
|
-
/* @__PURE__ */ jsx(Text, { children: "
|
|
3006
|
+
/* @__PURE__ */ jsx(Text, { children: "OpenAI Only" }),
|
|
3367
3007
|
/* @__PURE__ */ jsx(Badge, { tone: "caution", children: "Beta" })
|
|
3368
3008
|
] }),
|
|
3369
3009
|
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Full style guide capacity with 1M token context window." })
|
|
3370
3010
|
] })
|
|
3371
3011
|
] })
|
|
3372
3012
|
}
|
|
3373
|
-
),
|
|
3374
|
-
/* @__PURE__ */ jsx(
|
|
3375
|
-
Button,
|
|
3376
|
-
{
|
|
3377
|
-
type: "button",
|
|
3378
|
-
mode: translationMode === "sanity-with-review" ? "ghost" : "bleed",
|
|
3379
|
-
tone: translationMode === "sanity-with-review" ? "primary" : "default",
|
|
3380
|
-
onClick: () => handleModeChange("sanity-with-review"),
|
|
3381
|
-
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3382
|
-
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "sanity-with-review", readOnly: !0 }),
|
|
3383
|
-
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3384
|
-
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3385
|
-
/* @__PURE__ */ jsx(Text, { children: "Sanity + GPT-5 Review" }),
|
|
3386
|
-
/* @__PURE__ */ jsx(Badge, { tone: "primary", children: "Highest Quality" })
|
|
3387
|
-
] }),
|
|
3388
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Sanity translation refined by GPT-5. Takes longer but best results." })
|
|
3389
|
-
] })
|
|
3390
|
-
] })
|
|
3391
|
-
}
|
|
3392
3013
|
)
|
|
3393
3014
|
] }),
|
|
3394
3015
|
translationMode !== "sanity-only" && /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 3, border: !0, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
@@ -3551,41 +3172,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3551
3172
|
] }),
|
|
3552
3173
|
shouldTranslateReferences ? /* @__PURE__ */ jsx(Flex, { padding: 2, justify: "flex-end", children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: isLoadingReferences ? "Loading references..." : references.length === 0 ? "No translatable references found." : `${references.length} translatable references found.` }) }) : null
|
|
3553
3174
|
] });
|
|
3554
|
-
}, WriteSettingsPanel = () => {
|
|
3555
|
-
const { dispatch, shouldOverwriteFreshTranslations } = useTranslationContext(), handleShouldOverwriteFreshTranslationsChange = () => {
|
|
3556
|
-
dispatch({
|
|
3557
|
-
type: "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS",
|
|
3558
|
-
payload: !shouldOverwriteFreshTranslations
|
|
3559
|
-
});
|
|
3560
|
-
};
|
|
3561
|
-
return /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3562
|
-
/* @__PURE__ */ jsx(ReferenceConfigPanel, {}),
|
|
3563
|
-
/* @__PURE__ */ jsx(Flex, { justify: "space-between", align: "center", children: /* @__PURE__ */ jsx(
|
|
3564
|
-
Button,
|
|
3565
|
-
{
|
|
3566
|
-
type: "button",
|
|
3567
|
-
mode: shouldOverwriteFreshTranslations ? "ghost" : "bleed",
|
|
3568
|
-
size: 2,
|
|
3569
|
-
onClick: handleShouldOverwriteFreshTranslationsChange,
|
|
3570
|
-
tone: shouldOverwriteFreshTranslations ? "primary" : "neutral",
|
|
3571
|
-
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3572
|
-
/* @__PURE__ */ jsx(
|
|
3573
|
-
Checkbox,
|
|
3574
|
-
{
|
|
3575
|
-
checked: shouldOverwriteFreshTranslations,
|
|
3576
|
-
onChange: handleShouldOverwriteFreshTranslationsChange,
|
|
3577
|
-
label: shouldOverwriteFreshTranslations ? "Disable" : "Enable"
|
|
3578
|
-
}
|
|
3579
|
-
),
|
|
3580
|
-
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3581
|
-
/* @__PURE__ */ jsx(Text, { children: "Overwrite Fresh Translations" }),
|
|
3582
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "All existing translations will be overwritten even if they are fresh." })
|
|
3583
|
-
] })
|
|
3584
|
-
] })
|
|
3585
|
-
}
|
|
3586
|
-
) })
|
|
3587
|
-
] });
|
|
3588
|
-
}, TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
3175
|
+
}, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
3589
3176
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3590
3177
|
/* @__PURE__ */ jsx(Label, { children: "Languages" }),
|
|
3591
3178
|
/* @__PURE__ */ jsx(LanguageSelectionPanel, {})
|
|
@@ -3594,16 +3181,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3594
3181
|
/* @__PURE__ */ jsx(Label, { children: "Translation Method" }),
|
|
3595
3182
|
/* @__PURE__ */ jsx(TranslationModePanel, {})
|
|
3596
3183
|
] }),
|
|
3184
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3185
|
+
/* @__PURE__ */ jsx(Label, { children: "Content Release" }),
|
|
3186
|
+
/* @__PURE__ */ jsx(ReleaseSelectionPanel, {})
|
|
3187
|
+
] }),
|
|
3597
3188
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3598
3189
|
/* @__PURE__ */ jsx(Label, { children: "Settings" }),
|
|
3599
3190
|
/* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsx(WriteSettingsPanel, {}) })
|
|
3600
3191
|
] }),
|
|
3601
3192
|
/* @__PURE__ */ jsx(ActionButtonBar, { setIsDialogOpen })
|
|
3602
3193
|
] }), TERMINAL_STATUSES = ["completed", "skipped", "error"], TranslationStatusPanel = () => {
|
|
3603
|
-
const { overallStatus, translationJobs, translationMode } = useTranslationContext(), headline = useMemo(
|
|
3604
|
-
()
|
|
3605
|
-
|
|
3606
|
-
), jobs = useMemo(() => Array.from(translationJobs.values()), [translationJobs]), openaiStats = useMemo(() => {
|
|
3194
|
+
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(() => {
|
|
3195
|
+
forceCleanupActiveTranslation(document._id), dispatch({ type: "RESET_TRANSLATION_STATE" });
|
|
3196
|
+
}, [dispatch, document._id]), openaiStats = useMemo(() => {
|
|
3607
3197
|
if (translationMode === "sanity-only") return null;
|
|
3608
3198
|
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);
|
|
3609
3199
|
return { totalTokens, totalCost, jobsWithUsage: completedJobs.filter((j) => j.tokensUsed).length };
|
|
@@ -3637,17 +3227,12 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3637
3227
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3638
3228
|
"Error: ",
|
|
3639
3229
|
job.errorMessage
|
|
3640
|
-
] }) }) : null,
|
|
3641
|
-
job.status === "retrying" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3642
|
-
"Retrying after: ",
|
|
3643
|
-
job.errorMessage
|
|
3644
3230
|
] }) }) : null
|
|
3645
3231
|
] })
|
|
3646
3232
|
] }),
|
|
3647
3233
|
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
3648
3234
|
job.status === "skipped" ? /* @__PURE__ */ jsx(SkippedBadge, {}) : null,
|
|
3649
|
-
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {
|
|
3650
|
-
job.status === "retrying" ? /* @__PURE__ */ jsx(RetryingBadge, { retryCount: job.retryCount }) : null,
|
|
3235
|
+
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {}) : null,
|
|
3651
3236
|
job.targetId ? /* @__PURE__ */ jsx(
|
|
3652
3237
|
TranslationOpenerButton,
|
|
3653
3238
|
{
|
|
@@ -3664,18 +3249,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3664
3249
|
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Usage Summary" }),
|
|
3665
3250
|
/* @__PURE__ */ jsxs(Inline, { space: 4, children: [
|
|
3666
3251
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3667
|
-
"
|
|
3252
|
+
"Total Tokens: ",
|
|
3668
3253
|
/* @__PURE__ */ jsx("strong", { children: openaiStats.totalTokens.toLocaleString() })
|
|
3669
3254
|
] }),
|
|
3670
3255
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3671
|
-
"
|
|
3256
|
+
"Total Cost: ",
|
|
3672
3257
|
/* @__PURE__ */ jsxs("strong", { children: [
|
|
3673
3258
|
"$",
|
|
3674
3259
|
openaiStats.totalCost.toFixed(4)
|
|
3675
3260
|
] })
|
|
3676
3261
|
] })
|
|
3677
3262
|
] })
|
|
3678
|
-
] }) })
|
|
3263
|
+
] }) }),
|
|
3264
|
+
isTerminalState && /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "primary", text: "Translate More", icon: ResetIcon, onClick: handleReset }) })
|
|
3679
3265
|
] });
|
|
3680
3266
|
}, ProgressBar = ({ progress, max = 100 }) => {
|
|
3681
3267
|
const percentage = Math.min(progress / max * 100, 100);
|
|
@@ -3727,35 +3313,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3727
3313
|
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." }) }),
|
|
3728
3314
|
children: /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: "Skipped" })
|
|
3729
3315
|
}
|
|
3730
|
-
), ErrorBadge = (
|
|
3731
|
-
Tooltip,
|
|
3732
|
-
{
|
|
3733
|
-
portal: !0,
|
|
3734
|
-
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." }) }),
|
|
3735
|
-
children: /* @__PURE__ */ jsxs(Badge, { tone: "critical", children: [
|
|
3736
|
-
"Failed",
|
|
3737
|
-
retryCount && retryCount > 0 ? ` (${retryCount} retries)` : ""
|
|
3738
|
-
] })
|
|
3739
|
-
}
|
|
3740
|
-
), RetryingBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
|
|
3316
|
+
), ErrorBadge = () => /* @__PURE__ */ jsx(
|
|
3741
3317
|
Tooltip,
|
|
3742
3318
|
{
|
|
3743
3319
|
portal: !0,
|
|
3744
|
-
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "
|
|
3745
|
-
children: /* @__PURE__ */
|
|
3746
|
-
"Retry ",
|
|
3747
|
-
retryCount || 1
|
|
3748
|
-
] })
|
|
3320
|
+
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "This translation failed. Check the error message for details." }) }),
|
|
3321
|
+
children: /* @__PURE__ */ jsx(Badge, { tone: "critical", children: "Failed" })
|
|
3749
3322
|
}
|
|
3750
3323
|
), getStatusLabel = (status) => ({
|
|
3751
3324
|
idle: "Idle",
|
|
3752
3325
|
queued: "Queued...",
|
|
3753
3326
|
preparing_translation: "Preparing translation...",
|
|
3754
3327
|
translating: "Translating with Sanity Agent...",
|
|
3755
|
-
translating_with_openai: "Translating with
|
|
3756
|
-
reviewing_with_openai: "Reviewing translation with GPT-5...",
|
|
3328
|
+
translating_with_openai: "Translating with OpenAI...",
|
|
3757
3329
|
updating_metadata: "Saving metadata...",
|
|
3758
|
-
retrying: "Retrying...",
|
|
3759
3330
|
error: "Error",
|
|
3760
3331
|
skipped: "Skipped",
|
|
3761
3332
|
completed: "Completed"
|
|
@@ -3763,14 +3334,30 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3763
3334
|
"translating",
|
|
3764
3335
|
"linking_references",
|
|
3765
3336
|
"completed"
|
|
3766
|
-
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
|
|
3337
|
+
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen, onStatusChange }) => {
|
|
3767
3338
|
const { overallStatus, document } = useTranslationContext();
|
|
3768
|
-
return
|
|
3339
|
+
return useEffect(() => {
|
|
3340
|
+
onStatusChange?.(overallStatus);
|
|
3341
|
+
}, [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", {
|
|
3769
3342
|
documentId: document._id,
|
|
3770
3343
|
overallStatus
|
|
3771
3344
|
}), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
|
|
3772
|
-
}, getTranslateAction = (config) => ({ draft, published }) => {
|
|
3773
|
-
const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1)
|
|
3345
|
+
}, IN_PROGRESS_STATUSES = ["translating", "linking_references"], getButtonLabel = (status) => status === "completed" ? "Translation Complete" : status && IN_PROGRESS_STATUSES.includes(status) ? "Translating..." : "Translate", getTranslateAction = (config) => ({ draft, published }) => {
|
|
3346
|
+
const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1), [currentStatus, setCurrentStatus] = useState(null), isTranslating = currentStatus !== null && IN_PROGRESS_STATUSES.includes(currentStatus), handleStatusChange = useCallback((status) => {
|
|
3347
|
+
setCurrentStatus(status);
|
|
3348
|
+
}, []), handleClose = useCallback(() => {
|
|
3349
|
+
if (isTranslating) {
|
|
3350
|
+
if (!window.confirm(
|
|
3351
|
+
"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."
|
|
3352
|
+
))
|
|
3353
|
+
return;
|
|
3354
|
+
devLog.warn("User closed dialog during active translation", {
|
|
3355
|
+
documentId: document?._id,
|
|
3356
|
+
component: "TranslateAction"
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3359
|
+
setIsDialogOpen(!1);
|
|
3360
|
+
}, [isTranslating, document?._id]);
|
|
3774
3361
|
return devLog.debug("TranslateAction initialized", {
|
|
3775
3362
|
documentId: document?._id,
|
|
3776
3363
|
documentType: document?._type
|
|
@@ -3778,7 +3365,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3778
3365
|
documentType: document?._type,
|
|
3779
3366
|
translatableTypes: config.translatableSchemaTypes
|
|
3780
3367
|
}), null) : config ? {
|
|
3781
|
-
label:
|
|
3368
|
+
label: getButtonLabel(currentStatus),
|
|
3782
3369
|
icon: TranslateIcon,
|
|
3783
3370
|
onHandle: () => {
|
|
3784
3371
|
devLog.info("Translation dialog opened", {
|
|
@@ -3789,9 +3376,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3789
3376
|
},
|
|
3790
3377
|
dialog: isDialogOpen && {
|
|
3791
3378
|
type: "dialog",
|
|
3792
|
-
onClose:
|
|
3379
|
+
onClose: handleClose,
|
|
3793
3380
|
header: "Translate Document",
|
|
3794
|
-
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
|
|
3381
|
+
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen, onStatusChange: handleStatusChange }) })
|
|
3795
3382
|
}
|
|
3796
3383
|
} : (devLog.error("Missing config in TranslateAction"), null);
|
|
3797
3384
|
}, translationGlossaryTerm = defineType({
|
|
@@ -3907,7 +3494,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3907
3494
|
defineField({
|
|
3908
3495
|
name: "changeHistory",
|
|
3909
3496
|
title: "Change History",
|
|
3910
|
-
description: "History of changes made
|
|
3497
|
+
description: "History of changes made to translation settings.",
|
|
3911
3498
|
type: "array",
|
|
3912
3499
|
readOnly: !0,
|
|
3913
3500
|
group: "history",
|
|
@@ -4217,7 +3804,7 @@ const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.le
|
|
|
4217
3804
|
})), {
|
|
4218
3805
|
name: "sanity-plugin-ai-translation",
|
|
4219
3806
|
document: {
|
|
4220
|
-
actions: [getTranslateAction(config)
|
|
3807
|
+
actions: [getTranslateAction(config)]
|
|
4221
3808
|
},
|
|
4222
3809
|
schema: {
|
|
4223
3810
|
types: [translationSettings, translationGlossaryTerm]
|