@braze/sanity-plugin-ai-translation 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -54
- package/dist/index.d.ts +1 -4
- package/dist/index.js +751 -1479
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
import { createContext, useReducer, useEffect, useContext, useState, useCallback
|
|
1
|
+
import { getPublishedId, pathToString, useClient, defineType, defineField, definePlugin } from "sanity";
|
|
2
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { WarningOutlineIcon, SearchIcon, CheckmarkIcon, ErrorOutlineIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
|
|
4
|
+
import { createContext, useReducer, useEffect, useContext, useMemo, useState, useCallback } from "react";
|
|
5
5
|
import defaults from "lodash.defaults";
|
|
6
|
-
import { Card,
|
|
7
|
-
import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
|
|
6
|
+
import { Card, Stack, Flex, Text, Box, Heading, Button, Spinner, Inline, Checkbox, Grid, Radio, TextInput, Badge, Select, Label, Code, Tooltip } from "@sanity/ui";
|
|
8
7
|
import { toPlainText } from "@portabletext/react";
|
|
9
8
|
import { uuid } from "@sanity/uuid";
|
|
10
9
|
import cloneDeep from "lodash.clonedeep";
|
|
11
10
|
import get from "lodash.get";
|
|
12
11
|
import set from "lodash.set";
|
|
12
|
+
import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
|
|
13
13
|
const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true" || process.env.SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true", devLog = {
|
|
14
14
|
info: (message, context) => {
|
|
15
15
|
isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
|
|
@@ -62,19 +62,63 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
62
62
|
error: error instanceof Error ? error.message : "Network error"
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
}, activeTranslations = /* @__PURE__ */ new Map(), registerActiveTranslation = (documentId, releaseId) => {
|
|
66
|
+
const entry = {
|
|
67
|
+
state: {
|
|
68
|
+
documentId,
|
|
69
|
+
releaseId,
|
|
70
|
+
overallStatus: "translating",
|
|
71
|
+
translationJobs: /* @__PURE__ */ new Map(),
|
|
72
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
73
|
+
},
|
|
74
|
+
subscribers: /* @__PURE__ */ new Set()
|
|
75
|
+
};
|
|
76
|
+
activeTranslations.set(documentId, entry);
|
|
77
|
+
}, updateActiveTranslationStatus = (documentId, status) => {
|
|
78
|
+
const entry = activeTranslations.get(documentId);
|
|
79
|
+
entry && (entry.state.overallStatus = status, notifySubscribers(entry));
|
|
80
|
+
}, updateActiveTranslationJob = (documentId, job) => {
|
|
81
|
+
const entry = activeTranslations.get(documentId);
|
|
82
|
+
if (entry) {
|
|
83
|
+
const jobKey = `${job._id}_${job.language}`, existingJob = entry.state.translationJobs.get(jobKey), mergedJob = existingJob ? { ...existingJob, ...job } : job;
|
|
84
|
+
entry.state.translationJobs.set(jobKey, mergedJob), notifySubscribers(entry);
|
|
85
|
+
}
|
|
86
|
+
}, getActiveTranslation = (documentId) => activeTranslations.get(documentId)?.state ?? null, subscribeToActiveTranslation = (documentId, callback) => {
|
|
87
|
+
const entry = activeTranslations.get(documentId);
|
|
88
|
+
return entry ? (entry.subscribers.add(callback), callback(entry.state), () => {
|
|
89
|
+
entry.subscribers.delete(callback);
|
|
90
|
+
}) : () => {
|
|
91
|
+
};
|
|
92
|
+
}, cleanupActiveTranslation = (documentId, delayMs = 6e4) => {
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
const entry = activeTranslations.get(documentId);
|
|
95
|
+
entry && ["completed", "error"].includes(entry.state.overallStatus) && activeTranslations.delete(documentId);
|
|
96
|
+
}, delayMs);
|
|
97
|
+
}, notifySubscribers = (entry) => {
|
|
98
|
+
for (const subscriber of entry.subscribers)
|
|
99
|
+
try {
|
|
100
|
+
subscriber(entry.state);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Error in active translation subscriber:", error);
|
|
103
|
+
}
|
|
104
|
+
}, forceCleanupActiveTranslation = (documentId) => {
|
|
105
|
+
activeTranslations.delete(documentId);
|
|
65
106
|
}, TranslationContext = createContext({
|
|
66
107
|
document: null,
|
|
67
108
|
config: null,
|
|
68
109
|
overallStatus: "idle",
|
|
69
110
|
selectedTargetLanguages: [],
|
|
70
111
|
translationJobs: /* @__PURE__ */ new Map(),
|
|
71
|
-
shouldOverwriteFreshTranslations: !1,
|
|
72
112
|
shouldTranslateReferences: !1,
|
|
73
113
|
maxReferenceDepth: 1,
|
|
74
114
|
references: [],
|
|
75
115
|
translationMetadata: null,
|
|
76
116
|
translationMode: "sanity-only",
|
|
77
117
|
openaiValidated: !1,
|
|
118
|
+
releaseConfig: {
|
|
119
|
+
mode: "new",
|
|
120
|
+
newReleaseTitle: ""
|
|
121
|
+
},
|
|
78
122
|
dispatch: () => {
|
|
79
123
|
}
|
|
80
124
|
}), translationReducer = (context, action) => {
|
|
@@ -85,7 +129,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
85
129
|
};
|
|
86
130
|
switch (devLog.debug("Translation action dispatched", {
|
|
87
131
|
...logContext,
|
|
88
|
-
payload: action.payload
|
|
132
|
+
payload: "payload" in action ? action.payload : void 0
|
|
89
133
|
}), action.type) {
|
|
90
134
|
case "SET_OVERALL_STATUS":
|
|
91
135
|
return { ...context, overallStatus: action.payload };
|
|
@@ -116,11 +160,6 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
116
160
|
...context,
|
|
117
161
|
maxReferenceDepth: action.payload
|
|
118
162
|
};
|
|
119
|
-
case "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS":
|
|
120
|
-
return {
|
|
121
|
-
...context,
|
|
122
|
-
shouldOverwriteFreshTranslations: action.payload
|
|
123
|
-
};
|
|
124
163
|
case "SET_REFERENCES":
|
|
125
164
|
return {
|
|
126
165
|
...context,
|
|
@@ -133,31 +172,82 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
133
172
|
translationJobs: newTranslationJobs
|
|
134
173
|
};
|
|
135
174
|
}
|
|
175
|
+
case "SET_RELEASE_CONFIG":
|
|
176
|
+
return {
|
|
177
|
+
...context,
|
|
178
|
+
releaseConfig: {
|
|
179
|
+
...context.releaseConfig,
|
|
180
|
+
...action.payload
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
case "RESET_TRANSLATION_STATE":
|
|
184
|
+
return devLog.info("Resetting translation state for new translation", {
|
|
185
|
+
...logContext,
|
|
186
|
+
previousStatus: context.overallStatus,
|
|
187
|
+
previousJobCount: context.translationJobs.size
|
|
188
|
+
}), {
|
|
189
|
+
...context,
|
|
190
|
+
overallStatus: "idle",
|
|
191
|
+
translationJobs: /* @__PURE__ */ new Map(),
|
|
192
|
+
// Clear any release conflicts from previous run
|
|
193
|
+
releaseConfig: {
|
|
194
|
+
...context.releaseConfig,
|
|
195
|
+
conflicts: void 0,
|
|
196
|
+
replaceExisting: void 0
|
|
197
|
+
}
|
|
198
|
+
};
|
|
136
199
|
default:
|
|
137
200
|
return context;
|
|
138
201
|
}
|
|
139
|
-
}, TranslationContextProvider = ({ document, config, children }) => {
|
|
202
|
+
}, generateDefaultReleaseTitle = (document) => `[AI Translation] ${document.title || document.name || document._id.split("-").slice(0, 2).join("-")}`, TranslationContextProvider = ({ document, config, children }) => {
|
|
140
203
|
devLog.debug("TranslationContextProvider initialized", {
|
|
141
204
|
documentId: document._id,
|
|
142
205
|
documentType: document._type,
|
|
143
206
|
supportedLanguages: config.supportedLanguages?.map((l) => l.id),
|
|
144
207
|
component: "TranslationContextProvider"
|
|
145
208
|
});
|
|
146
|
-
const initialContext = {
|
|
209
|
+
const defaultReleaseTitle = generateDefaultReleaseTitle(document), activeTranslation = getActiveTranslation(document._id), initialContext = {
|
|
147
210
|
document,
|
|
148
211
|
config,
|
|
149
|
-
|
|
212
|
+
// If there's an active translation, restore its state
|
|
213
|
+
overallStatus: activeTranslation?.overallStatus ?? "idle",
|
|
150
214
|
translationMetadata: null,
|
|
151
215
|
selectedTargetLanguages: [],
|
|
152
|
-
shouldOverwriteFreshTranslations: !1,
|
|
153
216
|
shouldTranslateReferences: !1,
|
|
154
217
|
maxReferenceDepth: 1,
|
|
155
218
|
references: [],
|
|
156
|
-
|
|
219
|
+
// Restore jobs from active translation if available
|
|
220
|
+
translationJobs: activeTranslation?.translationJobs ?? /* @__PURE__ */ new Map(),
|
|
157
221
|
translationMode: config?.openai?.defaultMode || "sanity-only",
|
|
158
|
-
openaiValidated: !1
|
|
159
|
-
|
|
222
|
+
openaiValidated: !1,
|
|
223
|
+
releaseConfig: {
|
|
224
|
+
mode: "new",
|
|
225
|
+
newReleaseTitle: defaultReleaseTitle
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
activeTranslation && devLog.info("Reconnecting to active translation", {
|
|
229
|
+
documentId: document._id,
|
|
230
|
+
status: activeTranslation.overallStatus,
|
|
231
|
+
jobCount: activeTranslation.translationJobs.size,
|
|
232
|
+
component: "TranslationContextProvider"
|
|
233
|
+
});
|
|
234
|
+
const [context, dispatch] = useReducer(translationReducer, initialContext);
|
|
160
235
|
return useEffect(() => {
|
|
236
|
+
const handleActiveTranslationUpdate = (state) => {
|
|
237
|
+
dispatch({
|
|
238
|
+
type: "SET_OVERALL_STATUS",
|
|
239
|
+
payload: state.overallStatus
|
|
240
|
+
});
|
|
241
|
+
for (const job of state.translationJobs.values())
|
|
242
|
+
dispatch({
|
|
243
|
+
type: "SET_TRANSLATION_JOB_STATUS",
|
|
244
|
+
payload: job
|
|
245
|
+
});
|
|
246
|
+
}, unsubscribe = subscribeToActiveTranslation(document._id, handleActiveTranslationUpdate);
|
|
247
|
+
return () => {
|
|
248
|
+
unsubscribe();
|
|
249
|
+
};
|
|
250
|
+
}, [document._id]), useEffect(() => {
|
|
161
251
|
(async () => {
|
|
162
252
|
if (config?.openai?.enabled && config?.openai?.apiKey)
|
|
163
253
|
try {
|
|
@@ -196,1043 +286,128 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
196
286
|
"useTranslationContext must be used within a TranslationContextProvider with document and config props"
|
|
197
287
|
);
|
|
198
288
|
return context;
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
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({
|
|
289
|
+
}, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
290
|
+
(translation) => translation._key === targetLanguage
|
|
291
|
+
)?.value._ref || null, undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_TYPE = "translation.metadata", getTranslationMetadata = async ({
|
|
367
292
|
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) {
|
|
293
|
+
sourceDocument,
|
|
294
|
+
sourceLanguage
|
|
295
|
+
}) => {
|
|
562
296
|
try {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
297
|
+
return await client.fetch(
|
|
298
|
+
"*[_type == $tmType && references($sourceDocumentId)][0]",
|
|
299
|
+
{
|
|
300
|
+
tmType: TRANSLATION_METADATA_TYPE,
|
|
301
|
+
sourceDocumentId: undraftId(sourceDocument._id)
|
|
302
|
+
}
|
|
303
|
+
) || await client.create({
|
|
304
|
+
_type: TRANSLATION_METADATA_TYPE,
|
|
305
|
+
translations: [
|
|
306
|
+
{
|
|
307
|
+
_key: sourceLanguage,
|
|
308
|
+
_type: "internationalizedArrayReferenceValue",
|
|
309
|
+
value: {
|
|
310
|
+
_type: "reference",
|
|
311
|
+
_ref: undraftId(sourceDocument._id),
|
|
312
|
+
_weak: !0,
|
|
313
|
+
_strengthenOnPublish: {
|
|
314
|
+
_type: sourceDocument._type,
|
|
315
|
+
_weak: !1
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
translationHistory: []
|
|
566
321
|
});
|
|
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
|
-
} catch (error) {
|
|
573
|
-
throw devLog.error("Error during AI analysis", { error }), new Error(`Failed to analyze feedback: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
function parseAIResponse(responseText) {
|
|
577
|
-
try {
|
|
578
|
-
let parsed;
|
|
579
|
-
try {
|
|
580
|
-
parsed = JSON.parse(responseText);
|
|
581
|
-
} catch {
|
|
582
|
-
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
|
583
|
-
if (!jsonMatch) throw new Error("No JSON found in AI response");
|
|
584
|
-
parsed = JSON.parse(jsonMatch[0]);
|
|
585
|
-
}
|
|
586
|
-
if (!parsed || typeof parsed != "object")
|
|
587
|
-
throw new Error("Response is not a valid object");
|
|
588
|
-
const analysis = parsed;
|
|
589
|
-
if (typeof analysis.reasoning != "string")
|
|
590
|
-
throw new Error("Invalid response structure: missing reasoning");
|
|
591
|
-
return {
|
|
592
|
-
globalPromptUpdates: analysis.globalPromptUpdates || void 0,
|
|
593
|
-
localePromptUpdates: analysis.localePromptUpdates || void 0,
|
|
594
|
-
protectedPhrasesToAdd: Array.isArray(analysis.protectedPhrasesToAdd) ? analysis.protectedPhrasesToAdd : [],
|
|
595
|
-
glossaryTermsToAdd: Array.isArray(analysis.glossaryTermsToAdd) ? analysis.glossaryTermsToAdd : [],
|
|
596
|
-
reasoning: analysis.reasoning
|
|
597
|
-
};
|
|
598
322
|
} catch (error) {
|
|
599
|
-
throw
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
async function applyImprovementsFromSuggestions(client, schemaId, targetLanguage, suggestions, onProgress) {
|
|
603
|
-
const settingsId = "translation-settings";
|
|
604
|
-
await client.fetch("*[_id == $id][0]", { id: settingsId }) || await client.createIfNotExists({
|
|
605
|
-
_id: settingsId,
|
|
606
|
-
_type: "translationSettings"
|
|
607
|
-
});
|
|
608
|
-
const updates = [], globalPrompts = suggestions.filter((s) => s.type === "globalPrompt" && s.description.trim()), localePrompts = suggestions.filter((s) => s.type === "localePrompt" && s.description.trim()), protectedPhrases = suggestions.filter((s) => s.type === "protectedPhrase" && s.description.trim()), glossaryTerms = suggestions.filter(
|
|
609
|
-
(s) => s.type === "glossaryTerm" && s.sourceLocale?.trim() && s.sourceValue?.trim() && s.targetLocale?.trim() && s.targetValue?.trim()
|
|
610
|
-
);
|
|
611
|
-
if ((globalPrompts.length > 0 || localePrompts.length > 0) && (onProgress?.({
|
|
612
|
-
stage: "applying",
|
|
613
|
-
message: globalPrompts.length > 0 && localePrompts.length > 0 ? "Updating global and locale translation prompts..." : globalPrompts.length > 0 ? "Updating global translation prompt..." : `Updating ${targetLanguage} translation prompt...`
|
|
614
|
-
}), updates.push(updatePromptsWithTemplate(client, schemaId, settingsId, targetLanguage, globalPrompts, localePrompts))), protectedPhrases.length > 0) {
|
|
615
|
-
onProgress?.({ stage: "applying", message: "Adding protected phrases..." });
|
|
616
|
-
const phrases = protectedPhrases.map((s) => s.description);
|
|
617
|
-
updates.push(addProtectedPhrases(client, settingsId, phrases));
|
|
618
|
-
}
|
|
619
|
-
if (glossaryTerms.length > 0) {
|
|
620
|
-
onProgress?.({ stage: "applying", message: "Adding glossary terms..." });
|
|
621
|
-
const terms = glossaryTerms.map((s) => ({
|
|
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));
|
|
323
|
+
throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
|
|
630
324
|
}
|
|
631
|
-
|
|
325
|
+
}, checkDocumentConflicts = async ({
|
|
326
|
+
client,
|
|
327
|
+
releaseId,
|
|
328
|
+
documentId,
|
|
329
|
+
documentTitle,
|
|
330
|
+
documentType,
|
|
331
|
+
targetLanguages,
|
|
332
|
+
languageFieldName
|
|
333
|
+
}) => {
|
|
334
|
+
const conflicts = [], document = await client.withConfig({ perspective: "drafts" }).getDocument(documentId);
|
|
335
|
+
if (!document)
|
|
336
|
+
return conflicts;
|
|
337
|
+
const sourceLanguage = document[languageFieldName], translationMetadata = await getTranslationMetadata({
|
|
632
338
|
client,
|
|
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();
|
|
656
|
-
}
|
|
657
|
-
if (localePrompts.length > 0) {
|
|
658
|
-
const updates = localePrompts.map((s) => s.description.trim()).filter(Boolean), existingLocalePrompts = currentSettings?.localePrompts || [], existingLocalePrompt = existingLocalePrompts.find((lp) => lp._key === targetLanguage);
|
|
659
|
-
if (existingLocalePrompt) {
|
|
660
|
-
const currentValue = existingLocalePrompt.value || "", newValue = [
|
|
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
|
-
}
|
|
680
|
-
}
|
|
681
|
-
devLog.info("Prompts updated successfully", {
|
|
682
|
-
targetLanguage,
|
|
683
|
-
globalUpdated: globalPrompts.length > 0,
|
|
684
|
-
localeUpdated: localePrompts.length > 0
|
|
339
|
+
sourceDocument: document,
|
|
340
|
+
sourceLanguage
|
|
685
341
|
});
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
sourceValue,
|
|
704
|
-
targetValue
|
|
705
|
-
}), !1) : protectedPhrasesLower.includes(targetValue.toLowerCase()) ? (devLog.warn("Skipping glossary term because target value is a protected phrase", {
|
|
706
|
-
sourceValue,
|
|
707
|
-
targetValue
|
|
708
|
-
}), !1) : !0);
|
|
709
|
-
if (validTerms.length === 0) {
|
|
710
|
-
devLog.info("No valid glossary terms to add");
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
for (const { sourceLocale, sourceValue, targetLocale, targetValue } of validTerms) {
|
|
714
|
-
const existingTerm = await client.fetch(
|
|
715
|
-
`*[_type == "translationGlossaryTerm" && count(translations[_key == $sourceLocale && value == $sourceValue]) > 0
|
|
716
|
-
][0] {
|
|
717
|
-
_id,
|
|
718
|
-
translations
|
|
719
|
-
}`,
|
|
720
|
-
{ sourceLocale, sourceValue }
|
|
721
|
-
);
|
|
722
|
-
if (existingTerm) {
|
|
723
|
-
const existingTargetIndex = existingTerm.translations.findIndex((t) => t._key === targetLocale), existingTargetValue = existingTargetIndex >= 0 ? existingTerm.translations[existingTargetIndex].value : null;
|
|
724
|
-
existingTargetIndex === -1 ? await client.patch(existingTerm._id).append("translations", [
|
|
725
|
-
{
|
|
726
|
-
_type: "internationalizedArrayStringValue",
|
|
727
|
-
_key: targetLocale,
|
|
728
|
-
value: targetValue
|
|
729
|
-
}
|
|
730
|
-
]).commit() : existingTargetValue !== targetValue && await client.patch(existingTerm._id).set({ [`translations[${existingTargetIndex}].value`]: targetValue }).commit();
|
|
731
|
-
} else
|
|
732
|
-
await client.create({
|
|
733
|
-
_type: "translationGlossaryTerm",
|
|
734
|
-
translations: [
|
|
735
|
-
{
|
|
736
|
-
_type: "internationalizedArrayStringValue",
|
|
737
|
-
_key: sourceLocale,
|
|
738
|
-
value: sourceValue
|
|
739
|
-
},
|
|
740
|
-
{
|
|
741
|
-
_type: "internationalizedArrayStringValue",
|
|
742
|
-
_key: targetLocale,
|
|
743
|
-
value: targetValue
|
|
744
|
-
}
|
|
745
|
-
]
|
|
746
|
-
});
|
|
342
|
+
for (const targetLanguage of targetLanguages) {
|
|
343
|
+
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage);
|
|
344
|
+
if (existingTranslationId) {
|
|
345
|
+
const versionedId = `versions.${releaseId}.${existingTranslationId}`;
|
|
346
|
+
await client.getDocument(versionedId) && (conflicts.push({
|
|
347
|
+
sourceDocumentId: documentId,
|
|
348
|
+
sourceDocumentTitle: documentTitle || document.title || document.name,
|
|
349
|
+
sourceDocumentType: documentType || document._type,
|
|
350
|
+
targetLanguage,
|
|
351
|
+
existingVersionId: versionedId
|
|
352
|
+
}), devLog.debug("Found existing version in release", {
|
|
353
|
+
releaseId,
|
|
354
|
+
sourceDocumentId: documentId,
|
|
355
|
+
targetLanguage,
|
|
356
|
+
versionedId
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
747
359
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
|
360
|
+
return conflicts;
|
|
361
|
+
}, checkReleaseConflicts = async ({
|
|
362
|
+
client,
|
|
363
|
+
releaseId,
|
|
364
|
+
sourceDocument,
|
|
365
|
+
targetLanguages,
|
|
366
|
+
languageFieldName,
|
|
367
|
+
references = []
|
|
805
368
|
}) => {
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
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,
|
|
369
|
+
const allConflicts = [];
|
|
370
|
+
try {
|
|
371
|
+
const documentsToCheck = [
|
|
997
372
|
{
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
-
] });
|
|
1144
|
-
}
|
|
1145
|
-
return processingState === "error" ? /* @__PURE__ */ jsx(Box, { padding: 4, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
1146
|
-
/* @__PURE__ */ jsx(ProgressPanel, { status: "error", title: "Error processing feedback", description: error, tone: "critical" }),
|
|
1147
|
-
/* @__PURE__ */ jsxs(Flex, { justify: "flex-end", gap: 2, children: [
|
|
1148
|
-
/* @__PURE__ */ jsx(Button, { type: "button", mode: "ghost", onClick: handleCancel, children: "Close" }),
|
|
1149
|
-
/* @__PURE__ */ jsx(Button, { type: "reset", mode: "ghost", tone: "primary", onClick: () => setProcessingState("idle"), children: "Reset" })
|
|
1150
|
-
] })
|
|
1151
|
-
] }) }) : /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
1152
|
-
/* @__PURE__ */ jsx(Card, { tone: "transparent", padding: 4, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
1153
|
-
/* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
1154
|
-
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
1155
|
-
/* @__PURE__ */ jsx(Text, { size: 2, muted: !0, children: "Enter your feedback for the translation AI agent." }),
|
|
1156
|
-
/* @__PURE__ */ jsx(
|
|
1157
|
-
Tooltip,
|
|
1158
|
-
{
|
|
1159
|
-
portal: !0,
|
|
1160
|
-
content: /* @__PURE__ */ jsx(Box, { padding: 2, style: { maxWidth: "280px" }, children: /* @__PURE__ */ jsx(Text, { size: 1, children: "The AI agent will use your feedback to improve translation prompts, update protected phrases, and modify the glossary. Be specific about what should be changed." }) }),
|
|
1161
|
-
children: /* @__PURE__ */ jsx(Text, { size: 1, children: /* @__PURE__ */ jsx(InfoOutlineIcon, {}) })
|
|
1162
|
-
}
|
|
1163
|
-
)
|
|
1164
|
-
] }),
|
|
1165
|
-
/* @__PURE__ */ jsx(
|
|
1166
|
-
TextArea,
|
|
1167
|
-
{
|
|
1168
|
-
rows: 8,
|
|
1169
|
-
fontSize: 2,
|
|
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
|
|
1174
|
-
}
|
|
1175
|
-
)
|
|
1176
|
-
] }) }),
|
|
1177
|
-
/* @__PURE__ */ jsx(Stack, { space: 2, children: /* @__PURE__ */ jsx(Stack, { space: 3, children: /* @__PURE__ */ jsx(
|
|
1178
|
-
Button,
|
|
1179
|
-
{
|
|
1180
|
-
type: "button",
|
|
1181
|
-
mode: shouldIncludeEdits ? "ghost" : "bleed",
|
|
1182
|
-
size: 2,
|
|
1183
|
-
onClick: handleShouldIncludeEditsChange,
|
|
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
|
-
}
|
|
373
|
+
_id: sourceDocument._id,
|
|
374
|
+
_type: sourceDocument._type,
|
|
375
|
+
title: sourceDocument.title || sourceDocument.name
|
|
376
|
+
},
|
|
377
|
+
...references.map((ref) => ({
|
|
378
|
+
_id: ref._id,
|
|
379
|
+
_type: ref._type,
|
|
380
|
+
title: ref.title
|
|
381
|
+
}))
|
|
382
|
+
], conflictChecks = await Promise.all(
|
|
383
|
+
documentsToCheck.map(
|
|
384
|
+
(doc) => checkDocumentConflicts({
|
|
385
|
+
client,
|
|
386
|
+
releaseId,
|
|
387
|
+
documentId: doc._id,
|
|
388
|
+
documentTitle: doc.title,
|
|
389
|
+
documentType: doc._type,
|
|
390
|
+
targetLanguages,
|
|
391
|
+
languageFieldName
|
|
392
|
+
})
|
|
1208
393
|
)
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
component: "GiveTranslationFeedbackAction"
|
|
1227
|
-
}), setIsDialogOpen(!0);
|
|
1228
|
-
},
|
|
1229
|
-
dialog: isDialogOpen && {
|
|
1230
|
-
type: "dialog",
|
|
1231
|
-
onClose: () => setIsDialogOpen(!1),
|
|
1232
|
-
header: "Give Translation Feedback",
|
|
1233
|
-
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(GiveFeedbackDialog, { setIsDialogOpen }) })
|
|
1234
|
-
}
|
|
1235
|
-
} : (devLog.error("Missing config in TranslateAction"), null);
|
|
394
|
+
);
|
|
395
|
+
for (const conflicts of conflictChecks)
|
|
396
|
+
allConflicts.push(...conflicts);
|
|
397
|
+
devLog.debug("Release conflict check complete", {
|
|
398
|
+
releaseId,
|
|
399
|
+
documentsChecked: documentsToCheck.length,
|
|
400
|
+
conflictsFound: allConflicts.length
|
|
401
|
+
});
|
|
402
|
+
} catch (error) {
|
|
403
|
+
devLog.error("Error checking release conflicts", error instanceof Error ? error : new Error(String(error)), {
|
|
404
|
+
releaseId,
|
|
405
|
+
sourceDocumentId: sourceDocument._id,
|
|
406
|
+
targetLanguages,
|
|
407
|
+
referencesCount: references.length
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return allConflicts;
|
|
1236
411
|
};
|
|
1237
412
|
class Semaphore {
|
|
1238
413
|
permits;
|
|
@@ -1570,179 +745,41 @@ const fetchTranslationPrompts = async ({
|
|
|
1570
745
|
refIds,
|
|
1571
746
|
sourceLanguage,
|
|
1572
747
|
targetLanguage
|
|
1573
|
-
}
|
|
1574
|
-
), translationMap = /* @__PURE__ */ new Map();
|
|
1575
|
-
for (const t of translations)
|
|
1576
|
-
t.sourceId && t.targetId && translationMap.set(t.sourceId, t.targetId);
|
|
1577
|
-
const patch = client.patch(documentId);
|
|
1578
|
-
let hasChanges = !1;
|
|
1579
|
-
for (const ref of refs) {
|
|
1580
|
-
const sourceId = getPublishedId(ref.value), newId = translationMap.get(sourceId);
|
|
1581
|
-
newId && (patch.set({ [pathToString(ref.path)]: getPublishedId(newId) }), patch.set({
|
|
1582
|
-
[`${pathToString(ref.path.slice(0, -1))}._strengthenOnPublish`]: {
|
|
1583
|
-
type: translations.find((t) => t.targetId === getPublishedId(newId))?.targetType || void 0
|
|
1584
|
-
}
|
|
1585
|
-
}), hasChanges = !0);
|
|
1586
|
-
}
|
|
1587
|
-
hasChanges && (await patch.commit(), devLog.info("Updated references in batch", {
|
|
1588
|
-
documentId,
|
|
1589
|
-
count: refs.length,
|
|
1590
|
-
translatedCount: translationMap.size,
|
|
1591
|
-
component: "updateReferences"
|
|
1592
|
-
}));
|
|
1593
|
-
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages) => {
|
|
1594
|
-
for (const sourceDocument of sourceDocuments)
|
|
1595
|
-
for (const lang of targetLanguages) {
|
|
1596
|
-
const payload = {
|
|
1597
|
-
_id: sourceDocument._id,
|
|
1598
|
-
_type: sourceDocument._type,
|
|
1599
|
-
language: lang,
|
|
1600
|
-
status: "queued",
|
|
1601
|
-
title: sourceDocument.title
|
|
1602
|
-
};
|
|
1603
|
-
dispatch({
|
|
1604
|
-
type: "SET_TRANSLATION_JOB_STATUS",
|
|
1605
|
-
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);
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
return { success: !1, error: lastError, attempts };
|
|
1709
|
-
}
|
|
1710
|
-
const getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
1711
|
-
(translation) => translation._key === targetLanguage
|
|
1712
|
-
)?.value._ref || null, getTranslationMetadata = async ({
|
|
1713
|
-
client,
|
|
1714
|
-
sourceDocument,
|
|
1715
|
-
sourceLanguage
|
|
1716
|
-
}) => {
|
|
1717
|
-
try {
|
|
1718
|
-
return await client.fetch(
|
|
1719
|
-
"*[_type == $tmType && references($sourceDocumentId)][0]",
|
|
1720
|
-
{
|
|
1721
|
-
tmType: TRANSLATION_METADATA_TYPE,
|
|
1722
|
-
sourceDocumentId: undraftId(sourceDocument._id)
|
|
1723
|
-
}
|
|
1724
|
-
) || await client.create({
|
|
1725
|
-
_type: TRANSLATION_METADATA_TYPE,
|
|
1726
|
-
translations: [
|
|
1727
|
-
{
|
|
1728
|
-
_key: sourceLanguage,
|
|
1729
|
-
_type: "internationalizedArrayReferenceValue",
|
|
1730
|
-
value: {
|
|
1731
|
-
_type: "reference",
|
|
1732
|
-
_ref: undraftId(sourceDocument._id),
|
|
1733
|
-
_weak: !0,
|
|
1734
|
-
_strengthenOnPublish: {
|
|
1735
|
-
_type: sourceDocument._type,
|
|
1736
|
-
_weak: !1
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
],
|
|
1741
|
-
translationHistory: []
|
|
1742
|
-
});
|
|
1743
|
-
} catch (error) {
|
|
1744
|
-
throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
|
|
748
|
+
}
|
|
749
|
+
), translationMap = /* @__PURE__ */ new Map();
|
|
750
|
+
for (const t of translations)
|
|
751
|
+
t.sourceId && t.targetId && translationMap.set(t.sourceId, t.targetId);
|
|
752
|
+
const patch = client.patch(documentId);
|
|
753
|
+
let hasChanges = !1;
|
|
754
|
+
for (const ref of refs) {
|
|
755
|
+
const sourceId = getPublishedId(ref.value), newId = translationMap.get(sourceId);
|
|
756
|
+
newId && (patch.set({ [pathToString(ref.path)]: getPublishedId(newId) }), patch.set({
|
|
757
|
+
[`${pathToString(ref.path.slice(0, -1))}._strengthenOnPublish`]: {
|
|
758
|
+
type: translations.find((t) => t.targetId === getPublishedId(newId))?.targetType || void 0
|
|
759
|
+
}
|
|
760
|
+
}), hasChanges = !0);
|
|
1745
761
|
}
|
|
762
|
+
hasChanges && (await patch.commit(), devLog.info("Updated references in batch", {
|
|
763
|
+
documentId,
|
|
764
|
+
count: refs.length,
|
|
765
|
+
translatedCount: translationMap.size,
|
|
766
|
+
component: "updateReferences"
|
|
767
|
+
}));
|
|
768
|
+
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages, mainSourceDocumentId) => {
|
|
769
|
+
for (const sourceDocument of sourceDocuments)
|
|
770
|
+
for (const lang of targetLanguages) {
|
|
771
|
+
const payload = {
|
|
772
|
+
_id: sourceDocument._id,
|
|
773
|
+
_type: sourceDocument._type,
|
|
774
|
+
language: lang,
|
|
775
|
+
status: "queued",
|
|
776
|
+
title: sourceDocument.title
|
|
777
|
+
};
|
|
778
|
+
dispatch({
|
|
779
|
+
type: "SET_TRANSLATION_JOB_STATUS",
|
|
780
|
+
payload
|
|
781
|
+
}), mainSourceDocumentId && updateActiveTranslationJob(mainSourceDocumentId, payload);
|
|
782
|
+
}
|
|
1746
783
|
}, updateTranslationMetadata = async ({
|
|
1747
784
|
client,
|
|
1748
785
|
targetLanguage,
|
|
@@ -1789,15 +826,16 @@ const getExistingTranslationId = (translationMetadata, targetLanguage) => transl
|
|
|
1789
826
|
component: "updateTranslationMetadata"
|
|
1790
827
|
}), error;
|
|
1791
828
|
}
|
|
1792
|
-
}, setJobStatus = (dispatch, _id, language, status, payload) => {
|
|
829
|
+
}, setJobStatus = (dispatch, _id, language, status, payload, sourceDocumentId) => {
|
|
830
|
+
const job = defaults(payload, {
|
|
831
|
+
_id,
|
|
832
|
+
language,
|
|
833
|
+
status
|
|
834
|
+
});
|
|
1793
835
|
dispatch({
|
|
1794
836
|
type: "SET_TRANSLATION_JOB_STATUS",
|
|
1795
|
-
payload:
|
|
1796
|
-
|
|
1797
|
-
language,
|
|
1798
|
-
status
|
|
1799
|
-
})
|
|
1800
|
-
});
|
|
837
|
+
payload: job
|
|
838
|
+
}), sourceDocumentId && updateActiveTranslationJob(sourceDocumentId, job);
|
|
1801
839
|
};
|
|
1802
840
|
function shouldExcludeField(fieldPath, excludeFields) {
|
|
1803
841
|
for (const pattern of excludeFields) {
|
|
@@ -2331,7 +1369,21 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2331
1369
|
parent && parent.children.push(node);
|
|
2332
1370
|
} else
|
|
2333
1371
|
root.push(node);
|
|
2334
|
-
else
|
|
1372
|
+
else if (attributes.type === "string" && !content) {
|
|
1373
|
+
const contentLines = [];
|
|
1374
|
+
for (i++; i < lines.length; ) {
|
|
1375
|
+
const currentLine = lines[i];
|
|
1376
|
+
if (currentLine.trim() === "</field>")
|
|
1377
|
+
break;
|
|
1378
|
+
contentLines.push(currentLine), i++;
|
|
1379
|
+
}
|
|
1380
|
+
if (node.text = contentLines.join(`
|
|
1381
|
+
`).trim(), stack.length > 0) {
|
|
1382
|
+
const parent = stack[stack.length - 1];
|
|
1383
|
+
parent && parent.children.push(node);
|
|
1384
|
+
} else
|
|
1385
|
+
root.push(node);
|
|
1386
|
+
} else
|
|
2335
1387
|
stack.push(node);
|
|
2336
1388
|
continue;
|
|
2337
1389
|
}
|
|
@@ -2879,7 +1931,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2879
1931
|
translationMode,
|
|
2880
1932
|
openaiConfig,
|
|
2881
1933
|
releaseId,
|
|
2882
|
-
excludeFields = []
|
|
1934
|
+
excludeFields = [],
|
|
1935
|
+
replaceExisting = !1,
|
|
1936
|
+
mainSourceDocumentId
|
|
2883
1937
|
}) => {
|
|
2884
1938
|
const safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
|
|
2885
1939
|
excludeFields.length !== safeExcludeFields.length && devLog.warn("Language field was in excludeFields - removing it to prevent translation errors", {
|
|
@@ -2902,20 +1956,29 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2902
1956
|
filteredTitleValue: filteredDocument.title,
|
|
2903
1957
|
excludedValuesKeys: Object.keys(excludedValues),
|
|
2904
1958
|
excludedValues
|
|
2905
|
-
}), devLog.debug("Applying field transforms before translation", {
|
|
2906
|
-
documentId: sourceDocument._id,
|
|
2907
|
-
documentType: sourceDocument._type
|
|
2908
|
-
});
|
|
2909
|
-
const { transformedDocument, originalStructure } = applyTransforms({
|
|
2910
|
-
document: filteredDocument,
|
|
2911
|
-
registry: globalTransformRegistry,
|
|
2912
|
-
schemaType: sourceDocument._type
|
|
2913
1959
|
});
|
|
1960
|
+
let documentToTranslate, originalStructure = {};
|
|
1961
|
+
if (translationMode === "openai-only") {
|
|
1962
|
+
devLog.debug("Applying field transforms before translation (OpenAI mode)", {
|
|
1963
|
+
documentId: sourceDocument._id,
|
|
1964
|
+
documentType: sourceDocument._type
|
|
1965
|
+
});
|
|
1966
|
+
const transformResult = applyTransforms({
|
|
1967
|
+
document: filteredDocument,
|
|
1968
|
+
registry: globalTransformRegistry,
|
|
1969
|
+
schemaType: sourceDocument._type
|
|
1970
|
+
});
|
|
1971
|
+
documentToTranslate = transformResult.transformedDocument, originalStructure = transformResult.originalStructure;
|
|
1972
|
+
} else
|
|
1973
|
+
devLog.debug("Skipping field transforms (Sanity Agent mode)", {
|
|
1974
|
+
documentId: sourceDocument._id,
|
|
1975
|
+
documentType: sourceDocument._type
|
|
1976
|
+
}), documentToTranslate = filteredDocument;
|
|
2914
1977
|
let translatedDoc;
|
|
2915
1978
|
switch (translationMode) {
|
|
2916
1979
|
case "sanity-only": {
|
|
2917
|
-
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
|
|
2918
|
-
sourceDocument:
|
|
1980
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating", void 0, mainSourceDocumentId), translatedDoc = await getSanityAgentTranslation({
|
|
1981
|
+
sourceDocument: documentToTranslate,
|
|
2919
1982
|
targetLanguage,
|
|
2920
1983
|
languageFieldName,
|
|
2921
1984
|
schemaId,
|
|
@@ -2927,9 +1990,16 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2927
1990
|
case "openai-only": {
|
|
2928
1991
|
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2929
1992
|
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2930
|
-
setJobStatus(
|
|
1993
|
+
setJobStatus(
|
|
1994
|
+
dispatch,
|
|
1995
|
+
sourceDocument._id,
|
|
1996
|
+
targetLanguage,
|
|
1997
|
+
"translating_with_openai",
|
|
1998
|
+
void 0,
|
|
1999
|
+
mainSourceDocumentId
|
|
2000
|
+
), translatedDoc = await getOpenAITranslation({
|
|
2931
2001
|
dispatch,
|
|
2932
|
-
sourceDocument:
|
|
2002
|
+
sourceDocument: documentToTranslate,
|
|
2933
2003
|
targetLanguage,
|
|
2934
2004
|
languageFieldName,
|
|
2935
2005
|
translationPrompts,
|
|
@@ -2937,23 +2007,22 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2937
2007
|
});
|
|
2938
2008
|
break;
|
|
2939
2009
|
}
|
|
2940
|
-
case "sanity-with-review":
|
|
2941
|
-
throw new Error("Sanity with review translation method is not implemented");
|
|
2942
2010
|
default:
|
|
2943
2011
|
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2944
2012
|
}
|
|
2945
2013
|
if (!translatedDoc)
|
|
2946
2014
|
return null;
|
|
2947
|
-
|
|
2015
|
+
let transformedResult;
|
|
2016
|
+
Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
|
|
2948
2017
|
documentId: sourceDocument._id,
|
|
2949
2018
|
documentType: sourceDocument._type
|
|
2950
|
-
})
|
|
2951
|
-
const transformedResult = restoreTransforms({
|
|
2019
|
+
}), transformedResult = restoreTransforms({
|
|
2952
2020
|
translatedDocument: translatedDoc,
|
|
2953
2021
|
originalStructure,
|
|
2954
2022
|
registry: globalTransformRegistry,
|
|
2955
2023
|
schemaType: sourceDocument._type
|
|
2956
|
-
})
|
|
2024
|
+
})) : transformedResult = translatedDoc;
|
|
2025
|
+
const excludedValuesToRestore = { ...excludedValues };
|
|
2957
2026
|
delete excludedValuesToRestore[languageFieldName], devLog.info("Before restoring excluded fields", {
|
|
2958
2027
|
documentId: sourceDocument._id,
|
|
2959
2028
|
excludedFieldCount: Object.keys(excludedValues).length,
|
|
@@ -2972,7 +2041,11 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2972
2041
|
...finalDoc,
|
|
2973
2042
|
_id: targetDocumentVersionId
|
|
2974
2043
|
};
|
|
2975
|
-
return await client.
|
|
2044
|
+
return replaceExisting && await client.getDocument(targetDocumentVersionId) && (devLog.info("Replacing existing version in release", {
|
|
2045
|
+
documentId: sourceDocument._id,
|
|
2046
|
+
targetDocumentVersionId,
|
|
2047
|
+
releaseId
|
|
2048
|
+
}), await client.delete(targetDocumentVersionId)), await client.action({
|
|
2976
2049
|
actionType: "sanity.action.document.version.create",
|
|
2977
2050
|
publishedId: targetDocumentPublishedId,
|
|
2978
2051
|
document: newDoc
|
|
@@ -2984,84 +2057,66 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2984
2057
|
sourceLanguage,
|
|
2985
2058
|
targetLanguage,
|
|
2986
2059
|
languageFieldName,
|
|
2987
|
-
shouldOverwriteFreshTranslations,
|
|
2988
2060
|
schemaId,
|
|
2989
2061
|
translationPrompts,
|
|
2990
2062
|
translationMode,
|
|
2991
2063
|
openaiConfig,
|
|
2992
2064
|
releaseId,
|
|
2993
|
-
excludeFields
|
|
2065
|
+
excludeFields,
|
|
2066
|
+
replaceExisting,
|
|
2067
|
+
mainSourceDocumentId
|
|
2994
2068
|
}) => {
|
|
2995
2069
|
const logContext = {
|
|
2996
2070
|
documentId: sourceDocument._id,
|
|
2997
2071
|
targetLanguage,
|
|
2998
2072
|
schemaId,
|
|
2999
|
-
shouldOverwriteFreshTranslations,
|
|
3000
2073
|
translationMode
|
|
3001
2074
|
};
|
|
3002
|
-
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
|
|
2075
|
+
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation", void 0, mainSourceDocumentId);
|
|
3003
2076
|
try {
|
|
3004
2077
|
const translationMetadata = await getTranslationMetadata({
|
|
3005
2078
|
client,
|
|
3006
2079
|
sourceDocument,
|
|
3007
2080
|
sourceLanguage
|
|
3008
|
-
}),
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
2081
|
+
}), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
|
|
2082
|
+
dispatch,
|
|
2083
|
+
client,
|
|
2084
|
+
schemaId,
|
|
2085
|
+
sourceDocument,
|
|
2086
|
+
targetLanguage,
|
|
2087
|
+
existingTranslationId,
|
|
2088
|
+
languageFieldName,
|
|
2089
|
+
translationPrompts,
|
|
2090
|
+
translationMode,
|
|
2091
|
+
openaiConfig,
|
|
2092
|
+
releaseId,
|
|
2093
|
+
excludeFields,
|
|
2094
|
+
replaceExisting,
|
|
2095
|
+
mainSourceDocumentId
|
|
2096
|
+
});
|
|
2097
|
+
if (!newDocument) {
|
|
2098
|
+
devLog.error("Translation failed: no document returned", new Error("Translation returned null"), logContext), setJobStatus(
|
|
3019
2099
|
dispatch,
|
|
3020
|
-
|
|
3021
|
-
schemaId,
|
|
3022
|
-
sourceDocument,
|
|
2100
|
+
sourceDocument._id,
|
|
3023
2101
|
targetLanguage,
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
excludeFields
|
|
3031
|
-
}),
|
|
3032
|
-
{
|
|
3033
|
-
maxRetries: 3,
|
|
3034
|
-
initialDelayMs: 1e3,
|
|
3035
|
-
isRetryable: isRetryableError,
|
|
3036
|
-
onRetry: (attempt, error, delayMs) => {
|
|
3037
|
-
devLog.warn("Translation failed, scheduling retry", {
|
|
3038
|
-
...logContext,
|
|
3039
|
-
attempt,
|
|
3040
|
-
delayMs,
|
|
3041
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3042
|
-
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "retrying", {
|
|
3043
|
-
retryCount: attempt,
|
|
3044
|
-
errorMessage: error instanceof Error ? error.message : String(error)
|
|
3045
|
-
});
|
|
3046
|
-
}
|
|
3047
|
-
}
|
|
3048
|
-
);
|
|
3049
|
-
if (!translationResult.success || !translationResult.result) {
|
|
3050
|
-
const errorMessage = translationResult.error instanceof Error ? translationResult.error.message : String(translationResult.error || "Translation failed");
|
|
3051
|
-
devLog.error("Translation failed after retries", translationResult.error, {
|
|
3052
|
-
...logContext,
|
|
3053
|
-
attempts: translationResult.attempts
|
|
3054
|
-
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
3055
|
-
errorMessage,
|
|
3056
|
-
retryCount: translationResult.attempts - 1
|
|
3057
|
-
// -1 because attempts includes initial try
|
|
3058
|
-
}), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
2102
|
+
"error",
|
|
2103
|
+
{
|
|
2104
|
+
errorMessage: "Translation failed"
|
|
2105
|
+
},
|
|
2106
|
+
mainSourceDocumentId
|
|
2107
|
+
), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
3059
2108
|
return;
|
|
3060
2109
|
}
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
2110
|
+
return setJobStatus(
|
|
2111
|
+
dispatch,
|
|
2112
|
+
sourceDocument._id,
|
|
2113
|
+
targetLanguage,
|
|
2114
|
+
"updating_metadata",
|
|
2115
|
+
{
|
|
2116
|
+
targetId: newDocument._id
|
|
2117
|
+
},
|
|
2118
|
+
mainSourceDocumentId
|
|
2119
|
+
), await updateTranslationMetadata({
|
|
3065
2120
|
client,
|
|
3066
2121
|
sourceDocument,
|
|
3067
2122
|
sourceLanguage,
|
|
@@ -3069,17 +2124,31 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3069
2124
|
targetLanguage,
|
|
3070
2125
|
translationMetadata,
|
|
3071
2126
|
isNewTranslation: !existingTranslationId
|
|
3072
|
-
}), setJobStatus(
|
|
3073
|
-
|
|
3074
|
-
|
|
2127
|
+
}), setJobStatus(
|
|
2128
|
+
dispatch,
|
|
2129
|
+
sourceDocument._id,
|
|
2130
|
+
targetLanguage,
|
|
2131
|
+
"completed",
|
|
2132
|
+
{
|
|
2133
|
+
targetId: newDocument._id
|
|
2134
|
+
},
|
|
2135
|
+
mainSourceDocumentId
|
|
2136
|
+
), devLog.info("Translation completed successfully", {
|
|
3075
2137
|
...logContext,
|
|
3076
2138
|
newDocumentId: newDocument._id
|
|
3077
2139
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
3078
2140
|
} catch (error) {
|
|
3079
2141
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3080
|
-
devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(
|
|
3081
|
-
|
|
3082
|
-
|
|
2142
|
+
devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(
|
|
2143
|
+
dispatch,
|
|
2144
|
+
sourceDocument._id,
|
|
2145
|
+
targetLanguage,
|
|
2146
|
+
"error",
|
|
2147
|
+
{
|
|
2148
|
+
errorMessage
|
|
2149
|
+
},
|
|
2150
|
+
mainSourceDocumentId
|
|
2151
|
+
), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
3083
2152
|
return;
|
|
3084
2153
|
}
|
|
3085
2154
|
}, translationSemaphore = new Semaphore(2), translateDocumentSet = async ({
|
|
@@ -3088,13 +2157,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3088
2157
|
targetLanguages,
|
|
3089
2158
|
dispatch,
|
|
3090
2159
|
languageFieldName,
|
|
3091
|
-
shouldOverwriteFreshTranslations,
|
|
3092
2160
|
schemaId,
|
|
3093
2161
|
references = [],
|
|
3094
2162
|
maxConcurrentTranslationJobs,
|
|
3095
2163
|
translationMode,
|
|
3096
2164
|
openaiConfig,
|
|
3097
|
-
excludeFields
|
|
2165
|
+
excludeFields,
|
|
2166
|
+
releaseConfig
|
|
3098
2167
|
}) => {
|
|
3099
2168
|
const logContext = {
|
|
3100
2169
|
documentId: sourceDocument._id,
|
|
@@ -3103,28 +2172,36 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3103
2172
|
referencesCount: references.length,
|
|
3104
2173
|
maxConcurrentJobs: maxConcurrentTranslationJobs,
|
|
3105
2174
|
translationMode,
|
|
3106
|
-
|
|
2175
|
+
releaseMode: releaseConfig.mode
|
|
3107
2176
|
};
|
|
3108
|
-
let releaseId;
|
|
2177
|
+
let releaseId, createdNewRelease = !1;
|
|
3109
2178
|
try {
|
|
3110
2179
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
3111
2180
|
if (!sourceLanguage)
|
|
3112
2181
|
throw devLog.error("Source language not found"), new Error("Source language not found");
|
|
3113
|
-
maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
2182
|
+
if (maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
3114
2183
|
type: "SET_OVERALL_STATUS",
|
|
3115
2184
|
payload: "translating"
|
|
3116
|
-
})
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
}
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
2185
|
+
}), releaseConfig.mode === "existing" && releaseConfig.existingReleaseId)
|
|
2186
|
+
releaseId = releaseConfig.existingReleaseId, devLog.info("Using existing Content Release", {
|
|
2187
|
+
...logContext,
|
|
2188
|
+
releaseId
|
|
2189
|
+
});
|
|
2190
|
+
else {
|
|
2191
|
+
const releaseTitle = releaseConfig.newReleaseTitle, releaseDescription = `AI translation batch created on ${(/* @__PURE__ */ new Date()).toLocaleDateString()} at ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`;
|
|
2192
|
+
releaseId = (await client.releases.create({
|
|
2193
|
+
metadata: {
|
|
2194
|
+
title: releaseTitle,
|
|
2195
|
+
description: releaseDescription,
|
|
2196
|
+
releaseType: "undecided"
|
|
2197
|
+
}
|
|
2198
|
+
})).releaseId, createdNewRelease = !0, devLog.info("Content Release created", {
|
|
2199
|
+
...logContext,
|
|
2200
|
+
releaseId,
|
|
2201
|
+
releaseTitle
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
registerActiveTranslation(sourceDocument._id, releaseId), updateActiveTranslationStatus(sourceDocument._id, "translating");
|
|
3128
2205
|
const sourceDocuments = [
|
|
3129
2206
|
{
|
|
3130
2207
|
_id: sourceDocument._id,
|
|
@@ -3139,7 +2216,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3139
2216
|
targetLanguages,
|
|
3140
2217
|
sourceDocument
|
|
3141
2218
|
});
|
|
3142
|
-
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
|
|
2219
|
+
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages, sourceDocument._id);
|
|
3143
2220
|
const translationTasks = sourceDocumentIds.flatMap(
|
|
3144
2221
|
(documentId) => targetLanguages.map(async (targetLanguage) => {
|
|
3145
2222
|
try {
|
|
@@ -3147,7 +2224,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3147
2224
|
const document = await client.withConfig({
|
|
3148
2225
|
perspective: "drafts"
|
|
3149
2226
|
}).getDocument(documentId);
|
|
3150
|
-
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document &&
|
|
2227
|
+
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId) {
|
|
3151
2228
|
const versionedTargetDocumentId = await translateDocument({
|
|
3152
2229
|
dispatch,
|
|
3153
2230
|
client,
|
|
@@ -3155,13 +2232,15 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3155
2232
|
sourceLanguage,
|
|
3156
2233
|
targetLanguage,
|
|
3157
2234
|
languageFieldName,
|
|
3158
|
-
shouldOverwriteFreshTranslations,
|
|
3159
2235
|
schemaId,
|
|
3160
2236
|
translationPrompts,
|
|
3161
2237
|
translationMode,
|
|
3162
2238
|
openaiConfig,
|
|
3163
|
-
releaseId
|
|
3164
|
-
excludeFields
|
|
2239
|
+
releaseId,
|
|
2240
|
+
excludeFields,
|
|
2241
|
+
replaceExisting: releaseConfig.replaceExisting,
|
|
2242
|
+
// Pass main source document ID for active translations tracking
|
|
2243
|
+
mainSourceDocumentId: sourceDocument._id
|
|
3165
2244
|
});
|
|
3166
2245
|
versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
|
|
3167
2246
|
}
|
|
@@ -3188,7 +2267,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3188
2267
|
}), dispatch({
|
|
3189
2268
|
type: "SET_OVERALL_STATUS",
|
|
3190
2269
|
payload: "linking_references"
|
|
3191
|
-
}), await Promise.all(
|
|
2270
|
+
}), updateActiveTranslationStatus(sourceDocument._id, "linking_references"), await Promise.all(
|
|
3192
2271
|
versionedTargetDocumentIds.map(async (documentId) => {
|
|
3193
2272
|
await localizeReferences({
|
|
3194
2273
|
client,
|
|
@@ -3199,18 +2278,65 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3199
2278
|
})
|
|
3200
2279
|
);
|
|
3201
2280
|
const hasAnyFailures = failed > 0;
|
|
3202
|
-
hasAnyFailures && succeeded === 0 ? (devLog.error("All translation tasks failed", void 0, logContext), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" })) : hasAnyFailures ? (devLog.warn("Translation completed with some failures", {
|
|
2281
|
+
hasAnyFailures && succeeded === 0 ? (devLog.error("All translation tasks failed", void 0, logContext), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id)) : hasAnyFailures ? (devLog.warn("Translation completed with some failures", {
|
|
3203
2282
|
...logContext,
|
|
3204
2283
|
succeeded,
|
|
3205
2284
|
failed
|
|
3206
|
-
}), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
|
|
2285
|
+
}), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), updateActiveTranslationStatus(sourceDocument._id, "completed"), cleanupActiveTranslation(sourceDocument._id)) : (dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), updateActiveTranslationStatus(sourceDocument._id, "completed"), cleanupActiveTranslation(sourceDocument._id)), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
|
|
3207
2286
|
} catch (error) {
|
|
3208
|
-
|
|
2287
|
+
if (devLog.error(
|
|
3209
2288
|
"translateDocumentSet failed with unexpected error",
|
|
3210
2289
|
error instanceof Error ? error : new Error(String(error)),
|
|
3211
2290
|
logContext
|
|
3212
|
-
),
|
|
3213
|
-
|
|
2291
|
+
), releaseId && createdNewRelease)
|
|
2292
|
+
try {
|
|
2293
|
+
await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
|
|
2294
|
+
...logContext,
|
|
2295
|
+
releaseId
|
|
2296
|
+
});
|
|
2297
|
+
} catch (cleanupError) {
|
|
2298
|
+
devLog.error(
|
|
2299
|
+
"Failed to cleanup release",
|
|
2300
|
+
cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
|
|
2301
|
+
logContext
|
|
2302
|
+
);
|
|
2303
|
+
}
|
|
2304
|
+
throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), updateActiveTranslationStatus(sourceDocument._id, "error"), cleanupActiveTranslation(sourceDocument._id), error;
|
|
2305
|
+
}
|
|
2306
|
+
}, ReleaseConflictWarning = ({ conflicts, onCancel, onReplace }) => {
|
|
2307
|
+
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]);
|
|
2308
|
+
return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2309
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
2310
|
+
/* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
|
|
2311
|
+
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "This release already contains translations for this content and target language(s)." })
|
|
2312
|
+
] }),
|
|
2313
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2314
|
+
"The selected release already has ",
|
|
2315
|
+
summaryText,
|
|
2316
|
+
" that will be overwritten if you continue:"
|
|
2317
|
+
] }),
|
|
2318
|
+
/* @__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: [
|
|
2319
|
+
conflict.sourceDocumentTitle || conflict.sourceDocumentId,
|
|
2320
|
+
conflict.sourceDocumentType && ` (${conflict.sourceDocumentType})`,
|
|
2321
|
+
" \u2192 ",
|
|
2322
|
+
conflict.targetLanguage
|
|
2323
|
+
] }, conflict.existingVersionId)) }) }) }),
|
|
2324
|
+
/* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2325
|
+
/* @__PURE__ */ jsx(Heading, { size: 0, children: "Choose an action:" }),
|
|
2326
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2327
|
+
/* @__PURE__ */ jsx("strong", { children: "Cancel" }),
|
|
2328
|
+
" \u2014 Go back and select a different release, or create a new one."
|
|
2329
|
+
] }),
|
|
2330
|
+
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
2331
|
+
/* @__PURE__ */ jsx("strong", { children: "Replace" }),
|
|
2332
|
+
" \u2014 Delete the existing translations in this release and create new ones."
|
|
2333
|
+
] })
|
|
2334
|
+
] }),
|
|
2335
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", children: [
|
|
2336
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Cancel", onClick: onCancel }),
|
|
2337
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "caution", text: "Replace Existing", onClick: onReplace })
|
|
2338
|
+
] })
|
|
2339
|
+
] }) });
|
|
3214
2340
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
3215
2341
|
const {
|
|
3216
2342
|
document,
|
|
@@ -3219,39 +2345,81 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3219
2345
|
dispatch,
|
|
3220
2346
|
references,
|
|
3221
2347
|
selectedTargetLanguages,
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }),
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
2348
|
+
translationMode,
|
|
2349
|
+
releaseConfig
|
|
2350
|
+
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1);
|
|
2351
|
+
useEffect(() => {
|
|
2352
|
+
releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
|
|
2353
|
+
}, [releaseConfig.mode, conflictContext]);
|
|
2354
|
+
const startTranslation = useCallback(
|
|
2355
|
+
(replaceExisting, overrideReleaseId) => {
|
|
2356
|
+
setConflictContext(null), translateDocumentSet({
|
|
2357
|
+
sourceDocument: document,
|
|
2358
|
+
targetLanguages: selectedTargetLanguages,
|
|
2359
|
+
client,
|
|
2360
|
+
dispatch,
|
|
2361
|
+
languageFieldName: config.languageFieldName,
|
|
2362
|
+
schemaId: config.schemaId,
|
|
2363
|
+
references,
|
|
2364
|
+
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
2365
|
+
maxReferenceDepth: config.maxReferenceDepth,
|
|
2366
|
+
translationMode,
|
|
2367
|
+
openaiConfig: config.openai,
|
|
2368
|
+
excludeFields: config.excludeFields,
|
|
2369
|
+
releaseConfig: {
|
|
2370
|
+
...releaseConfig,
|
|
2371
|
+
replaceExisting,
|
|
2372
|
+
// Use the override release ID if provided (from conflict context),
|
|
2373
|
+
// otherwise use the current releaseConfig value
|
|
2374
|
+
existingReleaseId: overrideReleaseId ?? releaseConfig.existingReleaseId
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2377
|
+
},
|
|
2378
|
+
[
|
|
2379
|
+
document,
|
|
2380
|
+
selectedTargetLanguages,
|
|
3228
2381
|
client,
|
|
3229
2382
|
dispatch,
|
|
3230
|
-
|
|
3231
|
-
|
|
2383
|
+
config.languageFieldName,
|
|
2384
|
+
config.schemaId,
|
|
2385
|
+
config.maxConcurrentTranslationJobs,
|
|
2386
|
+
config.maxReferenceDepth,
|
|
2387
|
+
config.openai,
|
|
2388
|
+
config.excludeFields,
|
|
3232
2389
|
references,
|
|
3233
|
-
shouldOverwriteFreshTranslations,
|
|
3234
|
-
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
3235
|
-
maxReferenceDepth: config.maxReferenceDepth,
|
|
3236
2390
|
translationMode,
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
2391
|
+
releaseConfig
|
|
2392
|
+
]
|
|
2393
|
+
), handleTranslate = useCallback(async () => {
|
|
2394
|
+
if (releaseConfig.mode === "new") {
|
|
2395
|
+
startTranslation(!1);
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (releaseConfig.mode === "existing" && releaseConfig.existingReleaseId) {
|
|
2399
|
+
const checkedReleaseId = releaseConfig.existingReleaseId;
|
|
2400
|
+
setIsCheckingConflicts(!0);
|
|
2401
|
+
const foundConflicts = await checkReleaseConflicts({
|
|
2402
|
+
client,
|
|
2403
|
+
releaseId: checkedReleaseId,
|
|
2404
|
+
sourceDocument: document,
|
|
2405
|
+
targetLanguages: selectedTargetLanguages,
|
|
2406
|
+
languageFieldName: config.languageFieldName,
|
|
2407
|
+
references
|
|
2408
|
+
});
|
|
2409
|
+
if (setIsCheckingConflicts(!1), foundConflicts.length > 0) {
|
|
2410
|
+
setConflictContext({
|
|
2411
|
+
releaseId: checkedReleaseId,
|
|
2412
|
+
conflicts: foundConflicts
|
|
2413
|
+
});
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
startTranslation(!1);
|
|
2418
|
+
}, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, references]), handleCancelConflict = useCallback(() => {
|
|
2419
|
+
setConflictContext(null);
|
|
2420
|
+
}, []), handleReplaceConflict = useCallback(() => {
|
|
2421
|
+
conflictContext && startTranslation(!0, conflictContext.releaseId);
|
|
2422
|
+
}, [startTranslation, conflictContext]), handleSelectAll = useCallback(() => {
|
|
3255
2423
|
dispatch({
|
|
3256
2424
|
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
3257
2425
|
payload: config.supportedLanguages.map((lang) => lang.id).filter((id) => id !== document.language)
|
|
@@ -3262,7 +2430,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3262
2430
|
payload: []
|
|
3263
2431
|
});
|
|
3264
2432
|
}, [dispatch]);
|
|
3265
|
-
return /* @__PURE__ */
|
|
2433
|
+
return conflictContext ? /* @__PURE__ */ jsx(Stack, { space: 4, children: /* @__PURE__ */ jsx(
|
|
2434
|
+
ReleaseConflictWarning,
|
|
2435
|
+
{
|
|
2436
|
+
conflicts: conflictContext.conflicts,
|
|
2437
|
+
onCancel: handleCancelConflict,
|
|
2438
|
+
onReplace: handleReplaceConflict
|
|
2439
|
+
}
|
|
2440
|
+
) }) : /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
|
|
3266
2441
|
/* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
|
|
3267
2442
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectAll, children: "Select All" }),
|
|
3268
2443
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectNone, children: "Select None" })
|
|
@@ -3276,16 +2451,41 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3276
2451
|
tone: "positive",
|
|
3277
2452
|
size: 2,
|
|
3278
2453
|
onClick: handleTranslate,
|
|
3279
|
-
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus),
|
|
3280
|
-
children: "Translate"
|
|
2454
|
+
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus) || releaseConfig.mode === "existing" && !releaseConfig.existingReleaseId || isCheckingConflicts,
|
|
2455
|
+
children: isCheckingConflicts ? "Checking..." : "Translate"
|
|
3281
2456
|
}
|
|
3282
2457
|
)
|
|
3283
2458
|
] })
|
|
3284
2459
|
] });
|
|
2460
|
+
}, TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
|
|
2461
|
+
const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
|
|
2462
|
+
params: {
|
|
2463
|
+
documentId: undraftId(documentId),
|
|
2464
|
+
translationMetadataType: TRANSLATION_METADATA_TYPE
|
|
2465
|
+
},
|
|
2466
|
+
initialValue: null
|
|
2467
|
+
});
|
|
2468
|
+
return useEffect(() => {
|
|
2469
|
+
if (loading) {
|
|
2470
|
+
dispatch({ type: "SET_OVERALL_STATUS", payload: "fetching_metadata" });
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
if (error) {
|
|
2474
|
+
dispatch({ type: "SET_OVERALL_STATUS", payload: "error" });
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
if (data) {
|
|
2478
|
+
dispatch({ type: "SET_TRANSLATION_METADATA", payload: data }), dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
dispatch({ type: "SET_OVERALL_STATUS", payload: "idle" });
|
|
2482
|
+
}, [data, loading, error, dispatch]), {
|
|
2483
|
+
data,
|
|
2484
|
+
loading,
|
|
2485
|
+
error
|
|
2486
|
+
};
|
|
3285
2487
|
}, LanguageCheckbox = ({ lang, isLoading }) => {
|
|
3286
|
-
const { document, dispatch, selectedTargetLanguages
|
|
3287
|
-
(history) => history.language === lang.id && history.sourceRev !== document._rev
|
|
3288
|
-
), handleChange = () => {
|
|
2488
|
+
const { document, dispatch, selectedTargetLanguages } = useTranslationContext(), isDisabled = lang.id === document.language, isChecked = selectedTargetLanguages.includes(lang.id), handleChange = () => {
|
|
3289
2489
|
if (isDisabled) return;
|
|
3290
2490
|
const updatedLanguages = isChecked ? selectedTargetLanguages.filter((langId) => langId !== lang.id) : [...selectedTargetLanguages, lang.id];
|
|
3291
2491
|
dispatch({
|
|
@@ -3303,16 +2503,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3303
2503
|
disabled: isDisabled,
|
|
3304
2504
|
tone: isChecked ? "primary" : "default",
|
|
3305
2505
|
muted: !0,
|
|
3306
|
-
children: /* @__PURE__ */
|
|
3307
|
-
/* @__PURE__ */
|
|
3308
|
-
|
|
3309
|
-
/* @__PURE__ */
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
] }),
|
|
3314
|
-
!isDisabled && hasTranslation && /* @__PURE__ */ jsx(Flex, { flex: 1, justify: "flex-end", children: /* @__PURE__ */ jsx(Badge, { tone: isStale ? "caution" : "positive", size: 3, muted: !0, children: isStale ? "Stale" : "Fresh" }) })
|
|
3315
|
-
] })
|
|
2506
|
+
children: /* @__PURE__ */ jsx(Flex, { align: "center", gap: 2, style: { cursor: "pointer" }, children: /* @__PURE__ */ jsxs(Inline, { space: 3, children: [
|
|
2507
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: isChecked, onChange: handleChange, label: lang.title, disabled: isDisabled }),
|
|
2508
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
2509
|
+
/* @__PURE__ */ jsx(Text, { children: lang.title }),
|
|
2510
|
+
isDisabled && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "The current language" })
|
|
2511
|
+
] })
|
|
2512
|
+
] }) })
|
|
3316
2513
|
}
|
|
3317
2514
|
);
|
|
3318
2515
|
}, LanguageSelectionPanel = () => {
|
|
@@ -3320,6 +2517,134 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3320
2517
|
documentId: document._id
|
|
3321
2518
|
});
|
|
3322
2519
|
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)) }) });
|
|
2520
|
+
}, ReleaseSelectionPanel = () => {
|
|
2521
|
+
const { dispatch, releaseConfig } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [releases, setReleases] = useState([]), [isLoading, setIsLoading] = useState(!1), [error, setError] = useState(null), [searchQuery, setSearchQuery] = useState("");
|
|
2522
|
+
useEffect(() => {
|
|
2523
|
+
(async () => {
|
|
2524
|
+
setIsLoading(!0), setError(null);
|
|
2525
|
+
try {
|
|
2526
|
+
const result = await client.fetch(
|
|
2527
|
+
'releases::all()[state == "active"] | order(_createdAt desc)'
|
|
2528
|
+
);
|
|
2529
|
+
setReleases(result), devLog.debug("Fetched active releases", {
|
|
2530
|
+
count: result.length,
|
|
2531
|
+
component: "ReleaseSelectionPanel"
|
|
2532
|
+
});
|
|
2533
|
+
} catch (err) {
|
|
2534
|
+
const errorMessage = err instanceof Error ? err.message : "Failed to fetch releases";
|
|
2535
|
+
setError(errorMessage), devLog.error("Failed to fetch releases", err instanceof Error ? err : new Error(errorMessage), {
|
|
2536
|
+
component: "ReleaseSelectionPanel"
|
|
2537
|
+
});
|
|
2538
|
+
} finally {
|
|
2539
|
+
setIsLoading(!1);
|
|
2540
|
+
}
|
|
2541
|
+
})();
|
|
2542
|
+
}, [client]);
|
|
2543
|
+
const handleModeChange = useCallback(
|
|
2544
|
+
(mode) => {
|
|
2545
|
+
dispatch({
|
|
2546
|
+
type: "SET_RELEASE_CONFIG",
|
|
2547
|
+
payload: { mode }
|
|
2548
|
+
});
|
|
2549
|
+
},
|
|
2550
|
+
[dispatch]
|
|
2551
|
+
), handleTitleChange = useCallback(
|
|
2552
|
+
(event) => {
|
|
2553
|
+
dispatch({
|
|
2554
|
+
type: "SET_RELEASE_CONFIG",
|
|
2555
|
+
payload: { newReleaseTitle: event.target.value }
|
|
2556
|
+
});
|
|
2557
|
+
},
|
|
2558
|
+
[dispatch]
|
|
2559
|
+
), handleSelectRelease = useCallback(
|
|
2560
|
+
(releaseId) => {
|
|
2561
|
+
dispatch({
|
|
2562
|
+
type: "SET_RELEASE_CONFIG",
|
|
2563
|
+
payload: { existingReleaseId: releaseId }
|
|
2564
|
+
});
|
|
2565
|
+
},
|
|
2566
|
+
[dispatch]
|
|
2567
|
+
), handleSearchChange = useCallback((event) => {
|
|
2568
|
+
setSearchQuery(event.target.value);
|
|
2569
|
+
}, []), filteredReleases = useMemo(() => {
|
|
2570
|
+
if (!searchQuery.trim())
|
|
2571
|
+
return releases;
|
|
2572
|
+
const query = searchQuery.toLowerCase();
|
|
2573
|
+
return releases.filter((release) => release.metadata.title.toLowerCase().includes(query));
|
|
2574
|
+
}, [releases, searchQuery]), formatDate = (dateString) => new Date(dateString).toLocaleDateString(void 0, {
|
|
2575
|
+
month: "short",
|
|
2576
|
+
day: "numeric",
|
|
2577
|
+
year: "numeric"
|
|
2578
|
+
});
|
|
2579
|
+
return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2580
|
+
/* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
2581
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, as: "label", style: { cursor: "pointer" }, onClick: () => handleModeChange("new"), children: [
|
|
2582
|
+
/* @__PURE__ */ jsx(Radio, { checked: releaseConfig.mode === "new", readOnly: !0 }),
|
|
2583
|
+
/* @__PURE__ */ jsx(Text, { weight: "medium", children: "Create new release" })
|
|
2584
|
+
] }),
|
|
2585
|
+
/* @__PURE__ */ jsxs(
|
|
2586
|
+
Flex,
|
|
2587
|
+
{
|
|
2588
|
+
align: "center",
|
|
2589
|
+
gap: 3,
|
|
2590
|
+
as: "label",
|
|
2591
|
+
style: { cursor: "pointer" },
|
|
2592
|
+
onClick: () => handleModeChange("existing"),
|
|
2593
|
+
children: [
|
|
2594
|
+
/* @__PURE__ */ jsx(Radio, { checked: releaseConfig.mode === "existing", readOnly: !0 }),
|
|
2595
|
+
/* @__PURE__ */ jsx(Text, { weight: "medium", children: "Add to existing release" })
|
|
2596
|
+
]
|
|
2597
|
+
}
|
|
2598
|
+
)
|
|
2599
|
+
] }),
|
|
2600
|
+
releaseConfig.mode === "new" && /* @__PURE__ */ jsx(
|
|
2601
|
+
TextInput,
|
|
2602
|
+
{
|
|
2603
|
+
value: releaseConfig.newReleaseTitle,
|
|
2604
|
+
onChange: handleTitleChange,
|
|
2605
|
+
placeholder: "Release title..."
|
|
2606
|
+
}
|
|
2607
|
+
),
|
|
2608
|
+
releaseConfig.mode === "existing" && /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
2609
|
+
isLoading && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
2610
|
+
/* @__PURE__ */ jsx(Spinner, { muted: !0 }),
|
|
2611
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Loading..." })
|
|
2612
|
+
] }),
|
|
2613
|
+
error && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: error }),
|
|
2614
|
+
!isLoading && !error && releases.length === 0 && /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "No active releases found." }),
|
|
2615
|
+
!isLoading && !error && releases.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2616
|
+
/* @__PURE__ */ jsx(
|
|
2617
|
+
TextInput,
|
|
2618
|
+
{
|
|
2619
|
+
icon: SearchIcon,
|
|
2620
|
+
value: searchQuery,
|
|
2621
|
+
onChange: handleSearchChange,
|
|
2622
|
+
placeholder: "Search releases..."
|
|
2623
|
+
}
|
|
2624
|
+
),
|
|
2625
|
+
/* @__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) => {
|
|
2626
|
+
const releaseName = release._id.replace("_.releases.", ""), isSelected = releaseConfig.existingReleaseId === releaseName;
|
|
2627
|
+
return /* @__PURE__ */ jsx(
|
|
2628
|
+
Card,
|
|
2629
|
+
{
|
|
2630
|
+
padding: 3,
|
|
2631
|
+
radius: 2,
|
|
2632
|
+
tone: isSelected ? "primary" : "default",
|
|
2633
|
+
style: { cursor: "pointer" },
|
|
2634
|
+
onClick: () => handleSelectRelease(releaseName),
|
|
2635
|
+
border: !0,
|
|
2636
|
+
muted: isSelected,
|
|
2637
|
+
children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
|
|
2638
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: release.metadata.title }),
|
|
2639
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: formatDate(release._createdAt) })
|
|
2640
|
+
] })
|
|
2641
|
+
},
|
|
2642
|
+
release._id
|
|
2643
|
+
);
|
|
2644
|
+
}) }) })
|
|
2645
|
+
] })
|
|
2646
|
+
] })
|
|
2647
|
+
] }) });
|
|
3323
2648
|
}, TranslationModePanel = () => {
|
|
3324
2649
|
const { config, translationMode, openaiValidated, dispatch } = useTranslationContext();
|
|
3325
2650
|
if (!config?.openai?.enabled || !config?.openai?.apiKey)
|
|
@@ -3338,7 +2663,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3338
2663
|
return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3339
2664
|
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3340
2665
|
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "Translation Mode" }),
|
|
3341
|
-
/* @__PURE__ */ jsx(Badge, { tone: "positive", children: "
|
|
2666
|
+
/* @__PURE__ */ jsx(Badge, { tone: "positive", children: "OpenAI Available" })
|
|
3342
2667
|
] }),
|
|
3343
2668
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3344
2669
|
/* @__PURE__ */ jsx(
|
|
@@ -3368,32 +2693,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3368
2693
|
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "openai-only", readOnly: !0 }),
|
|
3369
2694
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3370
2695
|
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3371
|
-
/* @__PURE__ */ jsx(Text, { children: "
|
|
2696
|
+
/* @__PURE__ */ jsx(Text, { children: "OpenAI Only" }),
|
|
3372
2697
|
/* @__PURE__ */ jsx(Badge, { tone: "caution", children: "Beta" })
|
|
3373
2698
|
] }),
|
|
3374
2699
|
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Full style guide capacity with 1M token context window." })
|
|
3375
2700
|
] })
|
|
3376
2701
|
] })
|
|
3377
2702
|
}
|
|
3378
|
-
),
|
|
3379
|
-
/* @__PURE__ */ jsx(
|
|
3380
|
-
Button,
|
|
3381
|
-
{
|
|
3382
|
-
type: "button",
|
|
3383
|
-
mode: translationMode === "sanity-with-review" ? "ghost" : "bleed",
|
|
3384
|
-
tone: translationMode === "sanity-with-review" ? "primary" : "default",
|
|
3385
|
-
onClick: () => handleModeChange("sanity-with-review"),
|
|
3386
|
-
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3387
|
-
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "sanity-with-review", readOnly: !0 }),
|
|
3388
|
-
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3389
|
-
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3390
|
-
/* @__PURE__ */ jsx(Text, { children: "Sanity + GPT-5 Review" }),
|
|
3391
|
-
/* @__PURE__ */ jsx(Badge, { tone: "primary", children: "Highest Quality" })
|
|
3392
|
-
] }),
|
|
3393
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Sanity translation refined by GPT-5. Takes longer but best results." })
|
|
3394
|
-
] })
|
|
3395
|
-
] })
|
|
3396
|
-
}
|
|
3397
2703
|
)
|
|
3398
2704
|
] }),
|
|
3399
2705
|
translationMode !== "sanity-only" && /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 3, border: !0, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
@@ -3556,41 +2862,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3556
2862
|
] }),
|
|
3557
2863
|
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
|
|
3558
2864
|
] });
|
|
3559
|
-
}, WriteSettingsPanel = () => {
|
|
3560
|
-
const { dispatch, shouldOverwriteFreshTranslations } = useTranslationContext(), handleShouldOverwriteFreshTranslationsChange = () => {
|
|
3561
|
-
dispatch({
|
|
3562
|
-
type: "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS",
|
|
3563
|
-
payload: !shouldOverwriteFreshTranslations
|
|
3564
|
-
});
|
|
3565
|
-
};
|
|
3566
|
-
return /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3567
|
-
/* @__PURE__ */ jsx(ReferenceConfigPanel, {}),
|
|
3568
|
-
/* @__PURE__ */ jsx(Flex, { justify: "space-between", align: "center", children: /* @__PURE__ */ jsx(
|
|
3569
|
-
Button,
|
|
3570
|
-
{
|
|
3571
|
-
type: "button",
|
|
3572
|
-
mode: shouldOverwriteFreshTranslations ? "ghost" : "bleed",
|
|
3573
|
-
size: 2,
|
|
3574
|
-
onClick: handleShouldOverwriteFreshTranslationsChange,
|
|
3575
|
-
tone: shouldOverwriteFreshTranslations ? "primary" : "neutral",
|
|
3576
|
-
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3577
|
-
/* @__PURE__ */ jsx(
|
|
3578
|
-
Checkbox,
|
|
3579
|
-
{
|
|
3580
|
-
checked: shouldOverwriteFreshTranslations,
|
|
3581
|
-
onChange: handleShouldOverwriteFreshTranslationsChange,
|
|
3582
|
-
label: shouldOverwriteFreshTranslations ? "Disable" : "Enable"
|
|
3583
|
-
}
|
|
3584
|
-
),
|
|
3585
|
-
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3586
|
-
/* @__PURE__ */ jsx(Text, { children: "Overwrite Fresh Translations" }),
|
|
3587
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "All existing translations will be overwritten even if they are fresh." })
|
|
3588
|
-
] })
|
|
3589
|
-
] })
|
|
3590
|
-
}
|
|
3591
|
-
) })
|
|
3592
|
-
] });
|
|
3593
|
-
}, TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2865
|
+
}, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
3594
2866
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3595
2867
|
/* @__PURE__ */ jsx(Label, { children: "Languages" }),
|
|
3596
2868
|
/* @__PURE__ */ jsx(LanguageSelectionPanel, {})
|
|
@@ -3599,16 +2871,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3599
2871
|
/* @__PURE__ */ jsx(Label, { children: "Translation Method" }),
|
|
3600
2872
|
/* @__PURE__ */ jsx(TranslationModePanel, {})
|
|
3601
2873
|
] }),
|
|
2874
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
2875
|
+
/* @__PURE__ */ jsx(Label, { children: "Content Release" }),
|
|
2876
|
+
/* @__PURE__ */ jsx(ReleaseSelectionPanel, {})
|
|
2877
|
+
] }),
|
|
3602
2878
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3603
2879
|
/* @__PURE__ */ jsx(Label, { children: "Settings" }),
|
|
3604
2880
|
/* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsx(WriteSettingsPanel, {}) })
|
|
3605
2881
|
] }),
|
|
3606
2882
|
/* @__PURE__ */ jsx(ActionButtonBar, { setIsDialogOpen })
|
|
3607
2883
|
] }), TERMINAL_STATUSES = ["completed", "skipped", "error"], TranslationStatusPanel = () => {
|
|
3608
|
-
const { overallStatus, translationJobs, translationMode } = useTranslationContext(), headline = useMemo(
|
|
3609
|
-
()
|
|
3610
|
-
|
|
3611
|
-
), jobs = useMemo(() => Array.from(translationJobs.values()), [translationJobs]), openaiStats = useMemo(() => {
|
|
2884
|
+
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(() => {
|
|
2885
|
+
forceCleanupActiveTranslation(document._id), dispatch({ type: "RESET_TRANSLATION_STATE" });
|
|
2886
|
+
}, [dispatch, document._id]), openaiStats = useMemo(() => {
|
|
3612
2887
|
if (translationMode === "sanity-only") return null;
|
|
3613
2888
|
const completedJobs = jobs.filter((j) => j.status === "completed"), totalTokens = completedJobs.reduce((sum, job) => sum + (job.tokensUsed || 0), 0), totalCost = completedJobs.reduce((sum, job) => sum + (job.estimatedCost || 0), 0);
|
|
3614
2889
|
return { totalTokens, totalCost, jobsWithUsage: completedJobs.filter((j) => j.tokensUsed).length };
|
|
@@ -3642,17 +2917,12 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3642
2917
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3643
2918
|
"Error: ",
|
|
3644
2919
|
job.errorMessage
|
|
3645
|
-
] }) }) : null,
|
|
3646
|
-
job.status === "retrying" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3647
|
-
"Retrying after: ",
|
|
3648
|
-
job.errorMessage
|
|
3649
2920
|
] }) }) : null
|
|
3650
2921
|
] })
|
|
3651
2922
|
] }),
|
|
3652
2923
|
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
3653
2924
|
job.status === "skipped" ? /* @__PURE__ */ jsx(SkippedBadge, {}) : null,
|
|
3654
|
-
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {
|
|
3655
|
-
job.status === "retrying" ? /* @__PURE__ */ jsx(RetryingBadge, { retryCount: job.retryCount }) : null,
|
|
2925
|
+
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {}) : null,
|
|
3656
2926
|
job.targetId ? /* @__PURE__ */ jsx(
|
|
3657
2927
|
TranslationOpenerButton,
|
|
3658
2928
|
{
|
|
@@ -3669,18 +2939,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3669
2939
|
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Usage Summary" }),
|
|
3670
2940
|
/* @__PURE__ */ jsxs(Inline, { space: 4, children: [
|
|
3671
2941
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3672
|
-
"
|
|
2942
|
+
"Total Tokens: ",
|
|
3673
2943
|
/* @__PURE__ */ jsx("strong", { children: openaiStats.totalTokens.toLocaleString() })
|
|
3674
2944
|
] }),
|
|
3675
2945
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3676
|
-
"
|
|
2946
|
+
"Total Cost: ",
|
|
3677
2947
|
/* @__PURE__ */ jsxs("strong", { children: [
|
|
3678
2948
|
"$",
|
|
3679
2949
|
openaiStats.totalCost.toFixed(4)
|
|
3680
2950
|
] })
|
|
3681
2951
|
] })
|
|
3682
2952
|
] })
|
|
3683
|
-
] }) })
|
|
2953
|
+
] }) }),
|
|
2954
|
+
isTerminalState && /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "primary", text: "Translate More", icon: ResetIcon, onClick: handleReset }) })
|
|
3684
2955
|
] });
|
|
3685
2956
|
}, ProgressBar = ({ progress, max = 100 }) => {
|
|
3686
2957
|
const percentage = Math.min(progress / max * 100, 100);
|
|
@@ -3732,35 +3003,20 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3732
3003
|
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "There was already a fresh translation for this document and language." }) }),
|
|
3733
3004
|
children: /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: "Skipped" })
|
|
3734
3005
|
}
|
|
3735
|
-
), ErrorBadge = (
|
|
3736
|
-
Tooltip,
|
|
3737
|
-
{
|
|
3738
|
-
portal: !0,
|
|
3739
|
-
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: retryCount && retryCount > 0 ? `This translation failed after ${retryCount} retry attempt${retryCount > 1 ? "s" : ""}.` : "This translation failed. Check the error message for details." }) }),
|
|
3740
|
-
children: /* @__PURE__ */ jsxs(Badge, { tone: "critical", children: [
|
|
3741
|
-
"Failed",
|
|
3742
|
-
retryCount && retryCount > 0 ? ` (${retryCount} retries)` : ""
|
|
3743
|
-
] })
|
|
3744
|
-
}
|
|
3745
|
-
), RetryingBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
|
|
3006
|
+
), ErrorBadge = () => /* @__PURE__ */ jsx(
|
|
3746
3007
|
Tooltip,
|
|
3747
3008
|
{
|
|
3748
3009
|
portal: !0,
|
|
3749
|
-
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "
|
|
3750
|
-
children: /* @__PURE__ */
|
|
3751
|
-
"Retry ",
|
|
3752
|
-
retryCount || 1
|
|
3753
|
-
] })
|
|
3010
|
+
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "This translation failed. Check the error message for details." }) }),
|
|
3011
|
+
children: /* @__PURE__ */ jsx(Badge, { tone: "critical", children: "Failed" })
|
|
3754
3012
|
}
|
|
3755
3013
|
), getStatusLabel = (status) => ({
|
|
3756
3014
|
idle: "Idle",
|
|
3757
3015
|
queued: "Queued...",
|
|
3758
3016
|
preparing_translation: "Preparing translation...",
|
|
3759
3017
|
translating: "Translating with Sanity Agent...",
|
|
3760
|
-
translating_with_openai: "Translating with
|
|
3761
|
-
reviewing_with_openai: "Reviewing translation with GPT-5...",
|
|
3018
|
+
translating_with_openai: "Translating with OpenAI...",
|
|
3762
3019
|
updating_metadata: "Saving metadata...",
|
|
3763
|
-
retrying: "Retrying...",
|
|
3764
3020
|
error: "Error",
|
|
3765
3021
|
skipped: "Skipped",
|
|
3766
3022
|
completed: "Completed"
|
|
@@ -3768,14 +3024,30 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3768
3024
|
"translating",
|
|
3769
3025
|
"linking_references",
|
|
3770
3026
|
"completed"
|
|
3771
|
-
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
|
|
3027
|
+
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen, onStatusChange }) => {
|
|
3772
3028
|
const { overallStatus, document } = useTranslationContext();
|
|
3773
|
-
return
|
|
3029
|
+
return useEffect(() => {
|
|
3030
|
+
onStatusChange?.(overallStatus);
|
|
3031
|
+
}, [overallStatus, onStatusChange]), STATES_IN_WHICH_TO_SHOW_CONFIG.includes(overallStatus) ? /* @__PURE__ */ jsx(TranslationConfigView, { setIsDialogOpen }) : STATES_IN_WHICH_TO_SHOW_TRANSLATION_PROGRESS.includes(overallStatus) ? /* @__PURE__ */ jsx(TranslationStatusPanel, {}) : STATES_IN_WHICH_TO_SHOW_ERROR.includes(overallStatus) ? /* @__PURE__ */ jsx("div", { children: "Error occurred" }) : (devLog.warn("Unhandled translation dialog state", {
|
|
3774
3032
|
documentId: document._id,
|
|
3775
3033
|
overallStatus
|
|
3776
3034
|
}), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
|
|
3777
|
-
}, getTranslateAction = (config) => ({ draft, published }) => {
|
|
3778
|
-
const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1)
|
|
3035
|
+
}, IN_PROGRESS_STATUSES = ["translating", "linking_references"], getButtonLabel = (status) => status === "completed" ? "Translation Complete" : status && IN_PROGRESS_STATUSES.includes(status) ? "Translating..." : "Translate", getTranslateAction = (config) => ({ draft, published }) => {
|
|
3036
|
+
const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1), [currentStatus, setCurrentStatus] = useState(null), isTranslating = currentStatus !== null && IN_PROGRESS_STATUSES.includes(currentStatus), handleStatusChange = useCallback((status) => {
|
|
3037
|
+
setCurrentStatus(status);
|
|
3038
|
+
}, []), handleClose = useCallback(() => {
|
|
3039
|
+
if (isTranslating) {
|
|
3040
|
+
if (!window.confirm(
|
|
3041
|
+
"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."
|
|
3042
|
+
))
|
|
3043
|
+
return;
|
|
3044
|
+
devLog.warn("User closed dialog during active translation", {
|
|
3045
|
+
documentId: document?._id,
|
|
3046
|
+
component: "TranslateAction"
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
setIsDialogOpen(!1);
|
|
3050
|
+
}, [isTranslating, document?._id]);
|
|
3779
3051
|
return devLog.debug("TranslateAction initialized", {
|
|
3780
3052
|
documentId: document?._id,
|
|
3781
3053
|
documentType: document?._type
|
|
@@ -3783,7 +3055,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3783
3055
|
documentType: document?._type,
|
|
3784
3056
|
translatableTypes: config.translatableSchemaTypes
|
|
3785
3057
|
}), null) : config ? {
|
|
3786
|
-
label:
|
|
3058
|
+
label: getButtonLabel(currentStatus),
|
|
3787
3059
|
icon: TranslateIcon,
|
|
3788
3060
|
onHandle: () => {
|
|
3789
3061
|
devLog.info("Translation dialog opened", {
|
|
@@ -3794,9 +3066,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3794
3066
|
},
|
|
3795
3067
|
dialog: isDialogOpen && {
|
|
3796
3068
|
type: "dialog",
|
|
3797
|
-
onClose:
|
|
3069
|
+
onClose: handleClose,
|
|
3798
3070
|
header: "Translate Document",
|
|
3799
|
-
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
|
|
3071
|
+
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen, onStatusChange: handleStatusChange }) })
|
|
3800
3072
|
}
|
|
3801
3073
|
} : (devLog.error("Missing config in TranslateAction"), null);
|
|
3802
3074
|
}, translationGlossaryTerm = defineType({
|
|
@@ -3912,7 +3184,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3912
3184
|
defineField({
|
|
3913
3185
|
name: "changeHistory",
|
|
3914
3186
|
title: "Change History",
|
|
3915
|
-
description: "History of changes made
|
|
3187
|
+
description: "History of changes made to translation settings.",
|
|
3916
3188
|
type: "array",
|
|
3917
3189
|
readOnly: !0,
|
|
3918
3190
|
group: "history",
|
|
@@ -4222,7 +3494,7 @@ const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.le
|
|
|
4222
3494
|
})), {
|
|
4223
3495
|
name: "sanity-plugin-ai-translation",
|
|
4224
3496
|
document: {
|
|
4225
|
-
actions: [getTranslateAction(config)
|
|
3497
|
+
actions: [getTranslateAction(config)]
|
|
4226
3498
|
},
|
|
4227
3499
|
schema: {
|
|
4228
3500
|
types: [translationSettings, translationGlossaryTerm]
|