@braze/sanity-plugin-ai-translation 0.2.39 → 0.2.52
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 +52 -4
- package/dist/index.js +1371 -1509
- 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,65 @@ 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), jobWithDefinedValues = Object.fromEntries(
|
|
84
|
+
Object.entries(job).filter(([_, value]) => value !== void 0)
|
|
85
|
+
), mergedJob = existingJob ? { ...existingJob, ...jobWithDefinedValues } : jobWithDefinedValues;
|
|
86
|
+
entry.state.translationJobs.set(jobKey, mergedJob), notifySubscribers(entry);
|
|
87
|
+
}
|
|
88
|
+
}, getActiveTranslation = (documentId) => activeTranslations.get(documentId)?.state ?? null, subscribeToActiveTranslation = (documentId, callback) => {
|
|
89
|
+
const entry = activeTranslations.get(documentId);
|
|
90
|
+
return entry ? (entry.subscribers.add(callback), callback(entry.state), () => {
|
|
91
|
+
entry.subscribers.delete(callback);
|
|
92
|
+
}) : () => {
|
|
93
|
+
};
|
|
94
|
+
}, cleanupActiveTranslation = (documentId, delayMs = 6e4) => {
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
const entry = activeTranslations.get(documentId);
|
|
97
|
+
entry && ["completed", "error"].includes(entry.state.overallStatus) && activeTranslations.delete(documentId);
|
|
98
|
+
}, delayMs);
|
|
99
|
+
}, notifySubscribers = (entry) => {
|
|
100
|
+
for (const subscriber of entry.subscribers)
|
|
101
|
+
try {
|
|
102
|
+
subscriber(entry.state);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error("Error in active translation subscriber:", error);
|
|
105
|
+
}
|
|
106
|
+
}, forceCleanupActiveTranslation = (documentId) => {
|
|
107
|
+
activeTranslations.delete(documentId);
|
|
65
108
|
}, TranslationContext = createContext({
|
|
66
109
|
document: null,
|
|
67
110
|
config: null,
|
|
68
111
|
overallStatus: "idle",
|
|
69
112
|
selectedTargetLanguages: [],
|
|
70
113
|
translationJobs: /* @__PURE__ */ new Map(),
|
|
71
|
-
shouldOverwriteFreshTranslations: !1,
|
|
72
114
|
shouldTranslateReferences: !1,
|
|
73
115
|
maxReferenceDepth: 1,
|
|
74
116
|
references: [],
|
|
75
117
|
translationMetadata: null,
|
|
76
118
|
translationMode: "sanity-only",
|
|
77
119
|
openaiValidated: !1,
|
|
120
|
+
releaseConfig: {
|
|
121
|
+
mode: "new",
|
|
122
|
+
newReleaseTitle: ""
|
|
123
|
+
},
|
|
78
124
|
dispatch: () => {
|
|
79
125
|
}
|
|
80
126
|
}), translationReducer = (context, action) => {
|
|
@@ -85,7 +131,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
85
131
|
};
|
|
86
132
|
switch (devLog.debug("Translation action dispatched", {
|
|
87
133
|
...logContext,
|
|
88
|
-
payload: action.payload
|
|
134
|
+
payload: "payload" in action ? action.payload : void 0
|
|
89
135
|
}), action.type) {
|
|
90
136
|
case "SET_OVERALL_STATUS":
|
|
91
137
|
return { ...context, overallStatus: action.payload };
|
|
@@ -116,11 +162,6 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
116
162
|
...context,
|
|
117
163
|
maxReferenceDepth: action.payload
|
|
118
164
|
};
|
|
119
|
-
case "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS":
|
|
120
|
-
return {
|
|
121
|
-
...context,
|
|
122
|
-
shouldOverwriteFreshTranslations: action.payload
|
|
123
|
-
};
|
|
124
165
|
case "SET_REFERENCES":
|
|
125
166
|
return {
|
|
126
167
|
...context,
|
|
@@ -133,31 +174,82 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
133
174
|
translationJobs: newTranslationJobs
|
|
134
175
|
};
|
|
135
176
|
}
|
|
177
|
+
case "SET_RELEASE_CONFIG":
|
|
178
|
+
return {
|
|
179
|
+
...context,
|
|
180
|
+
releaseConfig: {
|
|
181
|
+
...context.releaseConfig,
|
|
182
|
+
...action.payload
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
case "RESET_TRANSLATION_STATE":
|
|
186
|
+
return devLog.info("Resetting translation state for new translation", {
|
|
187
|
+
...logContext,
|
|
188
|
+
previousStatus: context.overallStatus,
|
|
189
|
+
previousJobCount: context.translationJobs.size
|
|
190
|
+
}), {
|
|
191
|
+
...context,
|
|
192
|
+
overallStatus: "idle",
|
|
193
|
+
translationJobs: /* @__PURE__ */ new Map(),
|
|
194
|
+
// Clear any release conflicts from previous run
|
|
195
|
+
releaseConfig: {
|
|
196
|
+
...context.releaseConfig,
|
|
197
|
+
conflicts: void 0,
|
|
198
|
+
replaceExisting: void 0
|
|
199
|
+
}
|
|
200
|
+
};
|
|
136
201
|
default:
|
|
137
202
|
return context;
|
|
138
203
|
}
|
|
139
|
-
}, TranslationContextProvider = ({ document, config, children }) => {
|
|
204
|
+
}, generateDefaultReleaseTitle = (document) => `[AI Translation] ${document.title || document.name || document._id.split("-").slice(0, 2).join("-")}`, TranslationContextProvider = ({ document, config, children }) => {
|
|
140
205
|
devLog.debug("TranslationContextProvider initialized", {
|
|
141
206
|
documentId: document._id,
|
|
142
207
|
documentType: document._type,
|
|
143
208
|
supportedLanguages: config.supportedLanguages?.map((l) => l.id),
|
|
144
209
|
component: "TranslationContextProvider"
|
|
145
210
|
});
|
|
146
|
-
const initialContext = {
|
|
211
|
+
const defaultReleaseTitle = generateDefaultReleaseTitle(document), activeTranslation = getActiveTranslation(document._id), initialContext = {
|
|
147
212
|
document,
|
|
148
213
|
config,
|
|
149
|
-
|
|
214
|
+
// If there's an active translation, restore its state
|
|
215
|
+
overallStatus: activeTranslation?.overallStatus ?? "idle",
|
|
150
216
|
translationMetadata: null,
|
|
151
217
|
selectedTargetLanguages: [],
|
|
152
|
-
shouldOverwriteFreshTranslations: !1,
|
|
153
218
|
shouldTranslateReferences: !1,
|
|
154
219
|
maxReferenceDepth: 1,
|
|
155
220
|
references: [],
|
|
156
|
-
|
|
221
|
+
// Restore jobs from active translation if available
|
|
222
|
+
translationJobs: activeTranslation?.translationJobs ?? /* @__PURE__ */ new Map(),
|
|
157
223
|
translationMode: config?.openai?.defaultMode || "sanity-only",
|
|
158
|
-
openaiValidated: !1
|
|
159
|
-
|
|
224
|
+
openaiValidated: !1,
|
|
225
|
+
releaseConfig: {
|
|
226
|
+
mode: "new",
|
|
227
|
+
newReleaseTitle: defaultReleaseTitle
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
activeTranslation && devLog.info("Reconnecting to active translation", {
|
|
231
|
+
documentId: document._id,
|
|
232
|
+
status: activeTranslation.overallStatus,
|
|
233
|
+
jobCount: activeTranslation.translationJobs.size,
|
|
234
|
+
component: "TranslationContextProvider"
|
|
235
|
+
});
|
|
236
|
+
const [context, dispatch] = useReducer(translationReducer, initialContext);
|
|
160
237
|
return useEffect(() => {
|
|
238
|
+
const handleActiveTranslationUpdate = (state) => {
|
|
239
|
+
dispatch({
|
|
240
|
+
type: "SET_OVERALL_STATUS",
|
|
241
|
+
payload: state.overallStatus
|
|
242
|
+
});
|
|
243
|
+
for (const job of state.translationJobs.values())
|
|
244
|
+
dispatch({
|
|
245
|
+
type: "SET_TRANSLATION_JOB_STATUS",
|
|
246
|
+
payload: job
|
|
247
|
+
});
|
|
248
|
+
}, unsubscribe = subscribeToActiveTranslation(document._id, handleActiveTranslationUpdate);
|
|
249
|
+
return () => {
|
|
250
|
+
unsubscribe();
|
|
251
|
+
};
|
|
252
|
+
}, [document._id]), useEffect(() => {
|
|
161
253
|
(async () => {
|
|
162
254
|
if (config?.openai?.enabled && config?.openai?.apiKey)
|
|
163
255
|
try {
|
|
@@ -196,1044 +288,238 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
196
288
|
"useTranslationContext must be used within a TranslationContextProvider with document and config props"
|
|
197
289
|
);
|
|
198
290
|
return context;
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
documentId: undraftId(documentId),
|
|
203
|
-
translationMetadataType: TRANSLATION_METADATA_TYPE
|
|
204
|
-
},
|
|
205
|
-
initialValue: null
|
|
206
|
-
});
|
|
207
|
-
return useEffect(() => {
|
|
208
|
-
if (loading) {
|
|
209
|
-
dispatch({ type: "SET_OVERALL_STATUS", payload: "fetching_metadata" });
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (error) {
|
|
213
|
-
dispatch({ type: "SET_OVERALL_STATUS", payload: "error" });
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (data) {
|
|
217
|
-
dispatch({ type: "SET_TRANSLATION_METADATA", payload: data }), dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
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
|
-
}
|
|
278
|
-
return { valid: !0, status: "valid" };
|
|
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({
|
|
291
|
+
}, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
292
|
+
(translation) => translation._key === targetLanguage
|
|
293
|
+
)?.value._ref || null, undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_TYPE = "translation.metadata", getTranslationMetadata = async ({
|
|
367
294
|
client,
|
|
368
|
-
|
|
369
|
-
sourceLanguage
|
|
370
|
-
|
|
371
|
-
suggestions,
|
|
372
|
-
onProgress
|
|
373
|
-
}) {
|
|
374
|
-
if (!targetLanguage)
|
|
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) {
|
|
295
|
+
sourceDocument,
|
|
296
|
+
sourceLanguage
|
|
297
|
+
}) => {
|
|
562
298
|
try {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
299
|
+
return await client.fetch(
|
|
300
|
+
"*[_type == $tmType && references($sourceDocumentId)][0]",
|
|
301
|
+
{
|
|
302
|
+
tmType: TRANSLATION_METADATA_TYPE,
|
|
303
|
+
sourceDocumentId: undraftId(sourceDocument._id)
|
|
304
|
+
}
|
|
305
|
+
) || await client.create({
|
|
306
|
+
_type: TRANSLATION_METADATA_TYPE,
|
|
307
|
+
translations: [
|
|
308
|
+
{
|
|
309
|
+
_key: sourceLanguage,
|
|
310
|
+
_type: "internationalizedArrayReferenceValue",
|
|
311
|
+
value: {
|
|
312
|
+
_type: "reference",
|
|
313
|
+
_ref: undraftId(sourceDocument._id),
|
|
314
|
+
_weak: !0,
|
|
315
|
+
_strengthenOnPublish: {
|
|
316
|
+
_type: sourceDocument._type,
|
|
317
|
+
_weak: !1
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
],
|
|
322
|
+
translationHistory: []
|
|
566
323
|
});
|
|
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
324
|
} catch (error) {
|
|
573
|
-
throw
|
|
325
|
+
throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
|
|
574
326
|
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
327
|
+
}, checkDocumentConflicts = async ({
|
|
328
|
+
client,
|
|
329
|
+
releaseId,
|
|
330
|
+
documentId,
|
|
331
|
+
documentTitle,
|
|
332
|
+
documentType,
|
|
333
|
+
targetLanguages,
|
|
334
|
+
languageFieldName
|
|
335
|
+
}) => {
|
|
336
|
+
const conflicts = [], document = await client.withConfig({ perspective: "drafts" }).getDocument(documentId);
|
|
337
|
+
if (!document)
|
|
338
|
+
return conflicts;
|
|
339
|
+
const sourceLanguage = document[languageFieldName], translationMetadata = await getTranslationMetadata({
|
|
340
|
+
client,
|
|
341
|
+
sourceDocument: document,
|
|
342
|
+
sourceLanguage
|
|
343
|
+
});
|
|
344
|
+
for (const targetLanguage of targetLanguages) {
|
|
345
|
+
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage);
|
|
346
|
+
if (existingTranslationId) {
|
|
347
|
+
const versionedId = `versions.${releaseId}.${existingTranslationId}`;
|
|
348
|
+
await client.getDocument(versionedId) && (conflicts.push({
|
|
349
|
+
sourceDocumentId: documentId,
|
|
350
|
+
sourceDocumentTitle: documentTitle || document.title || document.name,
|
|
351
|
+
sourceDocumentType: documentType || document._type,
|
|
352
|
+
targetLanguage,
|
|
353
|
+
existingVersionId: versionedId
|
|
354
|
+
}), devLog.debug("Found existing version in release", {
|
|
355
|
+
releaseId,
|
|
356
|
+
sourceDocumentId: documentId,
|
|
357
|
+
targetLanguage,
|
|
358
|
+
versionedId
|
|
359
|
+
}));
|
|
585
360
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
361
|
+
}
|
|
362
|
+
return conflicts;
|
|
363
|
+
}, checkReleaseConflicts = async ({
|
|
364
|
+
client,
|
|
365
|
+
releaseId,
|
|
366
|
+
sourceDocument,
|
|
367
|
+
targetLanguages,
|
|
368
|
+
languageFieldName,
|
|
369
|
+
references = []
|
|
370
|
+
}) => {
|
|
371
|
+
const allConflicts = [];
|
|
372
|
+
try {
|
|
373
|
+
const documentsToCheck = [
|
|
374
|
+
{
|
|
375
|
+
_id: sourceDocument._id,
|
|
376
|
+
_type: sourceDocument._type,
|
|
377
|
+
title: sourceDocument.title || sourceDocument.name
|
|
378
|
+
},
|
|
379
|
+
...references.map((ref) => ({
|
|
380
|
+
_id: ref._id,
|
|
381
|
+
_type: ref._type,
|
|
382
|
+
title: ref.title
|
|
383
|
+
}))
|
|
384
|
+
], conflictChecks = await Promise.all(
|
|
385
|
+
documentsToCheck.map(
|
|
386
|
+
(doc) => checkDocumentConflicts({
|
|
387
|
+
client,
|
|
388
|
+
releaseId,
|
|
389
|
+
documentId: doc._id,
|
|
390
|
+
documentTitle: doc.title,
|
|
391
|
+
documentType: doc._type,
|
|
392
|
+
targetLanguages,
|
|
393
|
+
languageFieldName
|
|
394
|
+
})
|
|
395
|
+
)
|
|
396
|
+
);
|
|
397
|
+
for (const conflicts of conflictChecks)
|
|
398
|
+
allConflicts.push(...conflicts);
|
|
399
|
+
devLog.debug("Release conflict check complete", {
|
|
400
|
+
releaseId,
|
|
401
|
+
documentsChecked: documentsToCheck.length,
|
|
402
|
+
conflictsFound: allConflicts.length
|
|
403
|
+
});
|
|
598
404
|
} catch (error) {
|
|
599
|
-
|
|
405
|
+
devLog.error("Error checking release conflicts", error instanceof Error ? error : new Error(String(error)), {
|
|
406
|
+
releaseId,
|
|
407
|
+
sourceDocumentId: sourceDocument._id,
|
|
408
|
+
targetLanguages,
|
|
409
|
+
referencesCount: references.length
|
|
410
|
+
});
|
|
600
411
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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));
|
|
412
|
+
return allConflicts;
|
|
413
|
+
};
|
|
414
|
+
function isNetworkError(error) {
|
|
415
|
+
if (!error) return !1;
|
|
416
|
+
if (error instanceof TypeError) {
|
|
417
|
+
const message = error.message.toLowerCase();
|
|
418
|
+
return message.includes("fetch") || message.includes("network") || message.includes("connection") || message.includes("failed to fetch") || message.includes("networkerror");
|
|
618
419
|
}
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
// biome-ignore-start lint/style/noNonNullAssertion: safely handled
|
|
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));
|
|
420
|
+
if (error && typeof error == "object" && "code" in error) {
|
|
421
|
+
const code = error.code;
|
|
422
|
+
return ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "ECONNREFUSED", "EHOSTUNREACH"].includes(code);
|
|
630
423
|
}
|
|
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();
|
|
424
|
+
if (error && typeof error == "object" && "response" in error) {
|
|
425
|
+
const response = error.response;
|
|
426
|
+
if (response?.status)
|
|
427
|
+
return response.status >= 500 || response.status === 429;
|
|
656
428
|
}
|
|
657
|
-
if (
|
|
658
|
-
const
|
|
659
|
-
if (
|
|
660
|
-
|
|
661
|
-
currentValue,
|
|
662
|
-
currentValue ? `
|
|
663
|
-
` : "",
|
|
664
|
-
`## User Feedback (${timestamp})`,
|
|
665
|
-
...updates.map((update) => `- ${update}`)
|
|
666
|
-
].filter(Boolean).join(`
|
|
667
|
-
`), localeIndex = existingLocalePrompts.findIndex((lp) => lp._key === targetLanguage);
|
|
668
|
-
await client.patch(settingsId).set({ [`localePrompts[${localeIndex}].value`]: newValue }).commit();
|
|
669
|
-
} else {
|
|
670
|
-
const newValue = [`## User Feedback (${timestamp})`, ...updates.map((update) => `- ${update}`)].filter(Boolean).join(`
|
|
671
|
-
`);
|
|
672
|
-
await client.patch(settingsId).setIfMissing({ localePrompts: [] }).append("localePrompts", [
|
|
673
|
-
{
|
|
674
|
-
_type: "internationalizedArrayTextValue",
|
|
675
|
-
_key: targetLanguage,
|
|
676
|
-
value: newValue
|
|
677
|
-
}
|
|
678
|
-
]).commit();
|
|
679
|
-
}
|
|
429
|
+
if (error instanceof Error) {
|
|
430
|
+
const message = error.message;
|
|
431
|
+
if (/\b(50[0-9]|429)\b/.test(message))
|
|
432
|
+
return !0;
|
|
680
433
|
}
|
|
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 }));
|
|
434
|
+
return !1;
|
|
697
435
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
});
|
|
436
|
+
function isSanityMutationError(error) {
|
|
437
|
+
if (!error) return !1;
|
|
438
|
+
if (error instanceof Error) {
|
|
439
|
+
const message = error.message.toLowerCase();
|
|
440
|
+
return [
|
|
441
|
+
"validation",
|
|
442
|
+
"schema",
|
|
443
|
+
"invalid document",
|
|
444
|
+
"invalid field",
|
|
445
|
+
"type mismatch",
|
|
446
|
+
"reference",
|
|
447
|
+
"patch",
|
|
448
|
+
"mutation",
|
|
449
|
+
"document structure",
|
|
450
|
+
"required field",
|
|
451
|
+
"field type",
|
|
452
|
+
"sanity"
|
|
453
|
+
].some((pattern) => message.includes(pattern));
|
|
747
454
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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();
|
|
763
|
-
}
|
|
764
|
-
const ICON_SIZE = 64, progressIconMap = {
|
|
765
|
-
in_progress: /* @__PURE__ */ jsx(Spinner, {}),
|
|
766
|
-
completed: /* @__PURE__ */ jsx(CheckmarkCircleIcon, { fontSize: ICON_SIZE }),
|
|
767
|
-
error: /* @__PURE__ */ jsx(ErrorOutlineIcon, { fontSize: ICON_SIZE })
|
|
768
|
-
}, ProgressPanel = ({
|
|
769
|
-
title,
|
|
770
|
-
description,
|
|
771
|
-
tone,
|
|
772
|
-
status = "in_progress",
|
|
773
|
-
icon,
|
|
774
|
-
...props
|
|
775
|
-
}) => /* @__PURE__ */ jsx(Card, { tone, padding: 4, ...props, children: /* @__PURE__ */ jsxs(Flex, { gap: 4, align: "center", justify: "center", children: [
|
|
776
|
-
icon || progressIconMap[status],
|
|
777
|
-
/* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
778
|
-
title && /* @__PURE__ */ jsx(Text, { size: 2, weight: "bold", children: title }),
|
|
779
|
-
description && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: description })
|
|
780
|
-
] })
|
|
781
|
-
] }) }), IMPROVEMENT_TYPE_LABELS = {
|
|
782
|
-
globalPrompt: "Global Prompt",
|
|
783
|
-
localePrompt: "Locale Prompt",
|
|
784
|
-
protectedPhrase: "Protected Phrase",
|
|
785
|
-
glossaryTerm: "Glossary Term"
|
|
786
|
-
}, createNewSuggestion = (type = "protectedPhrase", sourceLanguage, targetLanguage) => {
|
|
787
|
-
const baseSuggestion = {
|
|
788
|
-
id: crypto.randomUUID(),
|
|
789
|
-
type,
|
|
790
|
-
description: "",
|
|
791
|
-
approved: !0
|
|
792
|
-
};
|
|
793
|
-
return type === "glossaryTerm" ? {
|
|
794
|
-
...baseSuggestion,
|
|
795
|
-
sourceValue: "",
|
|
796
|
-
targetValue: "",
|
|
797
|
-
sourceLocale: sourceLanguage,
|
|
798
|
-
targetLocale: targetLanguage
|
|
799
|
-
} : baseSuggestion;
|
|
800
|
-
}, SuggestionReviewList = ({
|
|
801
|
-
suggestions,
|
|
802
|
-
onSuggestionsChange,
|
|
803
|
-
sourceLanguage,
|
|
804
|
-
targetLanguage
|
|
805
|
-
}) => {
|
|
806
|
-
const [editingId, setEditingId] = useState(null), handleToggleApproval = (id) => {
|
|
807
|
-
const updated = suggestions.map((s) => s.id === id && s.validationStatus !== "invalid" ? { ...s, approved: !s.approved } : s);
|
|
808
|
-
onSuggestionsChange(updated);
|
|
809
|
-
}, handleTypeChange = (id, newType) => {
|
|
810
|
-
const updated = suggestions.map((s) => {
|
|
811
|
-
if (s.id !== id)
|
|
812
|
-
return s;
|
|
813
|
-
if (newType === "glossaryTerm" && s.type !== "glossaryTerm") {
|
|
814
|
-
const match = s.description.match(/^(.+?)\s*→\s*(.+)$/);
|
|
815
|
-
return {
|
|
816
|
-
...s,
|
|
817
|
-
type: newType,
|
|
818
|
-
sourceValue: match ? match[1].trim() : "",
|
|
819
|
-
targetValue: match ? match[2].trim() : "",
|
|
820
|
-
sourceLocale: sourceLanguage,
|
|
821
|
-
targetLocale: targetLanguage
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
return newType !== "glossaryTerm" && s.type === "glossaryTerm" ? {
|
|
825
|
-
...s,
|
|
826
|
-
type: newType,
|
|
827
|
-
description: s.sourceValue && s.targetValue ? `${s.sourceValue} \u2192 ${s.targetValue}` : s.description,
|
|
828
|
-
sourceValue: void 0,
|
|
829
|
-
targetValue: void 0
|
|
830
|
-
} : { ...s, type: newType };
|
|
831
|
-
});
|
|
832
|
-
onSuggestionsChange(updated);
|
|
833
|
-
}, handleDescriptionChange = (id, newDescription) => {
|
|
834
|
-
const updated = suggestions.map((s) => s.id === id ? { ...s, description: newDescription } : s);
|
|
835
|
-
onSuggestionsChange(updated);
|
|
836
|
-
}, handleGlossarySourceChange = (id, newSource) => {
|
|
837
|
-
const updated = suggestions.map(
|
|
838
|
-
(s) => s.id === id ? {
|
|
839
|
-
...s,
|
|
840
|
-
sourceValue: newSource,
|
|
841
|
-
description: `${newSource} \u2192 ${s.targetValue || ""}`
|
|
842
|
-
// Preserve sourceLocale and targetLocale
|
|
843
|
-
} : s
|
|
844
|
-
);
|
|
845
|
-
onSuggestionsChange(updated);
|
|
846
|
-
}, handleGlossaryTargetChange = (id, newTarget) => {
|
|
847
|
-
const updated = suggestions.map(
|
|
848
|
-
(s) => s.id === id ? {
|
|
849
|
-
...s,
|
|
850
|
-
targetValue: newTarget,
|
|
851
|
-
description: `${s.sourceValue || ""} \u2192 ${newTarget}`
|
|
852
|
-
// Preserve sourceLocale and targetLocale
|
|
853
|
-
} : s
|
|
854
|
-
);
|
|
855
|
-
onSuggestionsChange(updated);
|
|
856
|
-
}, handleRemove = (id) => {
|
|
857
|
-
const updated = suggestions.filter((s) => s.id !== id);
|
|
858
|
-
onSuggestionsChange(updated);
|
|
859
|
-
}, handleAddSuggestion = (type) => {
|
|
860
|
-
const newSuggestion = createNewSuggestion(type, sourceLanguage, targetLanguage);
|
|
861
|
-
onSuggestionsChange([...suggestions, newSuggestion]), setEditingId(newSuggestion.id);
|
|
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("");
|
|
1024
|
-
try {
|
|
1025
|
-
const translationHistory2 = translationMetadata?.translationHistory?.find(
|
|
1026
|
-
(history) => history.language === document[config.languageFieldName]
|
|
1027
|
-
);
|
|
1028
|
-
if (!translationHistory2)
|
|
1029
|
-
throw new Error(
|
|
1030
|
-
"No translation history found for this document. Please ensure this document was translated using the AI Translation plugin."
|
|
1031
|
-
);
|
|
1032
|
-
const { sourceSnapshot, targetSnapshot, sourceLanguage: srcLang } = translationHistory2;
|
|
1033
|
-
if (!sourceSnapshot || !targetSnapshot)
|
|
1034
|
-
throw new Error("Translation snapshots are missing from history.");
|
|
1035
|
-
const srcLanguage = srcLang || "en-us", tgtLanguage = document[config.languageFieldName];
|
|
1036
|
-
setSourceLanguage(srcLanguage), setTargetLanguage(tgtLanguage);
|
|
1037
|
-
const generatedSuggestions = await analyzeFeedback({
|
|
1038
|
-
client,
|
|
1039
|
-
schemaId: config.schemaId,
|
|
1040
|
-
sourceLanguage: srcLanguage,
|
|
1041
|
-
targetLanguage: tgtLanguage,
|
|
1042
|
-
userFeedback: feedback || void 0,
|
|
1043
|
-
shouldIncludeEdits,
|
|
1044
|
-
sourceSnapshot,
|
|
1045
|
-
targetSnapshot,
|
|
1046
|
-
currentDocument: shouldIncludeEdits ? document : void 0,
|
|
1047
|
-
onProgress: ({ stage, message }) => {
|
|
1048
|
-
setProcessingState(stage), setChangeSummary(message);
|
|
1049
|
-
}
|
|
1050
|
-
});
|
|
1051
|
-
setSuggestions(generatedSuggestions), setProcessingState("reviewing");
|
|
1052
|
-
} catch (err) {
|
|
1053
|
-
console.error("Error analyzing feedback:", err), setError(err instanceof Error ? err.message : "An unexpected error occurred"), setProcessingState("error");
|
|
1054
|
-
}
|
|
1055
|
-
}, handleApplySuggestions = async () => {
|
|
1056
|
-
if (!config) {
|
|
1057
|
-
setError("Missing plugin configuration");
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
if (suggestions.filter((s) => s.approved).length === 0) {
|
|
1061
|
-
setError("Please approve at least one suggestion to apply");
|
|
1062
|
-
return;
|
|
1063
|
-
}
|
|
1064
|
-
setError(""), setProcessingState("applying");
|
|
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
|
-
] });
|
|
455
|
+
if (error && typeof error == "object") {
|
|
456
|
+
const errorObj = error;
|
|
457
|
+
if (("statusCode" in errorObj || "response" in errorObj || "details" in errorObj) && "statusCode" in errorObj) {
|
|
458
|
+
const statusCode = errorObj.statusCode;
|
|
459
|
+
if (statusCode >= 400 && statusCode < 500 && statusCode !== 429)
|
|
460
|
+
return !0;
|
|
461
|
+
}
|
|
1144
462
|
}
|
|
1145
|
-
return
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
463
|
+
return !1;
|
|
464
|
+
}
|
|
465
|
+
const DEFAULT_RETRY_OPTIONS = {
|
|
466
|
+
maxAttempts: 2,
|
|
467
|
+
initialDelayMs: 0,
|
|
468
|
+
maxDelayMs: 5e3,
|
|
469
|
+
backoffMultiplier: 2,
|
|
470
|
+
retryableErrors: isNetworkError,
|
|
471
|
+
logRetries: !0
|
|
472
|
+
};
|
|
473
|
+
function calculateDelay(attempt, options) {
|
|
474
|
+
const delay = options.initialDelayMs * Math.pow(options.backoffMultiplier, attempt);
|
|
475
|
+
return Math.min(delay, options.maxDelayMs);
|
|
476
|
+
}
|
|
477
|
+
function sleep(ms) {
|
|
478
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
479
|
+
}
|
|
480
|
+
async function retryWithBackoff(operation, options = {}) {
|
|
481
|
+
const opts = {
|
|
482
|
+
...DEFAULT_RETRY_OPTIONS,
|
|
483
|
+
...options,
|
|
484
|
+
retryableErrors: options.retryableErrors || DEFAULT_RETRY_OPTIONS.retryableErrors
|
|
485
|
+
};
|
|
486
|
+
let lastError;
|
|
487
|
+
const maxAttempts = opts.maxAttempts + 1;
|
|
488
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++)
|
|
489
|
+
try {
|
|
490
|
+
const result = await operation();
|
|
491
|
+
return attempt > 1 && opts.logRetries && devLog.info(`Operation succeeded after ${attempt} attempts`, {
|
|
492
|
+
...opts.context,
|
|
493
|
+
attempt,
|
|
494
|
+
totalAttempts: attempt
|
|
495
|
+
}), result;
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (lastError = error, !opts.retryableErrors(error))
|
|
498
|
+
throw opts.logRetries && devLog.warn("Error is not retryable, aborting retries", {
|
|
499
|
+
...opts.context,
|
|
500
|
+
attempt,
|
|
501
|
+
error: error instanceof Error ? error.message : String(error)
|
|
502
|
+
}), error;
|
|
503
|
+
if (attempt >= maxAttempts)
|
|
504
|
+
throw opts.logRetries && devLog.error(
|
|
505
|
+
`Operation failed after ${maxAttempts} attempts`,
|
|
506
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1167
507
|
{
|
|
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
|
|
508
|
+
...opts.context,
|
|
509
|
+
totalAttempts: maxAttempts
|
|
1174
510
|
}
|
|
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 }) })
|
|
511
|
+
), error;
|
|
512
|
+
const delay = calculateDelay(attempt - 1, opts);
|
|
513
|
+
opts.logRetries && devLog.warn(`Operation failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {
|
|
514
|
+
...opts.context,
|
|
515
|
+
attempt,
|
|
516
|
+
totalAttempts: maxAttempts,
|
|
517
|
+
delayMs: delay,
|
|
518
|
+
error: error instanceof Error ? error.message : String(error)
|
|
519
|
+
}), delay > 0 && await sleep(delay);
|
|
1234
520
|
}
|
|
1235
|
-
|
|
1236
|
-
}
|
|
521
|
+
throw lastError;
|
|
522
|
+
}
|
|
1237
523
|
class Semaphore {
|
|
1238
524
|
permits;
|
|
1239
525
|
queue = [];
|
|
@@ -1584,13 +870,27 @@ const fetchTranslationPrompts = async ({
|
|
|
1584
870
|
}
|
|
1585
871
|
}), hasChanges = !0);
|
|
1586
872
|
}
|
|
1587
|
-
hasChanges && (await
|
|
873
|
+
hasChanges && (await retryWithBackoff(
|
|
874
|
+
async () => {
|
|
875
|
+
await patch.commit();
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
maxAttempts: 2,
|
|
879
|
+
initialDelayMs: 0,
|
|
880
|
+
retryableErrors: isNetworkError,
|
|
881
|
+
context: {
|
|
882
|
+
operation: "localizeReferences",
|
|
883
|
+
documentId,
|
|
884
|
+
referenceCount: refs.length
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
), devLog.info("Updated references in batch", {
|
|
1588
888
|
documentId,
|
|
1589
889
|
count: refs.length,
|
|
1590
890
|
translatedCount: translationMap.size,
|
|
1591
891
|
component: "updateReferences"
|
|
1592
892
|
}));
|
|
1593
|
-
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages) => {
|
|
893
|
+
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages, mainSourceDocumentId) => {
|
|
1594
894
|
for (const sourceDocument of sourceDocuments)
|
|
1595
895
|
for (const lang of targetLanguages) {
|
|
1596
896
|
const payload = {
|
|
@@ -1603,146 +903,8 @@ const fetchTranslationPrompts = async ({
|
|
|
1603
903
|
dispatch({
|
|
1604
904
|
type: "SET_TRANSLATION_JOB_STATUS",
|
|
1605
905
|
payload
|
|
1606
|
-
});
|
|
1607
|
-
}
|
|
1608
|
-
}, DEFAULT_OPTIONS = {
|
|
1609
|
-
maxRetries: 3,
|
|
1610
|
-
initialDelayMs: 1e3,
|
|
1611
|
-
maxDelayMs: 3e4,
|
|
1612
|
-
backoffMultiplier: 2,
|
|
1613
|
-
jitter: !0
|
|
1614
|
-
};
|
|
1615
|
-
function isRetryableError(error) {
|
|
1616
|
-
if (error instanceof Error) {
|
|
1617
|
-
const message = error.message.toLowerCase(), name = error.name.toLowerCase(), nonRetryablePatterns = [
|
|
1618
|
-
"schema not found",
|
|
1619
|
-
"invalid schema",
|
|
1620
|
-
"not found",
|
|
1621
|
-
"unauthorized",
|
|
1622
|
-
"forbidden",
|
|
1623
|
-
"invalid api key",
|
|
1624
|
-
"missing api key",
|
|
1625
|
-
"validation failed",
|
|
1626
|
-
"invalid document",
|
|
1627
|
-
"source language not found",
|
|
1628
|
-
"invalid configuration",
|
|
1629
|
-
"type mismatch"
|
|
1630
|
-
];
|
|
1631
|
-
for (const pattern of nonRetryablePatterns)
|
|
1632
|
-
if (message.includes(pattern) || name.includes(pattern))
|
|
1633
|
-
return !1;
|
|
1634
|
-
const retryablePatterns = [
|
|
1635
|
-
"failed to fetch",
|
|
1636
|
-
"network",
|
|
1637
|
-
"timeout",
|
|
1638
|
-
"econnreset",
|
|
1639
|
-
"econnrefused",
|
|
1640
|
-
"enotfound",
|
|
1641
|
-
"socket",
|
|
1642
|
-
"rate limit",
|
|
1643
|
-
"too many requests",
|
|
1644
|
-
"429",
|
|
1645
|
-
"500",
|
|
1646
|
-
"502",
|
|
1647
|
-
"503",
|
|
1648
|
-
"504",
|
|
1649
|
-
"internal server error",
|
|
1650
|
-
"bad gateway",
|
|
1651
|
-
"service unavailable",
|
|
1652
|
-
"gateway timeout",
|
|
1653
|
-
"temporarily unavailable",
|
|
1654
|
-
"try again"
|
|
1655
|
-
];
|
|
1656
|
-
for (const pattern of retryablePatterns)
|
|
1657
|
-
if (message.includes(pattern) || name.includes(pattern))
|
|
1658
|
-
return !0;
|
|
1659
|
-
}
|
|
1660
|
-
if (typeof error == "object" && error !== null) {
|
|
1661
|
-
const statusCode = error.statusCode || error.status;
|
|
1662
|
-
if (statusCode) {
|
|
1663
|
-
if (statusCode === 429 || statusCode >= 500 && statusCode < 600)
|
|
1664
|
-
return !0;
|
|
1665
|
-
if (statusCode >= 400 && statusCode < 500)
|
|
1666
|
-
return !1;
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
return !0;
|
|
1670
|
-
}
|
|
1671
|
-
function calculateBackoffDelay(attempt, options) {
|
|
1672
|
-
const { initialDelayMs, maxDelayMs, backoffMultiplier, jitter } = options;
|
|
1673
|
-
let delay = initialDelayMs * backoffMultiplier ** attempt;
|
|
1674
|
-
if (delay = Math.min(delay, maxDelayMs), jitter) {
|
|
1675
|
-
const jitterFactor = 0.75 + Math.random() * 0.5;
|
|
1676
|
-
delay = Math.round(delay * jitterFactor);
|
|
1677
|
-
}
|
|
1678
|
-
return delay;
|
|
1679
|
-
}
|
|
1680
|
-
function sleep(ms) {
|
|
1681
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1682
|
-
}
|
|
1683
|
-
async function withRetry(fn, options = {}) {
|
|
1684
|
-
const opts = { ...DEFAULT_OPTIONS, ...options }, shouldRetry = opts.isRetryable ?? isRetryableError;
|
|
1685
|
-
let lastError, attempts = 0;
|
|
1686
|
-
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
1687
|
-
attempts = attempt + 1;
|
|
1688
|
-
try {
|
|
1689
|
-
return { success: !0, result: await fn(), attempts };
|
|
1690
|
-
} catch (error) {
|
|
1691
|
-
if (lastError = error, attempt >= opts.maxRetries) {
|
|
1692
|
-
devLog.debug("Max retries exhausted", { attempt: attempts, error });
|
|
1693
|
-
break;
|
|
1694
|
-
}
|
|
1695
|
-
if (!shouldRetry(error)) {
|
|
1696
|
-
devLog.debug("Error is not retryable", { attempt: attempts, error });
|
|
1697
|
-
break;
|
|
1698
|
-
}
|
|
1699
|
-
const delay = calculateBackoffDelay(attempt, opts);
|
|
1700
|
-
devLog.debug("Scheduling retry", {
|
|
1701
|
-
attempt: attempts,
|
|
1702
|
-
nextAttempt: attempt + 2,
|
|
1703
|
-
delayMs: delay,
|
|
1704
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1705
|
-
}), opts.onRetry && opts.onRetry(attempt + 1, error, delay), await sleep(delay);
|
|
906
|
+
}), mainSourceDocumentId && updateActiveTranslationJob(mainSourceDocumentId, payload);
|
|
1706
907
|
}
|
|
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
908
|
}, updateTranslationMetadata = async ({
|
|
1747
909
|
client,
|
|
1748
910
|
targetLanguage,
|
|
@@ -1776,11 +938,71 @@ const getExistingTranslationId = (translationMetadata, targetLanguage) => transl
|
|
|
1776
938
|
targetRev: newDocument._rev,
|
|
1777
939
|
targetSnapshot: JSON.stringify(newDocument)
|
|
1778
940
|
}, translationPatch = client.patch(translationMetadata._id).setIfMissing({ translations: [] });
|
|
1779
|
-
isNewTranslation ? await
|
|
941
|
+
isNewTranslation ? await retryWithBackoff(
|
|
942
|
+
async () => {
|
|
943
|
+
await translationPatch.insert("after", "translations[-1]", [refToNewTranslation]).commit();
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
maxAttempts: 2,
|
|
947
|
+
initialDelayMs: 0,
|
|
948
|
+
retryableErrors: isNetworkError,
|
|
949
|
+
context: {
|
|
950
|
+
operation: "updateTranslationMetadata.translations",
|
|
951
|
+
sourceDocumentId: sourceDocument._id,
|
|
952
|
+
targetLanguage,
|
|
953
|
+
isNewTranslation: !0
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
) : await retryWithBackoff(
|
|
957
|
+
async () => {
|
|
958
|
+
await translationPatch.insert("replace", `translations[_key == "${targetLanguage}"]`, [refToNewTranslation]).commit();
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
maxAttempts: 2,
|
|
962
|
+
initialDelayMs: 0,
|
|
963
|
+
retryableErrors: isNetworkError,
|
|
964
|
+
context: {
|
|
965
|
+
operation: "updateTranslationMetadata.translations",
|
|
966
|
+
sourceDocumentId: sourceDocument._id,
|
|
967
|
+
targetLanguage,
|
|
968
|
+
isNewTranslation: !1
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
);
|
|
1780
972
|
const historyPatch = client.patch(translationMetadata._id).setIfMissing({ translationHistory: [] });
|
|
1781
973
|
translationMetadata.translationHistory?.some(
|
|
1782
974
|
(history) => history.language === targetLanguage
|
|
1783
|
-
) ? await
|
|
975
|
+
) ? await retryWithBackoff(
|
|
976
|
+
async () => {
|
|
977
|
+
await historyPatch.insert("replace", `translationHistory[_key == "${targetLanguage}"]`, [newTranslationHistory]).commit();
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
maxAttempts: 2,
|
|
981
|
+
initialDelayMs: 0,
|
|
982
|
+
retryableErrors: isNetworkError,
|
|
983
|
+
context: {
|
|
984
|
+
operation: "updateTranslationMetadata.history",
|
|
985
|
+
sourceDocumentId: sourceDocument._id,
|
|
986
|
+
targetLanguage,
|
|
987
|
+
isNewHistory: !1
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
) : await retryWithBackoff(
|
|
991
|
+
async () => {
|
|
992
|
+
await historyPatch.insert("after", "translationHistory[-1]", [newTranslationHistory]).commit();
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
maxAttempts: 2,
|
|
996
|
+
initialDelayMs: 0,
|
|
997
|
+
retryableErrors: isNetworkError,
|
|
998
|
+
context: {
|
|
999
|
+
operation: "updateTranslationMetadata.history",
|
|
1000
|
+
sourceDocumentId: sourceDocument._id,
|
|
1001
|
+
targetLanguage,
|
|
1002
|
+
isNewHistory: !0
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
);
|
|
1784
1006
|
} catch (error) {
|
|
1785
1007
|
throw devLog.error("Error updating translation metadata", error instanceof Error ? error : new Error(String(error)), {
|
|
1786
1008
|
sourceDocumentId: sourceDocument._id,
|
|
@@ -1789,15 +1011,16 @@ const getExistingTranslationId = (translationMetadata, targetLanguage) => transl
|
|
|
1789
1011
|
component: "updateTranslationMetadata"
|
|
1790
1012
|
}), error;
|
|
1791
1013
|
}
|
|
1792
|
-
}, setJobStatus = (dispatch, _id, language, status, payload) => {
|
|
1014
|
+
}, setJobStatus = (dispatch, _id, language, status, payload, sourceDocumentId) => {
|
|
1015
|
+
const job = defaults(payload, {
|
|
1016
|
+
_id,
|
|
1017
|
+
language,
|
|
1018
|
+
status
|
|
1019
|
+
});
|
|
1793
1020
|
dispatch({
|
|
1794
1021
|
type: "SET_TRANSLATION_JOB_STATUS",
|
|
1795
|
-
payload:
|
|
1796
|
-
|
|
1797
|
-
language,
|
|
1798
|
-
status
|
|
1799
|
-
})
|
|
1800
|
-
});
|
|
1022
|
+
payload: job
|
|
1023
|
+
}), sourceDocumentId && updateActiveTranslationJob(sourceDocumentId, job);
|
|
1801
1024
|
};
|
|
1802
1025
|
function shouldExcludeField(fieldPath, excludeFields) {
|
|
1803
1026
|
for (const pattern of excludeFields) {
|
|
@@ -2123,14 +1346,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2123
1346
|
}), devLog.debug("OpenAI GPT-5 Full Prompt", {
|
|
2124
1347
|
messages: request.messages
|
|
2125
1348
|
});
|
|
2126
|
-
const response = await
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
1349
|
+
const response = await retryWithBackoff(
|
|
1350
|
+
async () => {
|
|
1351
|
+
const fetchResponse = await fetch("https://api.openai.com/v1/responses", {
|
|
1352
|
+
method: "POST",
|
|
1353
|
+
headers: {
|
|
1354
|
+
"Content-Type": "application/json",
|
|
1355
|
+
Authorization: `Bearer ${apiKey}`
|
|
1356
|
+
},
|
|
1357
|
+
body: JSON.stringify(requestBody)
|
|
1358
|
+
});
|
|
1359
|
+
if (!fetchResponse.ok) {
|
|
1360
|
+
const errorMessage = `GPT-5 API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
|
|
1361
|
+
throw errorObj.response = { status: fetchResponse.status }, errorObj;
|
|
1362
|
+
}
|
|
1363
|
+
return fetchResponse;
|
|
2131
1364
|
},
|
|
2132
|
-
|
|
2133
|
-
|
|
1365
|
+
{
|
|
1366
|
+
maxAttempts: 2,
|
|
1367
|
+
initialDelayMs: 0,
|
|
1368
|
+
retryableErrors: isNetworkError,
|
|
1369
|
+
context: { model: requestBody.model, endpoint: "responses" }
|
|
1370
|
+
}
|
|
1371
|
+
);
|
|
2134
1372
|
if (!response.ok) {
|
|
2135
1373
|
const error = await response.json();
|
|
2136
1374
|
throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
|
|
@@ -2169,14 +1407,29 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2169
1407
|
}), devLog.debug("OpenAI Chat Completions Full Prompt", {
|
|
2170
1408
|
messages: request.messages
|
|
2171
1409
|
});
|
|
2172
|
-
const response = await
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
1410
|
+
const response = await retryWithBackoff(
|
|
1411
|
+
async () => {
|
|
1412
|
+
const fetchResponse = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1413
|
+
method: "POST",
|
|
1414
|
+
headers: {
|
|
1415
|
+
"Content-Type": "application/json",
|
|
1416
|
+
Authorization: `Bearer ${apiKey}`
|
|
1417
|
+
},
|
|
1418
|
+
body: JSON.stringify(requestBody)
|
|
1419
|
+
});
|
|
1420
|
+
if (!fetchResponse.ok) {
|
|
1421
|
+
const errorMessage = `OpenAI API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
|
|
1422
|
+
throw errorObj.response = { status: fetchResponse.status }, errorObj;
|
|
1423
|
+
}
|
|
1424
|
+
return fetchResponse;
|
|
2177
1425
|
},
|
|
2178
|
-
|
|
2179
|
-
|
|
1426
|
+
{
|
|
1427
|
+
maxAttempts: 2,
|
|
1428
|
+
initialDelayMs: 0,
|
|
1429
|
+
retryableErrors: isNetworkError,
|
|
1430
|
+
context: { model, endpoint: "chat/completions" }
|
|
1431
|
+
}
|
|
1432
|
+
);
|
|
2180
1433
|
if (!response.ok) {
|
|
2181
1434
|
const error = await response.json();
|
|
2182
1435
|
throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
|
|
@@ -2331,7 +1584,21 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2331
1584
|
parent && parent.children.push(node);
|
|
2332
1585
|
} else
|
|
2333
1586
|
root.push(node);
|
|
2334
|
-
else
|
|
1587
|
+
else if (attributes.type === "string" && !content) {
|
|
1588
|
+
const contentLines = [];
|
|
1589
|
+
for (i++; i < lines.length; ) {
|
|
1590
|
+
const currentLine = lines[i];
|
|
1591
|
+
if (currentLine.trim() === "</field>")
|
|
1592
|
+
break;
|
|
1593
|
+
contentLines.push(currentLine), i++;
|
|
1594
|
+
}
|
|
1595
|
+
if (node.text = contentLines.join(`
|
|
1596
|
+
`).trim(), stack.length > 0) {
|
|
1597
|
+
const parent = stack[stack.length - 1];
|
|
1598
|
+
parent && parent.children.push(node);
|
|
1599
|
+
} else
|
|
1600
|
+
root.push(node);
|
|
1601
|
+
} else
|
|
2335
1602
|
stack.push(node);
|
|
2336
1603
|
continue;
|
|
2337
1604
|
}
|
|
@@ -2595,7 +1862,9 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2595
1862
|
languageFieldName,
|
|
2596
1863
|
translationPrompts,
|
|
2597
1864
|
targetLanguage,
|
|
2598
|
-
openaiConfig
|
|
1865
|
+
openaiConfig,
|
|
1866
|
+
previousError,
|
|
1867
|
+
errorContext
|
|
2599
1868
|
}) => {
|
|
2600
1869
|
const sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
2601
1870
|
sourceId: sourceDocument._id,
|
|
@@ -2613,7 +1882,8 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2613
1882
|
sourceText,
|
|
2614
1883
|
sourceLanguage,
|
|
2615
1884
|
targetLanguage,
|
|
2616
|
-
translationPrompts
|
|
1885
|
+
translationPrompts,
|
|
1886
|
+
errorContext
|
|
2617
1887
|
}), response = await callGPT5(openaiConfig.apiKey, {
|
|
2618
1888
|
messages: [
|
|
2619
1889
|
{
|
|
@@ -2671,14 +1941,21 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2671
1941
|
sourceText,
|
|
2672
1942
|
sourceLanguage,
|
|
2673
1943
|
targetLanguage,
|
|
2674
|
-
translationPrompts
|
|
1944
|
+
translationPrompts,
|
|
1945
|
+
errorContext
|
|
2675
1946
|
}) => {
|
|
2676
1947
|
const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
|
|
2677
1948
|
return `# TRANSLATION TASK
|
|
2678
1949
|
|
|
2679
1950
|
Translate this ${sourceLanguage} document into ${targetLanguage}.
|
|
2680
1951
|
|
|
2681
|
-
|
|
1952
|
+
${errorContext ? `## \u26A0\uFE0F IMPORTANT: Previous Attempt Error
|
|
1953
|
+
|
|
1954
|
+
${errorContext}
|
|
1955
|
+
|
|
1956
|
+
Please carefully review the error above and ensure your translation fixes the issue.
|
|
1957
|
+
|
|
1958
|
+
` : ""}## Guidelines
|
|
2682
1959
|
|
|
2683
1960
|
${globalPrompt ? `### General Guidelines
|
|
2684
1961
|
${globalPrompt}
|
|
@@ -2852,21 +2129,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2852
2129
|
languageFieldName,
|
|
2853
2130
|
targetLanguage
|
|
2854
2131
|
});
|
|
2855
|
-
return await
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2132
|
+
return await retryWithBackoff(
|
|
2133
|
+
async () => await client.agent.action.translate({
|
|
2134
|
+
schemaId,
|
|
2135
|
+
toLanguage,
|
|
2136
|
+
fromLanguage,
|
|
2137
|
+
documentId: sourceDocument._id,
|
|
2138
|
+
noWrite: !0,
|
|
2139
|
+
// Do not write to the document, just return the translated content
|
|
2140
|
+
languageFieldPath: [languageFieldName],
|
|
2141
|
+
styleGuide,
|
|
2142
|
+
protectedPhrases: translationPrompts.protectedPhrases,
|
|
2143
|
+
conditionalPaths: {
|
|
2144
|
+
defaultHidden: !1,
|
|
2145
|
+
defaultReadOnly: !1
|
|
2146
|
+
}
|
|
2147
|
+
}),
|
|
2148
|
+
{
|
|
2149
|
+
maxAttempts: 2,
|
|
2150
|
+
initialDelayMs: 0,
|
|
2151
|
+
retryableErrors: isNetworkError,
|
|
2152
|
+
context: {
|
|
2153
|
+
operation: "agent.action.translate",
|
|
2154
|
+
documentId: sourceDocument._id,
|
|
2155
|
+
targetLanguage,
|
|
2156
|
+
schemaId
|
|
2157
|
+
}
|
|
2868
2158
|
}
|
|
2869
|
-
|
|
2159
|
+
);
|
|
2870
2160
|
}, handleTranslation = async ({
|
|
2871
2161
|
dispatch,
|
|
2872
2162
|
client,
|
|
@@ -2879,75 +2169,99 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2879
2169
|
translationMode,
|
|
2880
2170
|
openaiConfig,
|
|
2881
2171
|
releaseId,
|
|
2882
|
-
excludeFields = []
|
|
2172
|
+
excludeFields = [],
|
|
2173
|
+
replaceExisting = !1,
|
|
2174
|
+
mainSourceDocumentId
|
|
2883
2175
|
}) => {
|
|
2884
|
-
|
|
2176
|
+
const safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
|
|
2177
|
+
excludeFields.length !== safeExcludeFields.length && devLog.warn("Language field was in excludeFields - removing it to prevent translation errors", {
|
|
2178
|
+
documentId: sourceDocument._id,
|
|
2179
|
+
languageFieldName,
|
|
2180
|
+
originalExcludeFields: excludeFields,
|
|
2181
|
+
safeExcludeFields
|
|
2182
|
+
}), devLog.info("Filtering excluded fields before translation", {
|
|
2885
2183
|
documentId: sourceDocument._id,
|
|
2886
2184
|
documentType: sourceDocument._type,
|
|
2887
|
-
excludeFields,
|
|
2888
|
-
excludeFieldsLength:
|
|
2185
|
+
excludeFields: safeExcludeFields,
|
|
2186
|
+
excludeFieldsLength: safeExcludeFields?.length ?? 0,
|
|
2889
2187
|
sourceHasTitle: "title" in sourceDocument,
|
|
2890
2188
|
sourceTitleValue: sourceDocument.title
|
|
2891
2189
|
});
|
|
2892
|
-
const { filteredDocument, excludedValues } = filterExcludedFields(sourceDocument,
|
|
2190
|
+
const { filteredDocument, excludedValues } = filterExcludedFields(sourceDocument, safeExcludeFields);
|
|
2893
2191
|
devLog.info("After filtering excluded fields", {
|
|
2894
2192
|
documentId: sourceDocument._id,
|
|
2895
2193
|
filteredHasTitle: "title" in filteredDocument,
|
|
2896
2194
|
filteredTitleValue: filteredDocument.title,
|
|
2897
2195
|
excludedValuesKeys: Object.keys(excludedValues),
|
|
2898
2196
|
excludedValues
|
|
2899
|
-
}), devLog.debug("Applying field transforms before translation", {
|
|
2900
|
-
documentId: sourceDocument._id,
|
|
2901
|
-
documentType: sourceDocument._type
|
|
2902
|
-
});
|
|
2903
|
-
const { transformedDocument, originalStructure } = applyTransforms({
|
|
2904
|
-
document: filteredDocument,
|
|
2905
|
-
registry: globalTransformRegistry,
|
|
2906
|
-
schemaType: sourceDocument._type
|
|
2907
2197
|
});
|
|
2908
|
-
let
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2198
|
+
let documentToTranslate, originalStructure = {};
|
|
2199
|
+
if (translationMode === "openai-only") {
|
|
2200
|
+
devLog.debug("Applying field transforms before translation (OpenAI mode)", {
|
|
2201
|
+
documentId: sourceDocument._id,
|
|
2202
|
+
documentType: sourceDocument._type
|
|
2203
|
+
});
|
|
2204
|
+
const transformResult = applyTransforms({
|
|
2205
|
+
document: filteredDocument,
|
|
2206
|
+
registry: globalTransformRegistry,
|
|
2207
|
+
schemaType: sourceDocument._type
|
|
2208
|
+
});
|
|
2209
|
+
documentToTranslate = transformResult.transformedDocument, originalStructure = transformResult.originalStructure;
|
|
2210
|
+
} else
|
|
2211
|
+
devLog.debug("Skipping field transforms (Sanity Agent mode)", {
|
|
2212
|
+
documentId: sourceDocument._id,
|
|
2213
|
+
documentType: sourceDocument._type
|
|
2214
|
+
}), documentToTranslate = filteredDocument;
|
|
2215
|
+
let translatedDoc, previousError = null;
|
|
2216
|
+
const performTranslation = async (errorContext) => {
|
|
2217
|
+
switch (translationMode) {
|
|
2218
|
+
case "sanity-only":
|
|
2219
|
+
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating", void 0, mainSourceDocumentId), await getSanityAgentTranslation({
|
|
2220
|
+
sourceDocument: documentToTranslate,
|
|
2221
|
+
targetLanguage,
|
|
2222
|
+
languageFieldName,
|
|
2223
|
+
schemaId,
|
|
2224
|
+
client,
|
|
2225
|
+
translationPrompts
|
|
2226
|
+
});
|
|
2227
|
+
case "openai-only": {
|
|
2228
|
+
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2229
|
+
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2230
|
+
return setJobStatus(
|
|
2231
|
+
dispatch,
|
|
2232
|
+
sourceDocument._id,
|
|
2233
|
+
targetLanguage,
|
|
2234
|
+
"translating_with_openai",
|
|
2235
|
+
void 0,
|
|
2236
|
+
mainSourceDocumentId
|
|
2237
|
+
), await getOpenAITranslation({
|
|
2238
|
+
dispatch,
|
|
2239
|
+
sourceDocument: documentToTranslate,
|
|
2240
|
+
targetLanguage,
|
|
2241
|
+
languageFieldName,
|
|
2242
|
+
translationPrompts,
|
|
2243
|
+
openaiConfig,
|
|
2244
|
+
previousError: errorContext && previousError ? previousError : void 0,
|
|
2245
|
+
errorContext
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
default:
|
|
2249
|
+
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2933
2250
|
}
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
default:
|
|
2937
|
-
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2938
|
-
}
|
|
2939
|
-
if (!translatedDoc)
|
|
2251
|
+
};
|
|
2252
|
+
if (translatedDoc = await performTranslation(), !translatedDoc)
|
|
2940
2253
|
return null;
|
|
2941
|
-
|
|
2254
|
+
let transformedResult;
|
|
2255
|
+
Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
|
|
2942
2256
|
documentId: sourceDocument._id,
|
|
2943
2257
|
documentType: sourceDocument._type
|
|
2944
|
-
})
|
|
2945
|
-
const transformedResult = restoreTransforms({
|
|
2258
|
+
}), transformedResult = restoreTransforms({
|
|
2946
2259
|
translatedDocument: translatedDoc,
|
|
2947
2260
|
originalStructure,
|
|
2948
2261
|
registry: globalTransformRegistry,
|
|
2949
2262
|
schemaType: sourceDocument._type
|
|
2950
|
-
})
|
|
2263
|
+
})) : transformedResult = translatedDoc;
|
|
2264
|
+
const excludedValuesToRestore = { ...excludedValues };
|
|
2951
2265
|
delete excludedValuesToRestore[languageFieldName], devLog.info("Before restoring excluded fields", {
|
|
2952
2266
|
documentId: sourceDocument._id,
|
|
2953
2267
|
excludedFieldCount: Object.keys(excludedValues).length,
|
|
@@ -2966,11 +2280,76 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2966
2280
|
...finalDoc,
|
|
2967
2281
|
_id: targetDocumentVersionId
|
|
2968
2282
|
};
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
}),
|
|
2283
|
+
replaceExisting && await client.getDocument(targetDocumentVersionId) && (devLog.info("Replacing existing version in release", {
|
|
2284
|
+
documentId: sourceDocument._id,
|
|
2285
|
+
targetDocumentVersionId,
|
|
2286
|
+
releaseId
|
|
2287
|
+
}), await client.delete(targetDocumentVersionId));
|
|
2288
|
+
try {
|
|
2289
|
+
await retryWithBackoff(
|
|
2290
|
+
async () => {
|
|
2291
|
+
await client.action({
|
|
2292
|
+
actionType: "sanity.action.document.version.create",
|
|
2293
|
+
publishedId: targetDocumentPublishedId,
|
|
2294
|
+
document: newDoc
|
|
2295
|
+
});
|
|
2296
|
+
},
|
|
2297
|
+
{
|
|
2298
|
+
maxAttempts: 2,
|
|
2299
|
+
initialDelayMs: 0,
|
|
2300
|
+
retryableErrors: isNetworkError,
|
|
2301
|
+
context: {
|
|
2302
|
+
documentId: sourceDocument._id,
|
|
2303
|
+
targetLanguage,
|
|
2304
|
+
operation: "version.create"
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
);
|
|
2308
|
+
} catch (error) {
|
|
2309
|
+
if (translationMode === "openai-only" && isSanityMutationError(error)) {
|
|
2310
|
+
devLog.warn("Mutation error after OpenAI translation, retrying full translation", {
|
|
2311
|
+
documentId: sourceDocument._id,
|
|
2312
|
+
targetLanguage,
|
|
2313
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2314
|
+
}), previousError = error instanceof Error ? error : new Error(String(error));
|
|
2315
|
+
const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure.`;
|
|
2316
|
+
if (translatedDoc = await performTranslation(errorContext), !translatedDoc)
|
|
2317
|
+
throw new Error("Translation retry failed: no document returned");
|
|
2318
|
+
let retryTransformedResult;
|
|
2319
|
+
Object.keys(originalStructure).length > 0 ? retryTransformedResult = restoreTransforms({
|
|
2320
|
+
translatedDocument: translatedDoc,
|
|
2321
|
+
originalStructure,
|
|
2322
|
+
registry: globalTransformRegistry,
|
|
2323
|
+
schemaType: sourceDocument._type
|
|
2324
|
+
}) : retryTransformedResult = translatedDoc;
|
|
2325
|
+
const retryNewDoc = {
|
|
2326
|
+
...restoreExcludedFields(retryTransformedResult, excludedValuesToRestore),
|
|
2327
|
+
_id: targetDocumentVersionId
|
|
2328
|
+
};
|
|
2329
|
+
return await retryWithBackoff(
|
|
2330
|
+
async () => {
|
|
2331
|
+
await client.action({
|
|
2332
|
+
actionType: "sanity.action.document.version.create",
|
|
2333
|
+
publishedId: targetDocumentPublishedId,
|
|
2334
|
+
document: retryNewDoc
|
|
2335
|
+
});
|
|
2336
|
+
},
|
|
2337
|
+
{
|
|
2338
|
+
maxAttempts: 2,
|
|
2339
|
+
initialDelayMs: 0,
|
|
2340
|
+
retryableErrors: isNetworkError,
|
|
2341
|
+
context: {
|
|
2342
|
+
documentId: sourceDocument._id,
|
|
2343
|
+
targetLanguage,
|
|
2344
|
+
operation: "version.create",
|
|
2345
|
+
isRetry: !0
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
), retryNewDoc;
|
|
2349
|
+
}
|
|
2350
|
+
throw error;
|
|
2351
|
+
}
|
|
2352
|
+
return newDoc;
|
|
2974
2353
|
}, translateDocument = async ({
|
|
2975
2354
|
dispatch,
|
|
2976
2355
|
client,
|
|
@@ -2978,84 +2357,66 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2978
2357
|
sourceLanguage,
|
|
2979
2358
|
targetLanguage,
|
|
2980
2359
|
languageFieldName,
|
|
2981
|
-
shouldOverwriteFreshTranslations,
|
|
2982
2360
|
schemaId,
|
|
2983
2361
|
translationPrompts,
|
|
2984
2362
|
translationMode,
|
|
2985
2363
|
openaiConfig,
|
|
2986
2364
|
releaseId,
|
|
2987
|
-
excludeFields
|
|
2365
|
+
excludeFields,
|
|
2366
|
+
replaceExisting,
|
|
2367
|
+
mainSourceDocumentId
|
|
2988
2368
|
}) => {
|
|
2989
2369
|
const logContext = {
|
|
2990
2370
|
documentId: sourceDocument._id,
|
|
2991
2371
|
targetLanguage,
|
|
2992
2372
|
schemaId,
|
|
2993
|
-
shouldOverwriteFreshTranslations,
|
|
2994
2373
|
translationMode
|
|
2995
2374
|
};
|
|
2996
|
-
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
|
|
2375
|
+
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation", void 0, mainSourceDocumentId);
|
|
2997
2376
|
try {
|
|
2998
2377
|
const translationMetadata = await getTranslationMetadata({
|
|
2999
2378
|
client,
|
|
3000
2379
|
sourceDocument,
|
|
3001
2380
|
sourceLanguage
|
|
3002
|
-
}),
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
2381
|
+
}), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
|
|
2382
|
+
dispatch,
|
|
2383
|
+
client,
|
|
2384
|
+
schemaId,
|
|
2385
|
+
sourceDocument,
|
|
2386
|
+
targetLanguage,
|
|
2387
|
+
existingTranslationId,
|
|
2388
|
+
languageFieldName,
|
|
2389
|
+
translationPrompts,
|
|
2390
|
+
translationMode,
|
|
2391
|
+
openaiConfig,
|
|
2392
|
+
releaseId,
|
|
2393
|
+
excludeFields,
|
|
2394
|
+
replaceExisting,
|
|
2395
|
+
mainSourceDocumentId
|
|
2396
|
+
});
|
|
2397
|
+
if (!newDocument) {
|
|
2398
|
+
devLog.error("Translation failed: no document returned", new Error("Translation returned null"), logContext), setJobStatus(
|
|
3013
2399
|
dispatch,
|
|
3014
|
-
|
|
3015
|
-
schemaId,
|
|
3016
|
-
sourceDocument,
|
|
2400
|
+
sourceDocument._id,
|
|
3017
2401
|
targetLanguage,
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
excludeFields
|
|
3025
|
-
}),
|
|
3026
|
-
{
|
|
3027
|
-
maxRetries: 3,
|
|
3028
|
-
initialDelayMs: 1e3,
|
|
3029
|
-
isRetryable: isRetryableError,
|
|
3030
|
-
onRetry: (attempt, error, delayMs) => {
|
|
3031
|
-
devLog.warn("Translation failed, scheduling retry", {
|
|
3032
|
-
...logContext,
|
|
3033
|
-
attempt,
|
|
3034
|
-
delayMs,
|
|
3035
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3036
|
-
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "retrying", {
|
|
3037
|
-
retryCount: attempt,
|
|
3038
|
-
errorMessage: error instanceof Error ? error.message : String(error)
|
|
3039
|
-
});
|
|
3040
|
-
}
|
|
3041
|
-
}
|
|
3042
|
-
);
|
|
3043
|
-
if (!translationResult.success || !translationResult.result) {
|
|
3044
|
-
const errorMessage = translationResult.error instanceof Error ? translationResult.error.message : String(translationResult.error || "Translation failed");
|
|
3045
|
-
devLog.error("Translation failed after retries", translationResult.error, {
|
|
3046
|
-
...logContext,
|
|
3047
|
-
attempts: translationResult.attempts
|
|
3048
|
-
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
3049
|
-
errorMessage,
|
|
3050
|
-
retryCount: translationResult.attempts - 1
|
|
3051
|
-
// -1 because attempts includes initial try
|
|
3052
|
-
}), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
2402
|
+
"error",
|
|
2403
|
+
{
|
|
2404
|
+
errorMessage: "Translation failed"
|
|
2405
|
+
},
|
|
2406
|
+
mainSourceDocumentId
|
|
2407
|
+
), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
3053
2408
|
return;
|
|
3054
2409
|
}
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
2410
|
+
return setJobStatus(
|
|
2411
|
+
dispatch,
|
|
2412
|
+
sourceDocument._id,
|
|
2413
|
+
targetLanguage,
|
|
2414
|
+
"updating_metadata",
|
|
2415
|
+
{
|
|
2416
|
+
targetId: newDocument._id
|
|
2417
|
+
},
|
|
2418
|
+
mainSourceDocumentId
|
|
2419
|
+
), await updateTranslationMetadata({
|
|
3059
2420
|
client,
|
|
3060
2421
|
sourceDocument,
|
|
3061
2422
|
sourceLanguage,
|
|
@@ -3063,17 +2424,31 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3063
2424
|
targetLanguage,
|
|
3064
2425
|
translationMetadata,
|
|
3065
2426
|
isNewTranslation: !existingTranslationId
|
|
3066
|
-
}), setJobStatus(
|
|
3067
|
-
|
|
3068
|
-
|
|
2427
|
+
}), setJobStatus(
|
|
2428
|
+
dispatch,
|
|
2429
|
+
sourceDocument._id,
|
|
2430
|
+
targetLanguage,
|
|
2431
|
+
"completed",
|
|
2432
|
+
{
|
|
2433
|
+
targetId: newDocument._id
|
|
2434
|
+
},
|
|
2435
|
+
mainSourceDocumentId
|
|
2436
|
+
), devLog.info("Translation completed successfully", {
|
|
3069
2437
|
...logContext,
|
|
3070
2438
|
newDocumentId: newDocument._id
|
|
3071
2439
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
3072
2440
|
} catch (error) {
|
|
3073
2441
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3074
|
-
devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(
|
|
3075
|
-
|
|
3076
|
-
|
|
2442
|
+
devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(
|
|
2443
|
+
dispatch,
|
|
2444
|
+
sourceDocument._id,
|
|
2445
|
+
targetLanguage,
|
|
2446
|
+
"error",
|
|
2447
|
+
{
|
|
2448
|
+
errorMessage
|
|
2449
|
+
},
|
|
2450
|
+
mainSourceDocumentId
|
|
2451
|
+
), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
3077
2452
|
return;
|
|
3078
2453
|
}
|
|
3079
2454
|
}, translationSemaphore = new Semaphore(2), translateDocumentSet = async ({
|
|
@@ -3082,13 +2457,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3082
2457
|
targetLanguages,
|
|
3083
2458
|
dispatch,
|
|
3084
2459
|
languageFieldName,
|
|
3085
|
-
shouldOverwriteFreshTranslations,
|
|
3086
2460
|
schemaId,
|
|
3087
2461
|
references = [],
|
|
3088
2462
|
maxConcurrentTranslationJobs,
|
|
3089
2463
|
translationMode,
|
|
3090
2464
|
openaiConfig,
|
|
3091
|
-
excludeFields
|
|
2465
|
+
excludeFields,
|
|
2466
|
+
releaseConfig
|
|
3092
2467
|
}) => {
|
|
3093
2468
|
const logContext = {
|
|
3094
2469
|
documentId: sourceDocument._id,
|
|
@@ -3097,28 +2472,48 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3097
2472
|
referencesCount: references.length,
|
|
3098
2473
|
maxConcurrentJobs: maxConcurrentTranslationJobs,
|
|
3099
2474
|
translationMode,
|
|
3100
|
-
|
|
2475
|
+
releaseMode: releaseConfig.mode
|
|
3101
2476
|
};
|
|
3102
|
-
let releaseId;
|
|
2477
|
+
let releaseId, createdNewRelease = !1;
|
|
3103
2478
|
try {
|
|
3104
2479
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
3105
2480
|
if (!sourceLanguage)
|
|
3106
2481
|
throw devLog.error("Source language not found"), new Error("Source language not found");
|
|
3107
|
-
maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
2482
|
+
if (maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
3108
2483
|
type: "SET_OVERALL_STATUS",
|
|
3109
2484
|
payload: "translating"
|
|
3110
|
-
})
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
}
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
2485
|
+
}), releaseConfig.mode === "existing" && releaseConfig.existingReleaseId)
|
|
2486
|
+
releaseId = releaseConfig.existingReleaseId, devLog.info("Using existing Content Release", {
|
|
2487
|
+
...logContext,
|
|
2488
|
+
releaseId
|
|
2489
|
+
});
|
|
2490
|
+
else {
|
|
2491
|
+
const releaseTitle = releaseConfig.newReleaseTitle, releaseDescription = `AI translation batch created on ${(/* @__PURE__ */ new Date()).toLocaleDateString()} at ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`;
|
|
2492
|
+
releaseId = (await retryWithBackoff(
|
|
2493
|
+
async () => await client.releases.create({
|
|
2494
|
+
metadata: {
|
|
2495
|
+
title: releaseTitle,
|
|
2496
|
+
description: releaseDescription,
|
|
2497
|
+
releaseType: "undecided"
|
|
2498
|
+
}
|
|
2499
|
+
}),
|
|
2500
|
+
{
|
|
2501
|
+
maxAttempts: 2,
|
|
2502
|
+
initialDelayMs: 0,
|
|
2503
|
+
retryableErrors: isNetworkError,
|
|
2504
|
+
context: {
|
|
2505
|
+
operation: "releases.create",
|
|
2506
|
+
releaseTitle,
|
|
2507
|
+
...logContext
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
)).releaseId, createdNewRelease = !0, devLog.info("Content Release created", {
|
|
2511
|
+
...logContext,
|
|
2512
|
+
releaseId,
|
|
2513
|
+
releaseTitle
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
registerActiveTranslation(sourceDocument._id, releaseId), updateActiveTranslationStatus(sourceDocument._id, "translating");
|
|
3122
2517
|
const sourceDocuments = [
|
|
3123
2518
|
{
|
|
3124
2519
|
_id: sourceDocument._id,
|
|
@@ -3133,7 +2528,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3133
2528
|
targetLanguages,
|
|
3134
2529
|
sourceDocument
|
|
3135
2530
|
});
|
|
3136
|
-
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
|
|
2531
|
+
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages, sourceDocument._id);
|
|
3137
2532
|
const translationTasks = sourceDocumentIds.flatMap(
|
|
3138
2533
|
(documentId) => targetLanguages.map(async (targetLanguage) => {
|
|
3139
2534
|
try {
|
|
@@ -3141,7 +2536,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3141
2536
|
const document = await client.withConfig({
|
|
3142
2537
|
perspective: "drafts"
|
|
3143
2538
|
}).getDocument(documentId);
|
|
3144
|
-
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document &&
|
|
2539
|
+
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId) {
|
|
3145
2540
|
const versionedTargetDocumentId = await translateDocument({
|
|
3146
2541
|
dispatch,
|
|
3147
2542
|
client,
|
|
@@ -3149,13 +2544,15 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3149
2544
|
sourceLanguage,
|
|
3150
2545
|
targetLanguage,
|
|
3151
2546
|
languageFieldName,
|
|
3152
|
-
shouldOverwriteFreshTranslations,
|
|
3153
2547
|
schemaId,
|
|
3154
2548
|
translationPrompts,
|
|
3155
2549
|
translationMode,
|
|
3156
2550
|
openaiConfig,
|
|
3157
|
-
releaseId
|
|
3158
|
-
excludeFields
|
|
2551
|
+
releaseId,
|
|
2552
|
+
excludeFields,
|
|
2553
|
+
replaceExisting: releaseConfig.replaceExisting,
|
|
2554
|
+
// Pass main source document ID for active translations tracking
|
|
2555
|
+
mainSourceDocumentId: sourceDocument._id
|
|
3159
2556
|
});
|
|
3160
2557
|
versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
|
|
3161
2558
|
}
|
|
@@ -3182,7 +2579,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3182
2579
|
}), dispatch({
|
|
3183
2580
|
type: "SET_OVERALL_STATUS",
|
|
3184
2581
|
payload: "linking_references"
|
|
3185
|
-
}), await Promise.all(
|
|
2582
|
+
}), updateActiveTranslationStatus(sourceDocument._id, "linking_references"), await Promise.all(
|
|
3186
2583
|
versionedTargetDocumentIds.map(async (documentId) => {
|
|
3187
2584
|
await localizeReferences({
|
|
3188
2585
|
client,
|
|
@@ -3193,18 +2590,65 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3193
2590
|
})
|
|
3194
2591
|
);
|
|
3195
2592
|
const hasAnyFailures = failed > 0;
|
|
3196
|
-
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", {
|
|
2593
|
+
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", {
|
|
3197
2594
|
...logContext,
|
|
3198
2595
|
succeeded,
|
|
3199
2596
|
failed
|
|
3200
|
-
}), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
|
|
2597
|
+
}), 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();
|
|
3201
2598
|
} catch (error) {
|
|
3202
|
-
|
|
2599
|
+
if (devLog.error(
|
|
3203
2600
|
"translateDocumentSet failed with unexpected error",
|
|
3204
2601
|
error instanceof Error ? error : new Error(String(error)),
|
|
3205
2602
|
logContext
|
|
3206
|
-
),
|
|
2603
|
+
), releaseId && createdNewRelease)
|
|
2604
|
+
try {
|
|
2605
|
+
await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
|
|
2606
|
+
...logContext,
|
|
2607
|
+
releaseId
|
|
2608
|
+
});
|
|
2609
|
+
} catch (cleanupError) {
|
|
2610
|
+
devLog.error(
|
|
2611
|
+
"Failed to cleanup release",
|
|
2612
|
+
cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
|
|
2613
|
+
logContext
|
|
2614
|
+
);
|
|
2615
|
+
}
|
|
2616
|
+
throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id), error;
|
|
3207
2617
|
}
|
|
2618
|
+
}, ReleaseConflictWarning = ({ conflicts, onCancel, onReplace }) => {
|
|
2619
|
+
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]);
|
|
2620
|
+
return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2621
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
2622
|
+
/* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
|
|
2623
|
+
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "This release already contains translations for this content and target language(s)." })
|
|
2624
|
+
] }),
|
|
2625
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2626
|
+
"The selected release already has ",
|
|
2627
|
+
summaryText,
|
|
2628
|
+
" that will be overwritten if you continue:"
|
|
2629
|
+
] }),
|
|
2630
|
+
/* @__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: [
|
|
2631
|
+
conflict.sourceDocumentTitle || conflict.sourceDocumentId,
|
|
2632
|
+
conflict.sourceDocumentType && ` (${conflict.sourceDocumentType})`,
|
|
2633
|
+
" \u2192 ",
|
|
2634
|
+
conflict.targetLanguage
|
|
2635
|
+
] }, conflict.existingVersionId)) }) }) }),
|
|
2636
|
+
/* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2637
|
+
/* @__PURE__ */ jsx(Heading, { size: 0, children: "Choose an action:" }),
|
|
2638
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2639
|
+
/* @__PURE__ */ jsx("strong", { children: "Cancel" }),
|
|
2640
|
+
" \u2014 Go back and select a different release, or create a new one."
|
|
2641
|
+
] }),
|
|
2642
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2643
|
+
/* @__PURE__ */ jsx("strong", { children: "Replace" }),
|
|
2644
|
+
" \u2014 Delete the existing translations in this release and create new ones."
|
|
2645
|
+
] })
|
|
2646
|
+
] }),
|
|
2647
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", children: [
|
|
2648
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
|
|
2649
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "caution", text: "Replace Existing", onClick: onReplace })
|
|
2650
|
+
] })
|
|
2651
|
+
] }) });
|
|
3208
2652
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
3209
2653
|
const {
|
|
3210
2654
|
document,
|
|
@@ -3213,39 +2657,81 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3213
2657
|
dispatch,
|
|
3214
2658
|
references,
|
|
3215
2659
|
selectedTargetLanguages,
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }),
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
2660
|
+
translationMode,
|
|
2661
|
+
releaseConfig
|
|
2662
|
+
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1);
|
|
2663
|
+
useEffect(() => {
|
|
2664
|
+
releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
|
|
2665
|
+
}, [releaseConfig.mode, conflictContext]);
|
|
2666
|
+
const startTranslation = useCallback(
|
|
2667
|
+
(replaceExisting, overrideReleaseId) => {
|
|
2668
|
+
setConflictContext(null), translateDocumentSet({
|
|
2669
|
+
sourceDocument: document,
|
|
2670
|
+
targetLanguages: selectedTargetLanguages,
|
|
2671
|
+
client,
|
|
2672
|
+
dispatch,
|
|
2673
|
+
languageFieldName: config.languageFieldName,
|
|
2674
|
+
schemaId: config.schemaId,
|
|
2675
|
+
references,
|
|
2676
|
+
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
2677
|
+
maxReferenceDepth: config.maxReferenceDepth,
|
|
2678
|
+
translationMode,
|
|
2679
|
+
openaiConfig: config.openai,
|
|
2680
|
+
excludeFields: config.excludeFields,
|
|
2681
|
+
releaseConfig: {
|
|
2682
|
+
...releaseConfig,
|
|
2683
|
+
replaceExisting,
|
|
2684
|
+
// Use the override release ID if provided (from conflict context),
|
|
2685
|
+
// otherwise use the current releaseConfig value
|
|
2686
|
+
existingReleaseId: overrideReleaseId ?? releaseConfig.existingReleaseId
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2689
|
+
},
|
|
2690
|
+
[
|
|
2691
|
+
document,
|
|
2692
|
+
selectedTargetLanguages,
|
|
3222
2693
|
client,
|
|
3223
2694
|
dispatch,
|
|
3224
|
-
|
|
3225
|
-
|
|
2695
|
+
config.languageFieldName,
|
|
2696
|
+
config.schemaId,
|
|
2697
|
+
config.maxConcurrentTranslationJobs,
|
|
2698
|
+
config.maxReferenceDepth,
|
|
2699
|
+
config.openai,
|
|
2700
|
+
config.excludeFields,
|
|
3226
2701
|
references,
|
|
3227
|
-
shouldOverwriteFreshTranslations,
|
|
3228
|
-
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
3229
|
-
maxReferenceDepth: config.maxReferenceDepth,
|
|
3230
2702
|
translationMode,
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
2703
|
+
releaseConfig
|
|
2704
|
+
]
|
|
2705
|
+
), handleTranslate = useCallback(async () => {
|
|
2706
|
+
if (releaseConfig.mode === "new") {
|
|
2707
|
+
startTranslation(!1);
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
if (releaseConfig.mode === "existing" && releaseConfig.existingReleaseId) {
|
|
2711
|
+
const checkedReleaseId = releaseConfig.existingReleaseId;
|
|
2712
|
+
setIsCheckingConflicts(!0);
|
|
2713
|
+
const foundConflicts = await checkReleaseConflicts({
|
|
2714
|
+
client,
|
|
2715
|
+
releaseId: checkedReleaseId,
|
|
2716
|
+
sourceDocument: document,
|
|
2717
|
+
targetLanguages: selectedTargetLanguages,
|
|
2718
|
+
languageFieldName: config.languageFieldName,
|
|
2719
|
+
references
|
|
2720
|
+
});
|
|
2721
|
+
if (setIsCheckingConflicts(!1), foundConflicts.length > 0) {
|
|
2722
|
+
setConflictContext({
|
|
2723
|
+
releaseId: checkedReleaseId,
|
|
2724
|
+
conflicts: foundConflicts
|
|
2725
|
+
});
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
startTranslation(!1);
|
|
2730
|
+
}, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, references]), handleCancelConflict = useCallback(() => {
|
|
2731
|
+
setConflictContext(null);
|
|
2732
|
+
}, []), handleReplaceConflict = useCallback(() => {
|
|
2733
|
+
conflictContext && startTranslation(!0, conflictContext.releaseId);
|
|
2734
|
+
}, [startTranslation, conflictContext]), handleSelectAll = useCallback(() => {
|
|
3249
2735
|
dispatch({
|
|
3250
2736
|
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
3251
2737
|
payload: config.supportedLanguages.map((lang) => lang.id).filter((id) => id !== document.language)
|
|
@@ -3256,7 +2742,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3256
2742
|
payload: []
|
|
3257
2743
|
});
|
|
3258
2744
|
}, [dispatch]);
|
|
3259
|
-
return /* @__PURE__ */
|
|
2745
|
+
return conflictContext ? /* @__PURE__ */ jsx(Stack, { space: 4, children: /* @__PURE__ */ jsx(
|
|
2746
|
+
ReleaseConflictWarning,
|
|
2747
|
+
{
|
|
2748
|
+
conflicts: conflictContext.conflicts,
|
|
2749
|
+
onCancel: handleCancelConflict,
|
|
2750
|
+
onReplace: handleReplaceConflict
|
|
2751
|
+
}
|
|
2752
|
+
) }) : /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
|
|
3260
2753
|
/* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
|
|
3261
2754
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectAll, children: "Select All" }),
|
|
3262
2755
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectNone, children: "Select None" })
|
|
@@ -3270,16 +2763,41 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3270
2763
|
tone: "positive",
|
|
3271
2764
|
size: 2,
|
|
3272
2765
|
onClick: handleTranslate,
|
|
3273
|
-
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus),
|
|
3274
|
-
children: "Translate"
|
|
2766
|
+
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus) || releaseConfig.mode === "existing" && !releaseConfig.existingReleaseId || isCheckingConflicts,
|
|
2767
|
+
children: isCheckingConflicts ? "Checking..." : "Translate"
|
|
3275
2768
|
}
|
|
3276
2769
|
)
|
|
3277
2770
|
] })
|
|
3278
2771
|
] });
|
|
2772
|
+
}, TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
|
|
2773
|
+
const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
|
|
2774
|
+
params: {
|
|
2775
|
+
documentId: undraftId(documentId),
|
|
2776
|
+
translationMetadataType: TRANSLATION_METADATA_TYPE
|
|
2777
|
+
},
|
|
2778
|
+
initialValue: null
|
|
2779
|
+
});
|
|
2780
|
+
return useEffect(() => {
|
|
2781
|
+
if (loading) {
|
|
2782
|
+
dispatch({ type: "SET_OVERALL_STATUS", payload: "fetching_metadata" });
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
if (error) {
|
|
2786
|
+
dispatch({ type: "SET_OVERALL_STATUS", payload: "error" });
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
if (data) {
|
|
2790
|
+
dispatch({ type: "SET_TRANSLATION_METADATA", payload: data }), dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
|
|
2794
|
+
}, [data, loading, error, dispatch]), {
|
|
2795
|
+
data,
|
|
2796
|
+
loading,
|
|
2797
|
+
error
|
|
2798
|
+
};
|
|
3279
2799
|
}, LanguageCheckbox = ({ lang, isLoading }) => {
|
|
3280
|
-
const { document, dispatch, selectedTargetLanguages
|
|
3281
|
-
(history) => history.language === lang.id && history.sourceRev !== document._rev
|
|
3282
|
-
), handleChange = () => {
|
|
2800
|
+
const { document, dispatch, selectedTargetLanguages } = useTranslationContext(), isDisabled = lang.id === document.language, isChecked = selectedTargetLanguages.includes(lang.id), handleChange = () => {
|
|
3283
2801
|
if (isDisabled) return;
|
|
3284
2802
|
const updatedLanguages = isChecked ? selectedTargetLanguages.filter((langId) => langId !== lang.id) : [...selectedTargetLanguages, lang.id];
|
|
3285
2803
|
dispatch({
|
|
@@ -3297,16 +2815,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3297
2815
|
disabled: isDisabled,
|
|
3298
2816
|
tone: isChecked ? "primary" : "default",
|
|
3299
2817
|
muted: !0,
|
|
3300
|
-
children: /* @__PURE__ */
|
|
3301
|
-
/* @__PURE__ */
|
|
3302
|
-
|
|
3303
|
-
/* @__PURE__ */
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
] }),
|
|
3308
|
-
!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" }) })
|
|
3309
|
-
] })
|
|
2818
|
+
children: /* @__PURE__ */ jsx(Flex, { align: "center", gap: 2, style: { cursor: "pointer" }, children: /* @__PURE__ */ jsxs(Inline, { space: 3, children: [
|
|
2819
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: isChecked, onChange: handleChange, label: lang.title, disabled: isDisabled }),
|
|
2820
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
2821
|
+
/* @__PURE__ */ jsx(Text, { children: lang.title }),
|
|
2822
|
+
isDisabled && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "The current language" })
|
|
2823
|
+
] })
|
|
2824
|
+
] }) })
|
|
3310
2825
|
}
|
|
3311
2826
|
);
|
|
3312
2827
|
}, LanguageSelectionPanel = () => {
|
|
@@ -3314,6 +2829,134 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3314
2829
|
documentId: document._id
|
|
3315
2830
|
});
|
|
3316
2831
|
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)) }) });
|
|
2832
|
+
}, ReleaseSelectionPanel = () => {
|
|
2833
|
+
const { dispatch, releaseConfig } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [releases, setReleases] = useState([]), [isLoading, setIsLoading] = useState(!1), [error, setError] = useState(null), [searchQuery, setSearchQuery] = useState("");
|
|
2834
|
+
useEffect(() => {
|
|
2835
|
+
(async () => {
|
|
2836
|
+
setIsLoading(!0), setError(null);
|
|
2837
|
+
try {
|
|
2838
|
+
const result = await client.fetch(
|
|
2839
|
+
'releases::all()[state == "active"] | order(_createdAt desc)'
|
|
2840
|
+
);
|
|
2841
|
+
setReleases(result), devLog.debug("Fetched active releases", {
|
|
2842
|
+
count: result.length,
|
|
2843
|
+
component: "ReleaseSelectionPanel"
|
|
2844
|
+
});
|
|
2845
|
+
} catch (err) {
|
|
2846
|
+
const errorMessage = err instanceof Error ? err.message : "Failed to fetch releases";
|
|
2847
|
+
setError(errorMessage), devLog.error("Failed to fetch releases", err instanceof Error ? err : new Error(errorMessage), {
|
|
2848
|
+
component: "ReleaseSelectionPanel"
|
|
2849
|
+
});
|
|
2850
|
+
} finally {
|
|
2851
|
+
setIsLoading(!1);
|
|
2852
|
+
}
|
|
2853
|
+
})();
|
|
2854
|
+
}, [client]);
|
|
2855
|
+
const handleModeChange = useCallback(
|
|
2856
|
+
(mode) => {
|
|
2857
|
+
dispatch({
|
|
2858
|
+
type: "SET_RELEASE_CONFIG",
|
|
2859
|
+
payload: { mode }
|
|
2860
|
+
});
|
|
2861
|
+
},
|
|
2862
|
+
[dispatch]
|
|
2863
|
+
), handleTitleChange = useCallback(
|
|
2864
|
+
(event) => {
|
|
2865
|
+
dispatch({
|
|
2866
|
+
type: "SET_RELEASE_CONFIG",
|
|
2867
|
+
payload: { newReleaseTitle: event.target.value }
|
|
2868
|
+
});
|
|
2869
|
+
},
|
|
2870
|
+
[dispatch]
|
|
2871
|
+
), handleSelectRelease = useCallback(
|
|
2872
|
+
(releaseId) => {
|
|
2873
|
+
dispatch({
|
|
2874
|
+
type: "SET_RELEASE_CONFIG",
|
|
2875
|
+
payload: { existingReleaseId: releaseId }
|
|
2876
|
+
});
|
|
2877
|
+
},
|
|
2878
|
+
[dispatch]
|
|
2879
|
+
), handleSearchChange = useCallback((event) => {
|
|
2880
|
+
setSearchQuery(event.target.value);
|
|
2881
|
+
}, []), filteredReleases = useMemo(() => {
|
|
2882
|
+
if (!searchQuery.trim())
|
|
2883
|
+
return releases;
|
|
2884
|
+
const query = searchQuery.toLowerCase();
|
|
2885
|
+
return releases.filter((release) => release.metadata.title.toLowerCase().includes(query));
|
|
2886
|
+
}, [releases, searchQuery]), formatDate = (dateString) => new Date(dateString).toLocaleDateString(void 0, {
|
|
2887
|
+
month: "short",
|
|
2888
|
+
day: "numeric",
|
|
2889
|
+
year: "numeric"
|
|
2890
|
+
});
|
|
2891
|
+
return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2892
|
+
/* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
2893
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, as: "label", style: { cursor: "pointer" }, onClick: () => handleModeChange("new"), children: [
|
|
2894
|
+
/* @__PURE__ */ jsx(Radio, { checked: releaseConfig.mode === "new", readOnly: !0 }),
|
|
2895
|
+
/* @__PURE__ */ jsx(Text, { weight: "medium", children: "Create new release" })
|
|
2896
|
+
] }),
|
|
2897
|
+
/* @__PURE__ */ jsxs(
|
|
2898
|
+
Flex,
|
|
2899
|
+
{
|
|
2900
|
+
align: "center",
|
|
2901
|
+
gap: 3,
|
|
2902
|
+
as: "label",
|
|
2903
|
+
style: { cursor: "pointer" },
|
|
2904
|
+
onClick: () => handleModeChange("existing"),
|
|
2905
|
+
children: [
|
|
2906
|
+
/* @__PURE__ */ jsx(Radio, { checked: releaseConfig.mode === "existing", readOnly: !0 }),
|
|
2907
|
+
/* @__PURE__ */ jsx(Text, { weight: "medium", children: "Add to existing release" })
|
|
2908
|
+
]
|
|
2909
|
+
}
|
|
2910
|
+
)
|
|
2911
|
+
] }),
|
|
2912
|
+
releaseConfig.mode === "new" && /* @__PURE__ */ jsx(
|
|
2913
|
+
TextInput,
|
|
2914
|
+
{
|
|
2915
|
+
value: releaseConfig.newReleaseTitle,
|
|
2916
|
+
onChange: handleTitleChange,
|
|
2917
|
+
placeholder: "Release title..."
|
|
2918
|
+
}
|
|
2919
|
+
),
|
|
2920
|
+
releaseConfig.mode === "existing" && /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
2921
|
+
isLoading && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
2922
|
+
/* @__PURE__ */ jsx(Spinner, { muted: !0 }),
|
|
2923
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Loading..." })
|
|
2924
|
+
] }),
|
|
2925
|
+
error && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: error }),
|
|
2926
|
+
!isLoading && !error && releases.length === 0 && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "No active releases found." }),
|
|
2927
|
+
!isLoading && !error && releases.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2928
|
+
/* @__PURE__ */ jsx(
|
|
2929
|
+
TextInput,
|
|
2930
|
+
{
|
|
2931
|
+
icon: SearchIcon,
|
|
2932
|
+
value: searchQuery,
|
|
2933
|
+
onChange: handleSearchChange,
|
|
2934
|
+
placeholder: "Search releases..."
|
|
2935
|
+
}
|
|
2936
|
+
),
|
|
2937
|
+
/* @__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) => {
|
|
2938
|
+
const releaseName = release._id.replace("_.releases.", ""), isSelected = releaseConfig.existingReleaseId === releaseName;
|
|
2939
|
+
return /* @__PURE__ */ jsx(
|
|
2940
|
+
Card,
|
|
2941
|
+
{
|
|
2942
|
+
padding: 3,
|
|
2943
|
+
radius: 2,
|
|
2944
|
+
tone: isSelected ? "primary" : "default",
|
|
2945
|
+
style: { cursor: "pointer" },
|
|
2946
|
+
onClick: () => handleSelectRelease(releaseName),
|
|
2947
|
+
border: !0,
|
|
2948
|
+
muted: isSelected,
|
|
2949
|
+
children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
2950
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: release.metadata.title }),
|
|
2951
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: formatDate(release._createdAt) })
|
|
2952
|
+
] })
|
|
2953
|
+
},
|
|
2954
|
+
release._id
|
|
2955
|
+
);
|
|
2956
|
+
}) }) })
|
|
2957
|
+
] })
|
|
2958
|
+
] })
|
|
2959
|
+
] }) });
|
|
3317
2960
|
}, TranslationModePanel = () => {
|
|
3318
2961
|
const { config, translationMode, openaiValidated, dispatch } = useTranslationContext();
|
|
3319
2962
|
if (!config?.openai?.enabled || !config?.openai?.apiKey)
|
|
@@ -3332,7 +2975,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3332
2975
|
return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3333
2976
|
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3334
2977
|
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "Translation Mode" }),
|
|
3335
|
-
/* @__PURE__ */ jsx(Badge, { tone: "positive", children: "
|
|
2978
|
+
/* @__PURE__ */ jsx(Badge, { tone: "positive", children: "OpenAI Available" })
|
|
3336
2979
|
] }),
|
|
3337
2980
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3338
2981
|
/* @__PURE__ */ jsx(
|
|
@@ -3362,32 +3005,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3362
3005
|
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "openai-only", readOnly: !0 }),
|
|
3363
3006
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3364
3007
|
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3365
|
-
/* @__PURE__ */ jsx(Text, { children: "
|
|
3008
|
+
/* @__PURE__ */ jsx(Text, { children: "OpenAI Only" }),
|
|
3366
3009
|
/* @__PURE__ */ jsx(Badge, { tone: "caution", children: "Beta" })
|
|
3367
3010
|
] }),
|
|
3368
3011
|
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Full style guide capacity with 1M token context window." })
|
|
3369
3012
|
] })
|
|
3370
3013
|
] })
|
|
3371
3014
|
}
|
|
3372
|
-
),
|
|
3373
|
-
/* @__PURE__ */ jsx(
|
|
3374
|
-
Button,
|
|
3375
|
-
{
|
|
3376
|
-
type: "button",
|
|
3377
|
-
mode: translationMode === "sanity-with-review" ? "ghost" : "bleed",
|
|
3378
|
-
tone: translationMode === "sanity-with-review" ? "primary" : "default",
|
|
3379
|
-
onClick: () => handleModeChange("sanity-with-review"),
|
|
3380
|
-
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3381
|
-
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "sanity-with-review", readOnly: !0 }),
|
|
3382
|
-
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3383
|
-
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3384
|
-
/* @__PURE__ */ jsx(Text, { children: "Sanity + GPT-5 Review" }),
|
|
3385
|
-
/* @__PURE__ */ jsx(Badge, { tone: "primary", children: "Highest Quality" })
|
|
3386
|
-
] }),
|
|
3387
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Sanity translation refined by GPT-5. Takes longer but best results." })
|
|
3388
|
-
] })
|
|
3389
|
-
] })
|
|
3390
|
-
}
|
|
3391
3015
|
)
|
|
3392
3016
|
] }),
|
|
3393
3017
|
translationMode !== "sanity-only" && /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 3, border: !0, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
@@ -3550,41 +3174,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3550
3174
|
] }),
|
|
3551
3175
|
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
|
|
3552
3176
|
] });
|
|
3553
|
-
}, WriteSettingsPanel = () => {
|
|
3554
|
-
const { dispatch, shouldOverwriteFreshTranslations } = useTranslationContext(), handleShouldOverwriteFreshTranslationsChange = () => {
|
|
3555
|
-
dispatch({
|
|
3556
|
-
type: "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS",
|
|
3557
|
-
payload: !shouldOverwriteFreshTranslations
|
|
3558
|
-
});
|
|
3559
|
-
};
|
|
3560
|
-
return /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3561
|
-
/* @__PURE__ */ jsx(ReferenceConfigPanel, {}),
|
|
3562
|
-
/* @__PURE__ */ jsx(Flex, { justify: "space-between", align: "center", children: /* @__PURE__ */ jsx(
|
|
3563
|
-
Button,
|
|
3564
|
-
{
|
|
3565
|
-
type: "button",
|
|
3566
|
-
mode: shouldOverwriteFreshTranslations ? "ghost" : "bleed",
|
|
3567
|
-
size: 2,
|
|
3568
|
-
onClick: handleShouldOverwriteFreshTranslationsChange,
|
|
3569
|
-
tone: shouldOverwriteFreshTranslations ? "primary" : "neutral",
|
|
3570
|
-
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3571
|
-
/* @__PURE__ */ jsx(
|
|
3572
|
-
Checkbox,
|
|
3573
|
-
{
|
|
3574
|
-
checked: shouldOverwriteFreshTranslations,
|
|
3575
|
-
onChange: handleShouldOverwriteFreshTranslationsChange,
|
|
3576
|
-
label: shouldOverwriteFreshTranslations ? "Disable" : "Enable"
|
|
3577
|
-
}
|
|
3578
|
-
),
|
|
3579
|
-
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3580
|
-
/* @__PURE__ */ jsx(Text, { children: "Overwrite Fresh Translations" }),
|
|
3581
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "All existing translations will be overwritten even if they are fresh." })
|
|
3582
|
-
] })
|
|
3583
|
-
] })
|
|
3584
|
-
}
|
|
3585
|
-
) })
|
|
3586
|
-
] });
|
|
3587
|
-
}, TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
3177
|
+
}, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
3588
3178
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3589
3179
|
/* @__PURE__ */ jsx(Label, { children: "Languages" }),
|
|
3590
3180
|
/* @__PURE__ */ jsx(LanguageSelectionPanel, {})
|
|
@@ -3593,16 +3183,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3593
3183
|
/* @__PURE__ */ jsx(Label, { children: "Translation Method" }),
|
|
3594
3184
|
/* @__PURE__ */ jsx(TranslationModePanel, {})
|
|
3595
3185
|
] }),
|
|
3186
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3187
|
+
/* @__PURE__ */ jsx(Label, { children: "Content Release" }),
|
|
3188
|
+
/* @__PURE__ */ jsx(ReleaseSelectionPanel, {})
|
|
3189
|
+
] }),
|
|
3596
3190
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3597
3191
|
/* @__PURE__ */ jsx(Label, { children: "Settings" }),
|
|
3598
3192
|
/* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsx(WriteSettingsPanel, {}) })
|
|
3599
3193
|
] }),
|
|
3600
3194
|
/* @__PURE__ */ jsx(ActionButtonBar, { setIsDialogOpen })
|
|
3601
3195
|
] }), TERMINAL_STATUSES = ["completed", "skipped", "error"], TranslationStatusPanel = () => {
|
|
3602
|
-
const { overallStatus, translationJobs, translationMode } = useTranslationContext(), headline = useMemo(
|
|
3603
|
-
()
|
|
3604
|
-
|
|
3605
|
-
), jobs = useMemo(() => Array.from(translationJobs.values()), [translationJobs]), openaiStats = useMemo(() => {
|
|
3196
|
+
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(() => {
|
|
3197
|
+
forceCleanupActiveTranslation(document._id), dispatch({ type: "RESET_TRANSLATION_STATE" });
|
|
3198
|
+
}, [dispatch, document._id]), openaiStats = useMemo(() => {
|
|
3606
3199
|
if (translationMode === "sanity-only") return null;
|
|
3607
3200
|
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);
|
|
3608
3201
|
return { totalTokens, totalCost, jobsWithUsage: completedJobs.filter((j) => j.tokensUsed).length };
|
|
@@ -3636,17 +3229,12 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3636
3229
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3637
3230
|
"Error: ",
|
|
3638
3231
|
job.errorMessage
|
|
3639
|
-
] }) }) : null,
|
|
3640
|
-
job.status === "retrying" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3641
|
-
"Retrying after: ",
|
|
3642
|
-
job.errorMessage
|
|
3643
3232
|
] }) }) : null
|
|
3644
3233
|
] })
|
|
3645
3234
|
] }),
|
|
3646
3235
|
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
3647
3236
|
job.status === "skipped" ? /* @__PURE__ */ jsx(SkippedBadge, {}) : null,
|
|
3648
|
-
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {
|
|
3649
|
-
job.status === "retrying" ? /* @__PURE__ */ jsx(RetryingBadge, { retryCount: job.retryCount }) : null,
|
|
3237
|
+
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {}) : null,
|
|
3650
3238
|
job.targetId ? /* @__PURE__ */ jsx(
|
|
3651
3239
|
TranslationOpenerButton,
|
|
3652
3240
|
{
|
|
@@ -3663,18 +3251,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3663
3251
|
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Usage Summary" }),
|
|
3664
3252
|
/* @__PURE__ */ jsxs(Inline, { space: 4, children: [
|
|
3665
3253
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3666
|
-
"
|
|
3254
|
+
"Total Tokens: ",
|
|
3667
3255
|
/* @__PURE__ */ jsx("strong", { children: openaiStats.totalTokens.toLocaleString() })
|
|
3668
3256
|
] }),
|
|
3669
3257
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3670
|
-
"
|
|
3258
|
+
"Total Cost: ",
|
|
3671
3259
|
/* @__PURE__ */ jsxs("strong", { children: [
|
|
3672
3260
|
"$",
|
|
3673
3261
|
openaiStats.totalCost.toFixed(4)
|
|
3674
3262
|
] })
|
|
3675
3263
|
] })
|
|
3676
3264
|
] })
|
|
3677
|
-
] }) })
|
|
3265
|
+
] }) }),
|
|
3266
|
+
isTerminalState && /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "primary", text: "Translate More", icon: ResetIcon, onClick: handleReset }) })
|
|
3678
3267
|
] });
|
|
3679
3268
|
}, ProgressBar = ({ progress, max = 100 }) => {
|
|
3680
3269
|
const percentage = Math.min(progress / max * 100, 100);
|
|
@@ -3726,35 +3315,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3726
3315
|
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." }) }),
|
|
3727
3316
|
children: /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: "Skipped" })
|
|
3728
3317
|
}
|
|
3729
|
-
), ErrorBadge = (
|
|
3730
|
-
Tooltip,
|
|
3731
|
-
{
|
|
3732
|
-
portal: !0,
|
|
3733
|
-
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." }) }),
|
|
3734
|
-
children: /* @__PURE__ */ jsxs(Badge, { tone: "critical", children: [
|
|
3735
|
-
"Failed",
|
|
3736
|
-
retryCount && retryCount > 0 ? ` (${retryCount} retries)` : ""
|
|
3737
|
-
] })
|
|
3738
|
-
}
|
|
3739
|
-
), RetryingBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
|
|
3318
|
+
), ErrorBadge = () => /* @__PURE__ */ jsx(
|
|
3740
3319
|
Tooltip,
|
|
3741
3320
|
{
|
|
3742
3321
|
portal: !0,
|
|
3743
|
-
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "
|
|
3744
|
-
children: /* @__PURE__ */
|
|
3745
|
-
"Retry ",
|
|
3746
|
-
retryCount || 1
|
|
3747
|
-
] })
|
|
3322
|
+
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "This translation failed. Check the error message for details." }) }),
|
|
3323
|
+
children: /* @__PURE__ */ jsx(Badge, { tone: "critical", children: "Failed" })
|
|
3748
3324
|
}
|
|
3749
3325
|
), getStatusLabel = (status) => ({
|
|
3750
3326
|
idle: "Idle",
|
|
3751
3327
|
queued: "Queued...",
|
|
3752
3328
|
preparing_translation: "Preparing translation...",
|
|
3753
3329
|
translating: "Translating with Sanity Agent...",
|
|
3754
|
-
translating_with_openai: "Translating with
|
|
3755
|
-
reviewing_with_openai: "Reviewing translation with GPT-5...",
|
|
3330
|
+
translating_with_openai: "Translating with OpenAI...",
|
|
3756
3331
|
updating_metadata: "Saving metadata...",
|
|
3757
|
-
retrying: "Retrying...",
|
|
3758
3332
|
error: "Error",
|
|
3759
3333
|
skipped: "Skipped",
|
|
3760
3334
|
completed: "Completed"
|
|
@@ -3762,14 +3336,30 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3762
3336
|
"translating",
|
|
3763
3337
|
"linking_references",
|
|
3764
3338
|
"completed"
|
|
3765
|
-
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
|
|
3339
|
+
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen, onStatusChange }) => {
|
|
3766
3340
|
const { overallStatus, document } = useTranslationContext();
|
|
3767
|
-
return
|
|
3341
|
+
return useEffect(() => {
|
|
3342
|
+
onStatusChange?.(overallStatus);
|
|
3343
|
+
}, [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", {
|
|
3768
3344
|
documentId: document._id,
|
|
3769
3345
|
overallStatus
|
|
3770
3346
|
}), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
|
|
3771
|
-
}, getTranslateAction = (config) => ({ draft, published }) => {
|
|
3772
|
-
const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1)
|
|
3347
|
+
}, IN_PROGRESS_STATUSES = ["translating", "linking_references"], getButtonLabel = (status) => status === "completed" ? "Translation Complete" : status && IN_PROGRESS_STATUSES.includes(status) ? "Translating..." : "Translate", getTranslateAction = (config) => ({ draft, published }) => {
|
|
3348
|
+
const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1), [currentStatus, setCurrentStatus] = useState(null), isTranslating = currentStatus !== null && IN_PROGRESS_STATUSES.includes(currentStatus), handleStatusChange = useCallback((status) => {
|
|
3349
|
+
setCurrentStatus(status);
|
|
3350
|
+
}, []), handleClose = useCallback(() => {
|
|
3351
|
+
if (isTranslating) {
|
|
3352
|
+
if (!window.confirm(
|
|
3353
|
+
"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."
|
|
3354
|
+
))
|
|
3355
|
+
return;
|
|
3356
|
+
devLog.warn("User closed dialog during active translation", {
|
|
3357
|
+
documentId: document?._id,
|
|
3358
|
+
component: "TranslateAction"
|
|
3359
|
+
});
|
|
3360
|
+
}
|
|
3361
|
+
setIsDialogOpen(!1);
|
|
3362
|
+
}, [isTranslating, document?._id]);
|
|
3773
3363
|
return devLog.debug("TranslateAction initialized", {
|
|
3774
3364
|
documentId: document?._id,
|
|
3775
3365
|
documentType: document?._type
|
|
@@ -3777,7 +3367,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3777
3367
|
documentType: document?._type,
|
|
3778
3368
|
translatableTypes: config.translatableSchemaTypes
|
|
3779
3369
|
}), null) : config ? {
|
|
3780
|
-
label:
|
|
3370
|
+
label: getButtonLabel(currentStatus),
|
|
3781
3371
|
icon: TranslateIcon,
|
|
3782
3372
|
onHandle: () => {
|
|
3783
3373
|
devLog.info("Translation dialog opened", {
|
|
@@ -3788,9 +3378,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3788
3378
|
},
|
|
3789
3379
|
dialog: isDialogOpen && {
|
|
3790
3380
|
type: "dialog",
|
|
3791
|
-
onClose:
|
|
3381
|
+
onClose: handleClose,
|
|
3792
3382
|
header: "Translate Document",
|
|
3793
|
-
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
|
|
3383
|
+
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen, onStatusChange: handleStatusChange }) })
|
|
3794
3384
|
}
|
|
3795
3385
|
} : (devLog.error("Missing config in TranslateAction"), null);
|
|
3796
3386
|
}, translationGlossaryTerm = defineType({
|
|
@@ -3906,7 +3496,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3906
3496
|
defineField({
|
|
3907
3497
|
name: "changeHistory",
|
|
3908
3498
|
title: "Change History",
|
|
3909
|
-
description: "History of changes made
|
|
3499
|
+
description: "History of changes made to translation settings.",
|
|
3910
3500
|
type: "array",
|
|
3911
3501
|
readOnly: !0,
|
|
3912
3502
|
group: "history",
|
|
@@ -4211,12 +3801,283 @@ function deserializeVTTCaptionText(translationText, originalMetadata) {
|
|
|
4211
3801
|
resultExample: result.length > 0 ? result[0] : null
|
|
4212
3802
|
}), result;
|
|
4213
3803
|
}
|
|
3804
|
+
const LOCALE_TO_ISO = {
|
|
3805
|
+
// English variants
|
|
3806
|
+
"en-us": "eng",
|
|
3807
|
+
"en-US": "eng",
|
|
3808
|
+
"en-gb": "eng",
|
|
3809
|
+
"en-GB": "eng",
|
|
3810
|
+
"en-ca": "eng",
|
|
3811
|
+
"en-CA": "eng",
|
|
3812
|
+
"en-au": "eng",
|
|
3813
|
+
"en-AU": "eng",
|
|
3814
|
+
// Spanish variants
|
|
3815
|
+
es: "spa",
|
|
3816
|
+
"es-es": "spa",
|
|
3817
|
+
"es-ES": "spa",
|
|
3818
|
+
"es-mx": "spa",
|
|
3819
|
+
"es-MX": "spa",
|
|
3820
|
+
// French variants
|
|
3821
|
+
fr: "fra",
|
|
3822
|
+
"fr-fr": "fra",
|
|
3823
|
+
"fr-FR": "fra",
|
|
3824
|
+
"fr-ca": "fra",
|
|
3825
|
+
"fr-CA": "fra",
|
|
3826
|
+
// German variants
|
|
3827
|
+
de: "ger",
|
|
3828
|
+
"de-de": "ger",
|
|
3829
|
+
"de-DE": "ger",
|
|
3830
|
+
"de-at": "ger",
|
|
3831
|
+
"de-AT": "ger",
|
|
3832
|
+
// Italian variants
|
|
3833
|
+
it: "ita",
|
|
3834
|
+
"it-it": "ita",
|
|
3835
|
+
"it-IT": "ita",
|
|
3836
|
+
// Portuguese variants
|
|
3837
|
+
pt: "por",
|
|
3838
|
+
"pt-br": "por",
|
|
3839
|
+
"pt-BR": "por",
|
|
3840
|
+
"pt-pt": "por",
|
|
3841
|
+
"pt-PT": "por",
|
|
3842
|
+
// Russian variants
|
|
3843
|
+
ru: "rus",
|
|
3844
|
+
"ru-ru": "rus",
|
|
3845
|
+
"ru-RU": "rus",
|
|
3846
|
+
// Japanese variants
|
|
3847
|
+
ja: "jpn",
|
|
3848
|
+
"ja-jp": "jpn",
|
|
3849
|
+
"ja-JP": "jpn",
|
|
3850
|
+
// Korean variants
|
|
3851
|
+
ko: "kor",
|
|
3852
|
+
"ko-kr": "kor",
|
|
3853
|
+
"ko-KR": "kor",
|
|
3854
|
+
// Chinese variants
|
|
3855
|
+
zh: "chi",
|
|
3856
|
+
"zh-cn": "chi",
|
|
3857
|
+
"zh-CN": "chi",
|
|
3858
|
+
"zh-tw": "chi",
|
|
3859
|
+
"zh-TW": "chi",
|
|
3860
|
+
"zh-hk": "chi",
|
|
3861
|
+
"zh-HK": "chi"
|
|
3862
|
+
}, normalizeToIsoLanguageCode = (code) => {
|
|
3863
|
+
const normalizedCode = code.toLowerCase();
|
|
3864
|
+
return LOCALE_TO_ISO[normalizedCode];
|
|
3865
|
+
}, wistiaCaptionTransform = {
|
|
3866
|
+
matcher: {
|
|
3867
|
+
fieldName: "wistiaCaptions"
|
|
3868
|
+
},
|
|
3869
|
+
toTranslatable: (wistiaCaptions, context) => {
|
|
3870
|
+
if (devLog.info("Wistia captions toTranslatable - input:", {
|
|
3871
|
+
fieldPath: context.fieldPath,
|
|
3872
|
+
hasCaptionContent: !!wistiaCaptions?.captionContent,
|
|
3873
|
+
captionContentLength: wistiaCaptions?.captionContent?.length ?? 0,
|
|
3874
|
+
isoLanguage: wistiaCaptions?.isoLanguage
|
|
3875
|
+
}), !wistiaCaptions || !wistiaCaptions.captionContent)
|
|
3876
|
+
return devLog.debug("Wistia caption field is missing or has no content, returning empty transform", {
|
|
3877
|
+
fieldPath: context.fieldPath,
|
|
3878
|
+
value: wistiaCaptions
|
|
3879
|
+
}), {
|
|
3880
|
+
_type: "translatable.wistia.captions",
|
|
3881
|
+
translationText: "",
|
|
3882
|
+
_originalMetadata: {
|
|
3883
|
+
captionFileName: wistiaCaptions?.captionFileName,
|
|
3884
|
+
isoLanguage: wistiaCaptions?.isoLanguage,
|
|
3885
|
+
captionUploadStatus: wistiaCaptions?.captionUploadStatus,
|
|
3886
|
+
captionUploadMessage: wistiaCaptions?.captionUploadMessage,
|
|
3887
|
+
_originalIndex: 0
|
|
3888
|
+
},
|
|
3889
|
+
_srtCues: []
|
|
3890
|
+
};
|
|
3891
|
+
devLog.info("Applying Wistia caption transform", {
|
|
3892
|
+
fieldPath: context.fieldPath,
|
|
3893
|
+
captionContentPreview: wistiaCaptions.captionContent.substring(0, 200)
|
|
3894
|
+
});
|
|
3895
|
+
const srtCues = parseSRTContent(wistiaCaptions.captionContent);
|
|
3896
|
+
devLog.debug("Parsed SRT cues:", {
|
|
3897
|
+
fieldPath: context.fieldPath,
|
|
3898
|
+
cueCount: srtCues.length,
|
|
3899
|
+
firstCue: srtCues.length > 0 ? srtCues[0] : null
|
|
3900
|
+
});
|
|
3901
|
+
const translationText = serializeWistiaCaptionText(srtCues), originalMetadata = {
|
|
3902
|
+
captionFileName: wistiaCaptions.captionFileName,
|
|
3903
|
+
isoLanguage: wistiaCaptions.isoLanguage,
|
|
3904
|
+
captionUploadStatus: wistiaCaptions.captionUploadStatus,
|
|
3905
|
+
captionUploadMessage: wistiaCaptions.captionUploadMessage,
|
|
3906
|
+
_originalIndex: 0
|
|
3907
|
+
}, srtCuesMetadata = srtCues.map((cue) => ({
|
|
3908
|
+
index: cue.index,
|
|
3909
|
+
startTime: cue.startTime,
|
|
3910
|
+
endTime: cue.endTime
|
|
3911
|
+
}));
|
|
3912
|
+
return devLog.info("Wistia captions serialized:", {
|
|
3913
|
+
fieldPath: context.fieldPath,
|
|
3914
|
+
translationTextLength: translationText.length,
|
|
3915
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
|
|
3916
|
+
cueCount: srtCues.length
|
|
3917
|
+
}), {
|
|
3918
|
+
_type: "translatable.wistia.captions",
|
|
3919
|
+
translationText,
|
|
3920
|
+
_originalMetadata: originalMetadata,
|
|
3921
|
+
_srtCues: srtCuesMetadata
|
|
3922
|
+
};
|
|
3923
|
+
},
|
|
3924
|
+
fromTranslatable: (translatedValue, originalValue, context) => {
|
|
3925
|
+
if (devLog.info("Wistia captions fromTranslatable - input:", {
|
|
3926
|
+
fieldPath: context.fieldPath,
|
|
3927
|
+
valueType: typeof translatedValue,
|
|
3928
|
+
isCorrectType: translatedValue._type === "translatable.wistia.captions",
|
|
3929
|
+
hasText: !!translatedValue.translationText,
|
|
3930
|
+
hasMetadata: !!translatedValue._originalMetadata,
|
|
3931
|
+
hasCues: Array.isArray(translatedValue._srtCues)
|
|
3932
|
+
}), !translatedValue || translatedValue._type !== "translatable.wistia.captions" || !translatedValue._originalMetadata || !Array.isArray(translatedValue._srtCues))
|
|
3933
|
+
return devLog.debug("Value is not a translatable Wistia caption object, returning original", {
|
|
3934
|
+
fieldPath: context.fieldPath,
|
|
3935
|
+
valueType: typeof translatedValue,
|
|
3936
|
+
translatedValue
|
|
3937
|
+
}), originalValue || {};
|
|
3938
|
+
devLog.info("Processing translated Wistia captions", {
|
|
3939
|
+
fieldPath: context.fieldPath,
|
|
3940
|
+
cueCount: translatedValue._srtCues.length
|
|
3941
|
+
});
|
|
3942
|
+
const { translationText, _originalMetadata, _srtCues } = translatedValue;
|
|
3943
|
+
devLog.info("Wistia captions translationText:", {
|
|
3944
|
+
fieldPath: context.fieldPath,
|
|
3945
|
+
translationTextLength: translationText.length,
|
|
3946
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
|
|
3947
|
+
});
|
|
3948
|
+
const reconstructedCaptionContent = deserializeWistiaCaptionText(translationText, _srtCues), documentLanguage = getDocumentLanguage(context.document);
|
|
3949
|
+
let updatedIsoLanguage = _originalMetadata.isoLanguage;
|
|
3950
|
+
if (documentLanguage) {
|
|
3951
|
+
const isoCode = normalizeToIsoLanguageCode(documentLanguage);
|
|
3952
|
+
isoCode ? (updatedIsoLanguage = isoCode, devLog.debug("Updated isoLanguage based on document language", {
|
|
3953
|
+
fieldPath: context.fieldPath,
|
|
3954
|
+
documentLanguage,
|
|
3955
|
+
isoCode
|
|
3956
|
+
})) : devLog.debug("Could not convert document language to ISO code, preserving original", {
|
|
3957
|
+
fieldPath: context.fieldPath,
|
|
3958
|
+
documentLanguage,
|
|
3959
|
+
originalIsoLanguage: _originalMetadata.isoLanguage
|
|
3960
|
+
});
|
|
3961
|
+
} else
|
|
3962
|
+
devLog.debug("Document language field not found, preserving original isoLanguage", {
|
|
3963
|
+
fieldPath: context.fieldPath,
|
|
3964
|
+
originalIsoLanguage: _originalMetadata.isoLanguage
|
|
3965
|
+
});
|
|
3966
|
+
const result = {
|
|
3967
|
+
captionContent: reconstructedCaptionContent,
|
|
3968
|
+
captionFileName: _originalMetadata.captionFileName,
|
|
3969
|
+
isoLanguage: updatedIsoLanguage,
|
|
3970
|
+
captionUploadStatus: _originalMetadata.captionUploadStatus,
|
|
3971
|
+
captionUploadMessage: _originalMetadata.captionUploadMessage
|
|
3972
|
+
};
|
|
3973
|
+
return devLog.info("Wistia captions final result:", {
|
|
3974
|
+
fieldPath: context.fieldPath,
|
|
3975
|
+
hasCaptionContent: !!result.captionContent,
|
|
3976
|
+
captionContentLength: result.captionContent?.length ?? 0,
|
|
3977
|
+
isoLanguage: result.isoLanguage
|
|
3978
|
+
}), result;
|
|
3979
|
+
}
|
|
3980
|
+
};
|
|
3981
|
+
function parseSRTContent(content) {
|
|
3982
|
+
if (devLog.debug("parseSRTContent - input:", {
|
|
3983
|
+
contentLength: content.length,
|
|
3984
|
+
contentPreview: content.substring(0, 100) + (content.length > 100 ? "..." : "")
|
|
3985
|
+
}), !content || content.trim() === "")
|
|
3986
|
+
return [];
|
|
3987
|
+
const cues = [], cueBlocks = content.split(/\n\s*\n/);
|
|
3988
|
+
for (const block of cueBlocks) {
|
|
3989
|
+
const lines = block.trim().split(`
|
|
3990
|
+
`);
|
|
3991
|
+
if (lines.length < 3)
|
|
3992
|
+
continue;
|
|
3993
|
+
const cueNumber = Number.parseInt(lines[0].trim(), 10);
|
|
3994
|
+
if (Number.isNaN(cueNumber)) {
|
|
3995
|
+
devLog.warn("Invalid SRT cue number", { line: lines[0] });
|
|
3996
|
+
continue;
|
|
3997
|
+
}
|
|
3998
|
+
const timestampLine = lines[1].trim(), timestampMatch = timestampLine.match(/^(\d{2}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}[,.]\d{3})$/);
|
|
3999
|
+
if (!timestampMatch) {
|
|
4000
|
+
devLog.warn("Invalid SRT timestamp format", { line: timestampLine });
|
|
4001
|
+
continue;
|
|
4002
|
+
}
|
|
4003
|
+
const startTime = timestampMatch[1].replace(".", ","), endTime = timestampMatch[2].replace(".", ","), text = lines.slice(2).join(`
|
|
4004
|
+
`).trim();
|
|
4005
|
+
text && cues.push({
|
|
4006
|
+
index: cueNumber,
|
|
4007
|
+
startTime,
|
|
4008
|
+
endTime,
|
|
4009
|
+
text
|
|
4010
|
+
});
|
|
4011
|
+
}
|
|
4012
|
+
return devLog.debug("parseSRTContent - output:", {
|
|
4013
|
+
cueCount: cues.length,
|
|
4014
|
+
firstCue: cues.length > 0 ? cues[0] : null
|
|
4015
|
+
}), cues;
|
|
4016
|
+
}
|
|
4017
|
+
function serializeWistiaCaptionText(cues) {
|
|
4018
|
+
if (devLog.debug("serializeWistiaCaptionText - input:", {
|
|
4019
|
+
cuesCount: cues.length,
|
|
4020
|
+
firstCue: cues.length > 0 ? cues[0] : null
|
|
4021
|
+
}), !Array.isArray(cues) || cues.length === 0)
|
|
4022
|
+
return "";
|
|
4023
|
+
const result = cues.map((cue) => (devLog.debug(`Processing cue ${cue.index}:`, {
|
|
4024
|
+
text: cue.text,
|
|
4025
|
+
hasText: typeof cue.text == "string"
|
|
4026
|
+
}), `[${cue.index}] ${cue.text}`)).join(`
|
|
4027
|
+
`);
|
|
4028
|
+
return devLog.debug("serializeWistiaCaptionText - output:", {
|
|
4029
|
+
resultLength: result.length,
|
|
4030
|
+
resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
|
|
4031
|
+
}), result;
|
|
4032
|
+
}
|
|
4033
|
+
function deserializeWistiaCaptionText(translationText, srtCuesMetadata) {
|
|
4034
|
+
if (devLog.debug("deserializeWistiaCaptionText - input:", {
|
|
4035
|
+
translationTextLength: translationText.length,
|
|
4036
|
+
translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
|
|
4037
|
+
metadataCount: srtCuesMetadata.length,
|
|
4038
|
+
metadataExample: srtCuesMetadata.length > 0 ? srtCuesMetadata[0] : null
|
|
4039
|
+
}), !translationText || translationText.trim() === "")
|
|
4040
|
+
return "";
|
|
4041
|
+
const result = [], lines = translationText.split(`
|
|
4042
|
+
`), lineRegex = /^\[(\d+)\]\s*(.*)$/, metadataByIndex = /* @__PURE__ */ new Map();
|
|
4043
|
+
for (const metadata of srtCuesMetadata)
|
|
4044
|
+
metadataByIndex.set(metadata.index, { startTime: metadata.startTime, endTime: metadata.endTime });
|
|
4045
|
+
for (const line of lines) {
|
|
4046
|
+
const match = line.match(lineRegex);
|
|
4047
|
+
if (!match) {
|
|
4048
|
+
devLog.warn("Failed to parse caption line", { line });
|
|
4049
|
+
continue;
|
|
4050
|
+
}
|
|
4051
|
+
const index = Number.parseInt(match[1], 10), text = match[2];
|
|
4052
|
+
devLog.debug("Found caption:", {
|
|
4053
|
+
index,
|
|
4054
|
+
text
|
|
4055
|
+
});
|
|
4056
|
+
const metadata = metadataByIndex.get(index);
|
|
4057
|
+
metadata ? (devLog.debug(`Found matching metadata for index ${index}`), result.push(`${index}`), result.push(`${metadata.startTime} --> ${metadata.endTime}`), result.push(text), result.push("")) : devLog.warn("Missing metadata for caption", { index });
|
|
4058
|
+
}
|
|
4059
|
+
result.length > 0 && result[result.length - 1] === "" && result.pop();
|
|
4060
|
+
const reconstructed = result.join(`
|
|
4061
|
+
`);
|
|
4062
|
+
return devLog.debug("deserializeWistiaCaptionText - output:", {
|
|
4063
|
+
resultLength: reconstructed.length,
|
|
4064
|
+
resultPreview: reconstructed.substring(0, 200) + (reconstructed.length > 200 ? "..." : "")
|
|
4065
|
+
}), reconstructed;
|
|
4066
|
+
}
|
|
4067
|
+
function getDocumentLanguage(document) {
|
|
4068
|
+
if (!document || typeof document != "object")
|
|
4069
|
+
return;
|
|
4070
|
+
const languageFieldNames = ["language", "lang", "locale"];
|
|
4071
|
+
for (const fieldName of languageFieldNames)
|
|
4072
|
+
if (fieldName in document && typeof document[fieldName] == "string")
|
|
4073
|
+
return document[fieldName];
|
|
4074
|
+
}
|
|
4214
4075
|
const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
|
|
4215
4076
|
globalTransformRegistry.register(transform);
|
|
4216
4077
|
})), {
|
|
4217
4078
|
name: "sanity-plugin-ai-translation",
|
|
4218
4079
|
document: {
|
|
4219
|
-
actions: [getTranslateAction(config)
|
|
4080
|
+
actions: [getTranslateAction(config)]
|
|
4220
4081
|
},
|
|
4221
4082
|
schema: {
|
|
4222
4083
|
types: [translationSettings, translationGlossaryTerm]
|
|
@@ -4230,6 +4091,7 @@ export {
|
|
|
4230
4091
|
restoreExcludedFields,
|
|
4231
4092
|
richTextTransform,
|
|
4232
4093
|
translationHistorySchema,
|
|
4233
|
-
vttCaptionTransform
|
|
4094
|
+
vttCaptionTransform,
|
|
4095
|
+
wistiaCaptionTransform
|
|
4234
4096
|
};
|
|
4235
4097
|
//# sourceMappingURL=index.js.map
|