@braze/sanity-plugin-ai-translation 0.2.3 → 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 +147 -5
- package/dist/index.js +1052 -1310
- package/dist/index.js.map +1 -1
- package/package.json +3 -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
322
|
} catch (error) {
|
|
573
|
-
throw
|
|
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
|
-
} catch (error) {
|
|
599
|
-
throw devLog.error("Error parsing AI response", { error, responseText }), new Error("Failed to parse AI response as JSON");
|
|
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;
|
|
@@ -1409,11 +584,21 @@ const fetchTranslationPrompts = async ({
|
|
|
1409
584
|
const sourceDocumentString = documentToPlainText(sourceDocument), translationSettings2 = await client.fetch(
|
|
1410
585
|
`*[_id == "translation-settings"][0] {
|
|
1411
586
|
globalPrompt,
|
|
1412
|
-
localePrompts[
|
|
587
|
+
localePrompts[_key in $targetLanguages],
|
|
1413
588
|
protectedPhrases
|
|
1414
589
|
}`,
|
|
1415
590
|
{ targetLanguages }
|
|
1416
|
-
)
|
|
591
|
+
);
|
|
592
|
+
devLog.debug("Fetched translation settings", {
|
|
593
|
+
hasSettings: !!translationSettings2,
|
|
594
|
+
hasGlobalPrompt: !!translationSettings2?.globalPrompt,
|
|
595
|
+
globalPromptLength: translationSettings2?.globalPrompt?.length,
|
|
596
|
+
localePromptsCount: translationSettings2?.localePrompts?.length,
|
|
597
|
+
localePrompts: translationSettings2?.localePrompts,
|
|
598
|
+
protectedPhrasesCount: translationSettings2?.protectedPhrases?.length,
|
|
599
|
+
targetLanguages
|
|
600
|
+
});
|
|
601
|
+
const translationGlossaryTerms = await client.fetch(
|
|
1417
602
|
`*[
|
|
1418
603
|
_type == "translationGlossaryTerm"
|
|
1419
604
|
&& count(translations[_key in $targetLanguages]) > 0
|
|
@@ -1433,9 +618,21 @@ const fetchTranslationPrompts = async ({
|
|
|
1433
618
|
)), glossaryTermsByLocale[targetLanguage] = relevantTranslations;
|
|
1434
619
|
}
|
|
1435
620
|
let filteredProtectedPhrases = translationSettings2?.protectedPhrases;
|
|
1436
|
-
return sourceDocument && filteredProtectedPhrases && (
|
|
1437
|
-
|
|
1438
|
-
|
|
621
|
+
return sourceDocument && filteredProtectedPhrases && (devLog.debug("Filtering protected phrases", {
|
|
622
|
+
totalPhrases: filteredProtectedPhrases.length,
|
|
623
|
+
phrases: filteredProtectedPhrases,
|
|
624
|
+
sourceDocumentStringLength: sourceDocumentString.length,
|
|
625
|
+
sourceDocumentPreview: sourceDocumentString.substring(0, 500)
|
|
626
|
+
}), filteredProtectedPhrases = filteredProtectedPhrases.filter((phrase) => {
|
|
627
|
+
const appears = protectedPhraseAppearsInDocument(phrase, sourceDocumentString);
|
|
628
|
+
return devLog.debug("Protected phrase match result", {
|
|
629
|
+
phrase,
|
|
630
|
+
appears
|
|
631
|
+
}), appears;
|
|
632
|
+
}), devLog.debug("Filtered protected phrases result", {
|
|
633
|
+
remainingCount: filteredProtectedPhrases.length,
|
|
634
|
+
remainingPhrases: filteredProtectedPhrases
|
|
635
|
+
})), {
|
|
1439
636
|
globalPrompt: translationSettings2?.globalPrompt,
|
|
1440
637
|
protectedPhrases: filteredProtectedPhrases,
|
|
1441
638
|
localeInstructions: Object.fromEntries(
|
|
@@ -1568,7 +765,7 @@ const fetchTranslationPrompts = async ({
|
|
|
1568
765
|
translatedCount: translationMap.size,
|
|
1569
766
|
component: "updateReferences"
|
|
1570
767
|
}));
|
|
1571
|
-
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages) => {
|
|
768
|
+
}, setupTranslationJobStatuses = (dispatch, sourceDocuments, targetLanguages, mainSourceDocumentId) => {
|
|
1572
769
|
for (const sourceDocument of sourceDocuments)
|
|
1573
770
|
for (const lang of targetLanguages) {
|
|
1574
771
|
const payload = {
|
|
@@ -1581,44 +778,8 @@ const fetchTranslationPrompts = async ({
|
|
|
1581
778
|
dispatch({
|
|
1582
779
|
type: "SET_TRANSLATION_JOB_STATUS",
|
|
1583
780
|
payload
|
|
1584
|
-
});
|
|
781
|
+
}), mainSourceDocumentId && updateActiveTranslationJob(mainSourceDocumentId, payload);
|
|
1585
782
|
}
|
|
1586
|
-
}, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
1587
|
-
(translation) => translation._key === targetLanguage
|
|
1588
|
-
)?.value._ref || null, getTranslationMetadata = async ({
|
|
1589
|
-
client,
|
|
1590
|
-
sourceDocument,
|
|
1591
|
-
sourceLanguage
|
|
1592
|
-
}) => {
|
|
1593
|
-
try {
|
|
1594
|
-
return await client.fetch(
|
|
1595
|
-
"*[_type == $tmType && references($sourceDocumentId)][0]",
|
|
1596
|
-
{
|
|
1597
|
-
tmType: TRANSLATION_METADATA_TYPE,
|
|
1598
|
-
sourceDocumentId: undraftId(sourceDocument._id)
|
|
1599
|
-
}
|
|
1600
|
-
) || await client.create({
|
|
1601
|
-
_type: TRANSLATION_METADATA_TYPE,
|
|
1602
|
-
translations: [
|
|
1603
|
-
{
|
|
1604
|
-
_key: sourceLanguage,
|
|
1605
|
-
_type: "internationalizedArrayReferenceValue",
|
|
1606
|
-
value: {
|
|
1607
|
-
_type: "reference",
|
|
1608
|
-
_ref: undraftId(sourceDocument._id),
|
|
1609
|
-
_weak: !0,
|
|
1610
|
-
_strengthenOnPublish: {
|
|
1611
|
-
_type: sourceDocument._type,
|
|
1612
|
-
_weak: !1
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
],
|
|
1617
|
-
translationHistory: []
|
|
1618
|
-
});
|
|
1619
|
-
} catch (error) {
|
|
1620
|
-
throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
|
|
1621
|
-
}
|
|
1622
783
|
}, updateTranslationMetadata = async ({
|
|
1623
784
|
client,
|
|
1624
785
|
targetLanguage,
|
|
@@ -1665,15 +826,156 @@ const fetchTranslationPrompts = async ({
|
|
|
1665
826
|
component: "updateTranslationMetadata"
|
|
1666
827
|
}), error;
|
|
1667
828
|
}
|
|
1668
|
-
}, 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
|
+
});
|
|
1669
835
|
dispatch({
|
|
1670
836
|
type: "SET_TRANSLATION_JOB_STATUS",
|
|
1671
|
-
payload:
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
837
|
+
payload: job
|
|
838
|
+
}), sourceDocumentId && updateActiveTranslationJob(sourceDocumentId, job);
|
|
839
|
+
};
|
|
840
|
+
function shouldExcludeField(fieldPath, excludeFields) {
|
|
841
|
+
for (const pattern of excludeFields) {
|
|
842
|
+
if (fieldPath === pattern)
|
|
843
|
+
return !0;
|
|
844
|
+
if (pattern.startsWith("**.")) {
|
|
845
|
+
const fieldName = pattern.substring(3), segments = fieldPath.split(".");
|
|
846
|
+
if (segments[segments.length - 1]?.split("[")[0] === fieldName)
|
|
847
|
+
return !0;
|
|
848
|
+
} else if (!pattern.includes(".") && !pattern.includes("[")) {
|
|
849
|
+
const segments = fieldPath.split(".");
|
|
850
|
+
if (segments[segments.length - 1]?.split("[")[0] === pattern)
|
|
851
|
+
return !0;
|
|
852
|
+
} else if (pattern.includes("[*]")) {
|
|
853
|
+
const regexPattern = pattern.replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/\\\[\*\\\]/g, "\\[\\d+\\]");
|
|
854
|
+
if (new RegExp(`^${regexPattern}$`).test(fieldPath))
|
|
855
|
+
return !0;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return !1;
|
|
859
|
+
}
|
|
860
|
+
function filterExcludedFields(document, excludeFields = [], currentPath = "") {
|
|
861
|
+
if (!excludeFields.length)
|
|
862
|
+
return {
|
|
863
|
+
filteredDocument: document,
|
|
864
|
+
excludedValues: {}
|
|
865
|
+
};
|
|
866
|
+
const filteredDocument = {}, excludedValues = {};
|
|
867
|
+
let hasExclusions = !1;
|
|
868
|
+
for (const [key, value] of Object.entries(document)) {
|
|
869
|
+
const fieldPath = currentPath ? `${currentPath}.${key}` : key;
|
|
870
|
+
if (shouldExcludeField(fieldPath, excludeFields)) {
|
|
871
|
+
excludedValues[fieldPath] = value, hasExclusions = !0, devLog.debug("Excluding field from translation", {
|
|
872
|
+
fieldPath,
|
|
873
|
+
fieldType: typeof value,
|
|
874
|
+
value: typeof value == "string" ? value : "[object]"
|
|
875
|
+
});
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (value && typeof value == "object" && !Array.isArray(value) && value._type !== "reference") {
|
|
879
|
+
const nested = filterExcludedFields(value, excludeFields, fieldPath);
|
|
880
|
+
filteredDocument[key] = nested.filteredDocument, Object.assign(excludedValues, nested.excludedValues), Object.keys(nested.excludedValues).length > 0 && (hasExclusions = !0);
|
|
881
|
+
} else if (Array.isArray(value)) {
|
|
882
|
+
const filteredArray = [];
|
|
883
|
+
let arrayHasExclusions = !1;
|
|
884
|
+
for (let i = 0; i < value.length; i++) {
|
|
885
|
+
const arrayItem = value[i], itemPath = `${fieldPath}[${i}]`;
|
|
886
|
+
if (arrayItem && typeof arrayItem == "object" && !Array.isArray(arrayItem) && arrayItem._type !== "reference") {
|
|
887
|
+
const nested = filterExcludedFields(arrayItem, excludeFields, itemPath);
|
|
888
|
+
filteredArray[i] = nested.filteredDocument, Object.assign(excludedValues, nested.excludedValues), Object.keys(nested.excludedValues).length > 0 && (arrayHasExclusions = !0);
|
|
889
|
+
} else
|
|
890
|
+
filteredArray[i] = arrayItem;
|
|
891
|
+
}
|
|
892
|
+
filteredDocument[key] = filteredArray, arrayHasExclusions && (hasExclusions = !0);
|
|
893
|
+
} else
|
|
894
|
+
filteredDocument[key] = value;
|
|
895
|
+
}
|
|
896
|
+
return hasExclusions && devLog.info("Filtered excluded fields from document", {
|
|
897
|
+
documentId: document._id || "unknown",
|
|
898
|
+
excludedFieldCount: Object.keys(excludedValues).length,
|
|
899
|
+
excludedFields: Object.keys(excludedValues)
|
|
900
|
+
}), {
|
|
901
|
+
filteredDocument,
|
|
902
|
+
excludedValues
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function restoreExcludedFields(translatedDocument, excludedValues) {
|
|
906
|
+
if (!Object.keys(excludedValues).length)
|
|
907
|
+
return translatedDocument;
|
|
908
|
+
const result = { ...translatedDocument };
|
|
909
|
+
for (const [fieldPath, value] of Object.entries(excludedValues))
|
|
910
|
+
setNestedValue(result, fieldPath, value);
|
|
911
|
+
return devLog.debug("Restored excluded fields to translated document", {
|
|
912
|
+
documentId: result._id || "unknown",
|
|
913
|
+
restoredFieldCount: Object.keys(excludedValues).length,
|
|
914
|
+
restoredFields: Object.keys(excludedValues)
|
|
915
|
+
}), result;
|
|
916
|
+
}
|
|
917
|
+
function setNestedValue(obj, path, value) {
|
|
918
|
+
const segments = path.split(".");
|
|
919
|
+
let current = obj;
|
|
920
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
921
|
+
const segment = segments[i], arrayMatch2 = segment.match(/^(.+)\[(\d+)\]$/);
|
|
922
|
+
if (arrayMatch2) {
|
|
923
|
+
const [, fieldName, indexStr] = arrayMatch2, index = Number.parseInt(indexStr, 10);
|
|
924
|
+
for ((!(fieldName in current) || !Array.isArray(current[fieldName])) && (current[fieldName] = []); current[fieldName].length <= index; )
|
|
925
|
+
current[fieldName].push({});
|
|
926
|
+
current = current[fieldName][index];
|
|
927
|
+
} else
|
|
928
|
+
(!(segment in current) || typeof current[segment] != "object") && (current[segment] = {}), current = current[segment];
|
|
929
|
+
}
|
|
930
|
+
const finalSegment = segments[segments.length - 1], arrayMatch = finalSegment.match(/^(.+)\[(\d+)\]$/);
|
|
931
|
+
if (arrayMatch) {
|
|
932
|
+
const [, fieldName, indexStr] = arrayMatch, index = Number.parseInt(indexStr, 10);
|
|
933
|
+
for ((!(fieldName in current) || !Array.isArray(current[fieldName])) && (current[fieldName] = []); current[fieldName].length <= index; )
|
|
934
|
+
current[fieldName].push(null);
|
|
935
|
+
current[fieldName][index] = value;
|
|
936
|
+
} else
|
|
937
|
+
current[finalSegment] = value;
|
|
938
|
+
}
|
|
939
|
+
const COMMON_ENUM_FIELDS = [
|
|
940
|
+
"status",
|
|
941
|
+
"state",
|
|
942
|
+
"type",
|
|
943
|
+
"category",
|
|
944
|
+
"priority",
|
|
945
|
+
"visibility",
|
|
946
|
+
"role",
|
|
947
|
+
"permission",
|
|
948
|
+
"level",
|
|
949
|
+
"mode",
|
|
950
|
+
"format",
|
|
951
|
+
"variant",
|
|
952
|
+
"size",
|
|
953
|
+
"theme",
|
|
954
|
+
"kind",
|
|
955
|
+
"phase",
|
|
956
|
+
"stage"
|
|
957
|
+
], PREDEFINED_EXCLUSIONS = {
|
|
958
|
+
/** Common enum and status fields */
|
|
959
|
+
enums: [...COMMON_ENUM_FIELDS],
|
|
960
|
+
/** All metadata and system fields (starting with _) */
|
|
961
|
+
metadata: ["_id", "_type", "_rev", "_createdAt", "_updatedAt"],
|
|
962
|
+
/** Common technical and configuration fields */
|
|
963
|
+
technical: [
|
|
964
|
+
"slug",
|
|
965
|
+
"handle",
|
|
966
|
+
"key",
|
|
967
|
+
"code",
|
|
968
|
+
"id",
|
|
969
|
+
"uuid",
|
|
970
|
+
"token",
|
|
971
|
+
"hash",
|
|
972
|
+
"checksum",
|
|
973
|
+
"version",
|
|
974
|
+
"config",
|
|
975
|
+
"settings"
|
|
976
|
+
],
|
|
977
|
+
/** SEO and URL-related fields that often shouldn't be translated */
|
|
978
|
+
seo: ["slug", "handle", "permalink", "canonical", "redirect", "robots", "sitemap"]
|
|
1677
979
|
};
|
|
1678
980
|
function applyTransforms({
|
|
1679
981
|
document,
|
|
@@ -1837,9 +1139,10 @@ class TransformRegistry {
|
|
|
1837
1139
|
}), matches[0];
|
|
1838
1140
|
}
|
|
1839
1141
|
}
|
|
1840
|
-
const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKey, request) => (request.model || "gpt-5")
|
|
1142
|
+
const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKey, request) => (request.model || "gpt-5").startsWith("gpt-5") ? await callGPT5ResponsesAPI(apiKey, request) : await callChatCompletionsAPI(apiKey, request), callGPT5ResponsesAPI = async (apiKey, request) => {
|
|
1841
1143
|
const model = request.model || "gpt-5", requestBody = {
|
|
1842
|
-
model: "gpt-5.1",
|
|
1144
|
+
model: model === "gpt-5" ? "gpt-5.1" : model,
|
|
1145
|
+
// Map gpt-5 to gpt-5.1, use others as-is
|
|
1843
1146
|
input: request.messages
|
|
1844
1147
|
// Uses 'input' instead of 'messages'
|
|
1845
1148
|
};
|
|
@@ -1847,6 +1150,16 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1847
1150
|
effort: request.reasoningEffort,
|
|
1848
1151
|
summary: "auto"
|
|
1849
1152
|
// Enable automatic reasoning summaries
|
|
1153
|
+
}), devLog.debug("OpenAI GPT-5 Request", {
|
|
1154
|
+
model: requestBody.model,
|
|
1155
|
+
reasoningEffort: request.reasoningEffort,
|
|
1156
|
+
messages: request.messages.map((m) => ({
|
|
1157
|
+
role: m.role,
|
|
1158
|
+
contentLength: m.content.length,
|
|
1159
|
+
contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
|
|
1160
|
+
}))
|
|
1161
|
+
}), devLog.debug("OpenAI GPT-5 Full Prompt", {
|
|
1162
|
+
messages: request.messages
|
|
1850
1163
|
});
|
|
1851
1164
|
const response = await fetch("https://api.openai.com/v1/responses", {
|
|
1852
1165
|
method: "POST",
|
|
@@ -1864,7 +1177,8 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1864
1177
|
prompt: result.usage?.input_tokens || 0,
|
|
1865
1178
|
completion: result.usage?.output_tokens || 0,
|
|
1866
1179
|
reasoning: result.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
1867
|
-
total: result.usage?.total_tokens || 0
|
|
1180
|
+
total: result.usage?.total_tokens || 0,
|
|
1181
|
+
cached: result.usage?.input_tokens_details?.cached_tokens || 0
|
|
1868
1182
|
}, cost = calculateCost(tokensUsed, model);
|
|
1869
1183
|
let content = "";
|
|
1870
1184
|
if (result.output && Array.isArray(result.output)) {
|
|
@@ -1881,7 +1195,19 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1881
1195
|
model,
|
|
1882
1196
|
messages: request.messages,
|
|
1883
1197
|
temperature: request.temperature ?? 0.3
|
|
1884
|
-
}
|
|
1198
|
+
};
|
|
1199
|
+
devLog.debug("OpenAI Chat Completions Request", {
|
|
1200
|
+
model,
|
|
1201
|
+
temperature: request.temperature ?? 0.3,
|
|
1202
|
+
messages: request.messages.map((m) => ({
|
|
1203
|
+
role: m.role,
|
|
1204
|
+
contentLength: m.content.length,
|
|
1205
|
+
contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
|
|
1206
|
+
}))
|
|
1207
|
+
}), devLog.debug("OpenAI Chat Completions Full Prompt", {
|
|
1208
|
+
messages: request.messages
|
|
1209
|
+
});
|
|
1210
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1885
1211
|
method: "POST",
|
|
1886
1212
|
headers: {
|
|
1887
1213
|
"Content-Type": "application/json",
|
|
@@ -1897,7 +1223,8 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1897
1223
|
prompt: usage.prompt_tokens || 0,
|
|
1898
1224
|
completion: usage.completion_tokens || 0,
|
|
1899
1225
|
reasoning: usage.reasoning_tokens || 0,
|
|
1900
|
-
total: usage.total_tokens || 0
|
|
1226
|
+
total: usage.total_tokens || 0,
|
|
1227
|
+
cached: usage.prompt_tokens_details?.cached_tokens || 0
|
|
1901
1228
|
}, cost = calculateCost(tokensUsed, model);
|
|
1902
1229
|
return {
|
|
1903
1230
|
content: result.choices[0].message.content || "",
|
|
@@ -1907,27 +1234,47 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1907
1234
|
}, calculateCost = (tokens, model) => {
|
|
1908
1235
|
const pricing = {
|
|
1909
1236
|
"gpt-5": {
|
|
1910
|
-
input:
|
|
1237
|
+
input: 125e-8,
|
|
1238
|
+
output: 1e-5,
|
|
1239
|
+
reasoning: 0
|
|
1240
|
+
},
|
|
1241
|
+
"gpt-5.1": {
|
|
1242
|
+
input: 125e-8,
|
|
1911
1243
|
output: 1e-5,
|
|
1912
|
-
reasoning:
|
|
1244
|
+
reasoning: 0
|
|
1245
|
+
},
|
|
1246
|
+
"gpt-5-mini": {
|
|
1247
|
+
input: 25e-8,
|
|
1248
|
+
output: 2e-6,
|
|
1249
|
+
reasoning: 0
|
|
1250
|
+
},
|
|
1251
|
+
"gpt-5-nano": {
|
|
1252
|
+
input: 50000000000000004e-24,
|
|
1253
|
+
output: 40000000000000003e-23,
|
|
1254
|
+
reasoning: 0
|
|
1255
|
+
},
|
|
1256
|
+
"gpt-5-pro": {
|
|
1257
|
+
input: 15e-6,
|
|
1258
|
+
output: 12e-5,
|
|
1259
|
+
reasoning: 0
|
|
1913
1260
|
},
|
|
1914
1261
|
"gpt-4o": {
|
|
1915
|
-
input:
|
|
1916
|
-
output:
|
|
1262
|
+
input: 25e-7,
|
|
1263
|
+
output: 1e-5,
|
|
1917
1264
|
reasoning: 0
|
|
1918
1265
|
},
|
|
1919
1266
|
"gpt-4-turbo": {
|
|
1920
1267
|
input: 1e-5,
|
|
1921
|
-
output:
|
|
1268
|
+
output: 3e-5,
|
|
1922
1269
|
reasoning: 0
|
|
1923
1270
|
},
|
|
1924
1271
|
"gpt-4": {
|
|
1925
|
-
input:
|
|
1926
|
-
output:
|
|
1272
|
+
input: 3e-5,
|
|
1273
|
+
output: 6e-5,
|
|
1927
1274
|
reasoning: 0
|
|
1928
1275
|
}
|
|
1929
|
-
}, modelPricing = pricing[model] || pricing["gpt-5"];
|
|
1930
|
-
return tokens.prompt * modelPricing.input + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
|
|
1276
|
+
}, modelPricing = pricing[model] || pricing["gpt-5"], cachedInputRate = model === "gpt-5.1" ? 0.125 / 1e6 : modelPricing.input;
|
|
1277
|
+
return Math.max(0, tokens.prompt - tokens.cached) * modelPricing.input + tokens.cached * cachedInputRate + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
|
|
1931
1278
|
}, deserializeDocument = (originalDoc, reviewedText) => {
|
|
1932
1279
|
const parsed = parseReviewFormat(reviewedText), result = cloneDeep(originalDoc);
|
|
1933
1280
|
return applyTextChanges(result, parsed), result;
|
|
@@ -1943,7 +1290,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1943
1290
|
type: "field",
|
|
1944
1291
|
name: fieldName,
|
|
1945
1292
|
text: unescapeXml(text2),
|
|
1946
|
-
attributes: {},
|
|
1293
|
+
attributes: { type: "string" },
|
|
1947
1294
|
children: []
|
|
1948
1295
|
};
|
|
1949
1296
|
if (stack.length > 0) {
|
|
@@ -2016,6 +1363,26 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2016
1363
|
parent && parent.children.push(node);
|
|
2017
1364
|
} else
|
|
2018
1365
|
root.push(node);
|
|
1366
|
+
} else if (attributes.type === "string" && content && content.includes(closingTag))
|
|
1367
|
+
if (node.text = unescapeXml(content.replace(closingTag, "")), stack.length > 0) {
|
|
1368
|
+
const parent = stack[stack.length - 1];
|
|
1369
|
+
parent && parent.children.push(node);
|
|
1370
|
+
} else
|
|
1371
|
+
root.push(node);
|
|
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);
|
|
2019
1386
|
} else
|
|
2020
1387
|
stack.push(node);
|
|
2021
1388
|
continue;
|
|
@@ -2072,7 +1439,10 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2072
1439
|
});
|
|
2073
1440
|
return;
|
|
2074
1441
|
}
|
|
2075
|
-
node.text !== void 0 && !node.attributes.readonly && set(doc, path, node.text)
|
|
1442
|
+
node.text !== void 0 && !node.attributes.readonly && (get(doc, path) !== void 0 ? set(doc, path, node.text) : devLog.debug("Skipping field not present in original document", {
|
|
1443
|
+
path: path.join("."),
|
|
1444
|
+
attemptedValue: node.text
|
|
1445
|
+
}));
|
|
2076
1446
|
}, applyBlockChanges = (block, blockNode) => {
|
|
2077
1447
|
if (blockNode.attributes._key && block._key !== blockNode.attributes._key) {
|
|
2078
1448
|
devLog.warn("Block key mismatch, skipping", {
|
|
@@ -2103,7 +1473,10 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2103
1473
|
});
|
|
2104
1474
|
return;
|
|
2105
1475
|
}
|
|
2106
|
-
fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (block[fieldName] = fieldNode.text
|
|
1476
|
+
fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (fieldName in block ? block[fieldName] = fieldNode.text : devLog.debug("Skipping block field not present in original", {
|
|
1477
|
+
fieldName,
|
|
1478
|
+
attemptedValue: fieldNode.text
|
|
1479
|
+
}));
|
|
2107
1480
|
}, applySpanChanges = (span, spanNode) => {
|
|
2108
1481
|
if (spanNode.attributes._key && span._key !== spanNode.attributes._key) {
|
|
2109
1482
|
devLog.warn("Span key mismatch, skipping", {
|
|
@@ -2148,7 +1521,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2148
1521
|
return;
|
|
2149
1522
|
}
|
|
2150
1523
|
if (typeof value == "string") {
|
|
2151
|
-
value.length > 0 && lines.push(`${
|
|
1524
|
+
value.length > 0 && lines.push(`${indentStr}<field name="${fieldName}" type="string">${escapeXml(value)}</field>`);
|
|
2152
1525
|
return;
|
|
2153
1526
|
}
|
|
2154
1527
|
(typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
|
|
@@ -2185,7 +1558,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2185
1558
|
return;
|
|
2186
1559
|
}
|
|
2187
1560
|
if (typeof value == "string" && value.length > 0) {
|
|
2188
|
-
lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
|
|
1561
|
+
lines.push(`${indentStr}<field name="${fieldName}" type="string">${escapeXml(value)}</field>`);
|
|
2189
1562
|
return;
|
|
2190
1563
|
}
|
|
2191
1564
|
(typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
|
|
@@ -2327,7 +1700,19 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2327
1700
|
`Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
|
|
2328
1701
|
);
|
|
2329
1702
|
}
|
|
2330
|
-
|
|
1703
|
+
const translationValidation = validateTranslationCompleteness(sourceDocument, result, languageFieldName);
|
|
1704
|
+
return translationValidation.valid || (devLog.warn("Translation completeness warning", {
|
|
1705
|
+
...logContext,
|
|
1706
|
+
skippedFields: translationValidation.skippedFields,
|
|
1707
|
+
warning: "GPT-5 may have skipped some important fields"
|
|
1708
|
+
}), devLog.info("Skipped field details", {
|
|
1709
|
+
...logContext,
|
|
1710
|
+
skippedFields: translationValidation.skippedFields.map((field) => ({
|
|
1711
|
+
path: field.path,
|
|
1712
|
+
originalValue: field.originalValue,
|
|
1713
|
+
translatedValue: field.translatedValue
|
|
1714
|
+
}))
|
|
1715
|
+
})), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
|
|
2331
1716
|
tokensUsed: response.tokensUsed.total,
|
|
2332
1717
|
estimatedCost: response.cost
|
|
2333
1718
|
}), result;
|
|
@@ -2368,12 +1753,19 @@ ${sourceText}
|
|
|
2368
1753
|
|
|
2369
1754
|
## Instructions
|
|
2370
1755
|
|
|
2371
|
-
1.
|
|
2372
|
-
2.
|
|
2373
|
-
3.
|
|
2374
|
-
4.
|
|
2375
|
-
5.
|
|
2376
|
-
6.
|
|
1756
|
+
1. **TRANSLATE ALL TEXT FIELDS**: Every field marked with type="string" or containing text must be translated
|
|
1757
|
+
2. **DO NOT SKIP SIMPLE FIELDS**: Pay special attention to simple fields like "title", "name", "description" - these are often the most important
|
|
1758
|
+
3. Follow the guidelines and terminology requirements above
|
|
1759
|
+
4. Preserve protected phrases exactly as they appear
|
|
1760
|
+
5. Return the document in THE EXACT SAME FORMAT
|
|
1761
|
+
6. **CRITICAL**: Only modify text content between tags
|
|
1762
|
+
7. Never add, remove, or change any tags, attributes, or structure
|
|
1763
|
+
|
|
1764
|
+
**Common fields that must always be translated:**
|
|
1765
|
+
- title, name, heading, label
|
|
1766
|
+
- description, caption, alt text
|
|
1767
|
+
- All text within <field> tags
|
|
1768
|
+
- All text within <span> tags
|
|
2377
1769
|
|
|
2378
1770
|
If you used significant reasoning to make translation decisions, you may include a brief summary:
|
|
2379
1771
|
<reasoning>
|
|
@@ -2381,6 +1773,43 @@ Brief summary of key translation decisions and reasoning
|
|
|
2381
1773
|
</reasoning>
|
|
2382
1774
|
|
|
2383
1775
|
Then return the translated document:`;
|
|
1776
|
+
}, validateTranslationCompleteness = (sourceDoc, translatedDoc, languageFieldName) => {
|
|
1777
|
+
const skippedFields = [], checkField = (source, translated, path = []) => {
|
|
1778
|
+
if (path.length > 0) {
|
|
1779
|
+
const fieldName = path[path.length - 1];
|
|
1780
|
+
if (fieldName.startsWith("_") || fieldName === languageFieldName)
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (typeof source == "string" && typeof translated == "string") {
|
|
1784
|
+
if (source.length > 0 && source === translated) {
|
|
1785
|
+
const fieldName = path[path.length - 1] || "unknown";
|
|
1786
|
+
(["title", "name", "heading", "label", "description", "caption", "alt"].includes(
|
|
1787
|
+
fieldName
|
|
1788
|
+
) || source.length > 10) && skippedFields.push({
|
|
1789
|
+
path: path.join("."),
|
|
1790
|
+
originalValue: source,
|
|
1791
|
+
translatedValue: translated
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
if (source && translated && typeof source == "object" && typeof translated == "object") {
|
|
1797
|
+
if (Array.isArray(source) && Array.isArray(translated)) {
|
|
1798
|
+
const maxLength = Math.min(source.length, translated.length);
|
|
1799
|
+
for (let i = 0; i < maxLength; i++)
|
|
1800
|
+
checkField(source[i], translated[i], [...path, String(i)]);
|
|
1801
|
+
} else if (!Array.isArray(source) && !Array.isArray(translated))
|
|
1802
|
+
for (const key of Object.keys(source))
|
|
1803
|
+
!key.startsWith("_") && key !== languageFieldName && checkField(source[key], translated[key], [
|
|
1804
|
+
...path,
|
|
1805
|
+
key
|
|
1806
|
+
]);
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
return checkField(sourceDoc, translatedDoc), {
|
|
1810
|
+
valid: skippedFields.length === 0,
|
|
1811
|
+
skippedFields
|
|
1812
|
+
};
|
|
2384
1813
|
}, assembleLanguageSpecs = ({
|
|
2385
1814
|
sourceDocument,
|
|
2386
1815
|
languageFieldName,
|
|
@@ -2501,22 +1930,55 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2501
1930
|
translationPrompts,
|
|
2502
1931
|
translationMode,
|
|
2503
1932
|
openaiConfig,
|
|
2504
|
-
releaseId
|
|
1933
|
+
releaseId,
|
|
1934
|
+
excludeFields = [],
|
|
1935
|
+
replaceExisting = !1,
|
|
1936
|
+
mainSourceDocumentId
|
|
2505
1937
|
}) => {
|
|
2506
|
-
|
|
1938
|
+
const safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
|
|
1939
|
+
excludeFields.length !== safeExcludeFields.length && devLog.warn("Language field was in excludeFields - removing it to prevent translation errors", {
|
|
2507
1940
|
documentId: sourceDocument._id,
|
|
2508
|
-
|
|
1941
|
+
languageFieldName,
|
|
1942
|
+
originalExcludeFields: excludeFields,
|
|
1943
|
+
safeExcludeFields
|
|
1944
|
+
}), devLog.info("Filtering excluded fields before translation", {
|
|
1945
|
+
documentId: sourceDocument._id,
|
|
1946
|
+
documentType: sourceDocument._type,
|
|
1947
|
+
excludeFields: safeExcludeFields,
|
|
1948
|
+
excludeFieldsLength: safeExcludeFields?.length ?? 0,
|
|
1949
|
+
sourceHasTitle: "title" in sourceDocument,
|
|
1950
|
+
sourceTitleValue: sourceDocument.title
|
|
2509
1951
|
});
|
|
2510
|
-
const {
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
1952
|
+
const { filteredDocument, excludedValues } = filterExcludedFields(sourceDocument, safeExcludeFields);
|
|
1953
|
+
devLog.info("After filtering excluded fields", {
|
|
1954
|
+
documentId: sourceDocument._id,
|
|
1955
|
+
filteredHasTitle: "title" in filteredDocument,
|
|
1956
|
+
filteredTitleValue: filteredDocument.title,
|
|
1957
|
+
excludedValuesKeys: Object.keys(excludedValues),
|
|
1958
|
+
excludedValues
|
|
2514
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;
|
|
2515
1977
|
let translatedDoc;
|
|
2516
1978
|
switch (translationMode) {
|
|
2517
1979
|
case "sanity-only": {
|
|
2518
|
-
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
|
|
2519
|
-
sourceDocument:
|
|
1980
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating", void 0, mainSourceDocumentId), translatedDoc = await getSanityAgentTranslation({
|
|
1981
|
+
sourceDocument: documentToTranslate,
|
|
2520
1982
|
targetLanguage,
|
|
2521
1983
|
languageFieldName,
|
|
2522
1984
|
schemaId,
|
|
@@ -2528,9 +1990,16 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2528
1990
|
case "openai-only": {
|
|
2529
1991
|
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2530
1992
|
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2531
|
-
setJobStatus(
|
|
1993
|
+
setJobStatus(
|
|
1994
|
+
dispatch,
|
|
1995
|
+
sourceDocument._id,
|
|
1996
|
+
targetLanguage,
|
|
1997
|
+
"translating_with_openai",
|
|
1998
|
+
void 0,
|
|
1999
|
+
mainSourceDocumentId
|
|
2000
|
+
), translatedDoc = await getOpenAITranslation({
|
|
2532
2001
|
dispatch,
|
|
2533
|
-
sourceDocument:
|
|
2002
|
+
sourceDocument: documentToTranslate,
|
|
2534
2003
|
targetLanguage,
|
|
2535
2004
|
languageFieldName,
|
|
2536
2005
|
translationPrompts,
|
|
@@ -2538,27 +2007,45 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2538
2007
|
});
|
|
2539
2008
|
break;
|
|
2540
2009
|
}
|
|
2541
|
-
case "sanity-with-review":
|
|
2542
|
-
throw new Error("Sanity with review translation method is not implemented");
|
|
2543
2010
|
default:
|
|
2544
2011
|
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2545
2012
|
}
|
|
2546
2013
|
if (!translatedDoc)
|
|
2547
2014
|
return null;
|
|
2548
|
-
|
|
2015
|
+
let transformedResult;
|
|
2016
|
+
Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
|
|
2549
2017
|
documentId: sourceDocument._id,
|
|
2550
2018
|
documentType: sourceDocument._type
|
|
2551
|
-
})
|
|
2552
|
-
const finalDoc = restoreTransforms({
|
|
2019
|
+
}), transformedResult = restoreTransforms({
|
|
2553
2020
|
translatedDocument: translatedDoc,
|
|
2554
2021
|
originalStructure,
|
|
2555
2022
|
registry: globalTransformRegistry,
|
|
2556
2023
|
schemaType: sourceDocument._type
|
|
2557
|
-
})
|
|
2024
|
+
})) : transformedResult = translatedDoc;
|
|
2025
|
+
const excludedValuesToRestore = { ...excludedValues };
|
|
2026
|
+
delete excludedValuesToRestore[languageFieldName], devLog.info("Before restoring excluded fields", {
|
|
2027
|
+
documentId: sourceDocument._id,
|
|
2028
|
+
excludedFieldCount: Object.keys(excludedValues).length,
|
|
2029
|
+
excludedValuesKeys: Object.keys(excludedValues),
|
|
2030
|
+
languageFieldExcluded: languageFieldName in excludedValues,
|
|
2031
|
+
transformedResultHasTitle: "title" in transformedResult,
|
|
2032
|
+
transformedResultTitle: transformedResult.title
|
|
2033
|
+
});
|
|
2034
|
+
const finalDoc = restoreExcludedFields(transformedResult, excludedValuesToRestore);
|
|
2035
|
+
devLog.info("After restoring excluded fields", {
|
|
2036
|
+
documentId: sourceDocument._id,
|
|
2037
|
+
finalDocHasTitle: "title" in finalDoc,
|
|
2038
|
+
finalDocTitle: finalDoc.title
|
|
2039
|
+
});
|
|
2040
|
+
const targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
|
|
2558
2041
|
...finalDoc,
|
|
2559
2042
|
_id: targetDocumentVersionId
|
|
2560
2043
|
};
|
|
2561
|
-
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({
|
|
2562
2049
|
actionType: "sanity.action.document.version.create",
|
|
2563
2050
|
publishedId: targetDocumentPublishedId,
|
|
2564
2051
|
document: newDoc
|
|
@@ -2570,36 +2057,28 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2570
2057
|
sourceLanguage,
|
|
2571
2058
|
targetLanguage,
|
|
2572
2059
|
languageFieldName,
|
|
2573
|
-
shouldOverwriteFreshTranslations,
|
|
2574
2060
|
schemaId,
|
|
2575
2061
|
translationPrompts,
|
|
2576
2062
|
translationMode,
|
|
2577
2063
|
openaiConfig,
|
|
2578
|
-
releaseId
|
|
2064
|
+
releaseId,
|
|
2065
|
+
excludeFields,
|
|
2066
|
+
replaceExisting,
|
|
2067
|
+
mainSourceDocumentId
|
|
2579
2068
|
}) => {
|
|
2580
2069
|
const logContext = {
|
|
2581
2070
|
documentId: sourceDocument._id,
|
|
2582
2071
|
targetLanguage,
|
|
2583
2072
|
schemaId,
|
|
2584
|
-
shouldOverwriteFreshTranslations,
|
|
2585
2073
|
translationMode
|
|
2586
2074
|
};
|
|
2587
|
-
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);
|
|
2588
2076
|
try {
|
|
2589
2077
|
const translationMetadata = await getTranslationMetadata({
|
|
2590
2078
|
client,
|
|
2591
2079
|
sourceDocument,
|
|
2592
2080
|
sourceLanguage
|
|
2593
|
-
}),
|
|
2594
|
-
(history) => history.language === targetLanguage && history.sourceRev === sourceDocument._rev
|
|
2595
|
-
);
|
|
2596
|
-
if (!shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
|
|
2597
|
-
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
|
|
2598
|
-
targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
|
|
2599
|
-
});
|
|
2600
|
-
return;
|
|
2601
|
-
}
|
|
2602
|
-
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
|
|
2081
|
+
}), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
|
|
2603
2082
|
dispatch,
|
|
2604
2083
|
client,
|
|
2605
2084
|
schemaId,
|
|
@@ -2610,15 +2089,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2610
2089
|
translationPrompts,
|
|
2611
2090
|
translationMode,
|
|
2612
2091
|
openaiConfig,
|
|
2613
|
-
releaseId
|
|
2092
|
+
releaseId,
|
|
2093
|
+
excludeFields,
|
|
2094
|
+
replaceExisting,
|
|
2095
|
+
mainSourceDocumentId
|
|
2614
2096
|
});
|
|
2615
2097
|
if (!newDocument) {
|
|
2616
|
-
|
|
2098
|
+
devLog.error("Translation failed: no document returned", new Error("Translation returned null"), logContext), setJobStatus(
|
|
2099
|
+
dispatch,
|
|
2100
|
+
sourceDocument._id,
|
|
2101
|
+
targetLanguage,
|
|
2102
|
+
"error",
|
|
2103
|
+
{
|
|
2104
|
+
errorMessage: "Translation failed"
|
|
2105
|
+
},
|
|
2106
|
+
mainSourceDocumentId
|
|
2107
|
+
), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
2617
2108
|
return;
|
|
2618
2109
|
}
|
|
2619
|
-
return setJobStatus(
|
|
2620
|
-
|
|
2621
|
-
|
|
2110
|
+
return setJobStatus(
|
|
2111
|
+
dispatch,
|
|
2112
|
+
sourceDocument._id,
|
|
2113
|
+
targetLanguage,
|
|
2114
|
+
"updating_metadata",
|
|
2115
|
+
{
|
|
2116
|
+
targetId: newDocument._id
|
|
2117
|
+
},
|
|
2118
|
+
mainSourceDocumentId
|
|
2119
|
+
), await updateTranslationMetadata({
|
|
2622
2120
|
client,
|
|
2623
2121
|
sourceDocument,
|
|
2624
2122
|
sourceLanguage,
|
|
@@ -2626,17 +2124,31 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2626
2124
|
targetLanguage,
|
|
2627
2125
|
translationMetadata,
|
|
2628
2126
|
isNewTranslation: !existingTranslationId
|
|
2629
|
-
}), setJobStatus(
|
|
2630
|
-
|
|
2631
|
-
|
|
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", {
|
|
2632
2137
|
...logContext,
|
|
2633
2138
|
newDocumentId: newDocument._id
|
|
2634
2139
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
2635
2140
|
} catch (error) {
|
|
2636
2141
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2637
|
-
devLog.error("Translation failed", error instanceof Error ? error : new Error(String(error)), logContext), setJobStatus(
|
|
2638
|
-
|
|
2639
|
-
|
|
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();
|
|
2640
2152
|
return;
|
|
2641
2153
|
}
|
|
2642
2154
|
}, translationSemaphore = new Semaphore(2), translateDocumentSet = async ({
|
|
@@ -2645,12 +2157,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2645
2157
|
targetLanguages,
|
|
2646
2158
|
dispatch,
|
|
2647
2159
|
languageFieldName,
|
|
2648
|
-
shouldOverwriteFreshTranslations,
|
|
2649
2160
|
schemaId,
|
|
2650
2161
|
references = [],
|
|
2651
2162
|
maxConcurrentTranslationJobs,
|
|
2652
2163
|
translationMode,
|
|
2653
|
-
openaiConfig
|
|
2164
|
+
openaiConfig,
|
|
2165
|
+
excludeFields,
|
|
2166
|
+
releaseConfig
|
|
2654
2167
|
}) => {
|
|
2655
2168
|
const logContext = {
|
|
2656
2169
|
documentId: sourceDocument._id,
|
|
@@ -2659,28 +2172,36 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2659
2172
|
referencesCount: references.length,
|
|
2660
2173
|
maxConcurrentJobs: maxConcurrentTranslationJobs,
|
|
2661
2174
|
translationMode,
|
|
2662
|
-
|
|
2175
|
+
releaseMode: releaseConfig.mode
|
|
2663
2176
|
};
|
|
2664
|
-
let releaseId;
|
|
2177
|
+
let releaseId, createdNewRelease = !1;
|
|
2665
2178
|
try {
|
|
2666
2179
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
2667
2180
|
if (!sourceLanguage)
|
|
2668
2181
|
throw devLog.error("Source language not found"), new Error("Source language not found");
|
|
2669
|
-
maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
2182
|
+
if (maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
2670
2183
|
type: "SET_OVERALL_STATUS",
|
|
2671
2184
|
payload: "translating"
|
|
2672
|
-
})
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
}
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
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");
|
|
2684
2205
|
const sourceDocuments = [
|
|
2685
2206
|
{
|
|
2686
2207
|
_id: sourceDocument._id,
|
|
@@ -2695,7 +2216,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2695
2216
|
targetLanguages,
|
|
2696
2217
|
sourceDocument
|
|
2697
2218
|
});
|
|
2698
|
-
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
|
|
2219
|
+
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages, sourceDocument._id);
|
|
2699
2220
|
const translationTasks = sourceDocumentIds.flatMap(
|
|
2700
2221
|
(documentId) => targetLanguages.map(async (targetLanguage) => {
|
|
2701
2222
|
try {
|
|
@@ -2703,7 +2224,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2703
2224
|
const document = await client.withConfig({
|
|
2704
2225
|
perspective: "drafts"
|
|
2705
2226
|
}).getDocument(documentId);
|
|
2706
|
-
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document &&
|
|
2227
|
+
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId) {
|
|
2707
2228
|
const versionedTargetDocumentId = await translateDocument({
|
|
2708
2229
|
dispatch,
|
|
2709
2230
|
client,
|
|
@@ -2711,12 +2232,15 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2711
2232
|
sourceLanguage,
|
|
2712
2233
|
targetLanguage,
|
|
2713
2234
|
languageFieldName,
|
|
2714
|
-
shouldOverwriteFreshTranslations,
|
|
2715
2235
|
schemaId,
|
|
2716
2236
|
translationPrompts,
|
|
2717
2237
|
translationMode,
|
|
2718
2238
|
openaiConfig,
|
|
2719
|
-
releaseId
|
|
2239
|
+
releaseId,
|
|
2240
|
+
excludeFields,
|
|
2241
|
+
replaceExisting: releaseConfig.replaceExisting,
|
|
2242
|
+
// Pass main source document ID for active translations tracking
|
|
2243
|
+
mainSourceDocumentId: sourceDocument._id
|
|
2720
2244
|
});
|
|
2721
2245
|
versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
|
|
2722
2246
|
}
|
|
@@ -2743,7 +2267,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2743
2267
|
}), dispatch({
|
|
2744
2268
|
type: "SET_OVERALL_STATUS",
|
|
2745
2269
|
payload: "linking_references"
|
|
2746
|
-
}), await Promise.all(
|
|
2270
|
+
}), updateActiveTranslationStatus(sourceDocument._id, "linking_references"), await Promise.all(
|
|
2747
2271
|
versionedTargetDocumentIds.map(async (documentId) => {
|
|
2748
2272
|
await localizeReferences({
|
|
2749
2273
|
client,
|
|
@@ -2754,18 +2278,65 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2754
2278
|
})
|
|
2755
2279
|
);
|
|
2756
2280
|
const hasAnyFailures = failed > 0;
|
|
2757
|
-
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", {
|
|
2758
2282
|
...logContext,
|
|
2759
2283
|
succeeded,
|
|
2760
2284
|
failed
|
|
2761
|
-
}), 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();
|
|
2762
2286
|
} catch (error) {
|
|
2763
|
-
|
|
2287
|
+
if (devLog.error(
|
|
2764
2288
|
"translateDocumentSet failed with unexpected error",
|
|
2765
2289
|
error instanceof Error ? error : new Error(String(error)),
|
|
2766
2290
|
logContext
|
|
2767
|
-
),
|
|
2768
|
-
|
|
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
|
+
] }) });
|
|
2769
2340
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
2770
2341
|
const {
|
|
2771
2342
|
document,
|
|
@@ -2774,37 +2345,81 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2774
2345
|
dispatch,
|
|
2775
2346
|
references,
|
|
2776
2347
|
selectedTargetLanguages,
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }),
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
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,
|
|
2783
2381
|
client,
|
|
2784
2382
|
dispatch,
|
|
2785
|
-
|
|
2786
|
-
|
|
2383
|
+
config.languageFieldName,
|
|
2384
|
+
config.schemaId,
|
|
2385
|
+
config.maxConcurrentTranslationJobs,
|
|
2386
|
+
config.maxReferenceDepth,
|
|
2387
|
+
config.openai,
|
|
2388
|
+
config.excludeFields,
|
|
2787
2389
|
references,
|
|
2788
|
-
shouldOverwriteFreshTranslations,
|
|
2789
|
-
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
2790
|
-
maxReferenceDepth: config.maxReferenceDepth,
|
|
2791
2390
|
translationMode,
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
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(() => {
|
|
2808
2423
|
dispatch({
|
|
2809
2424
|
type: "SET_SELECTED_TARGET_LANGUAGES",
|
|
2810
2425
|
payload: config.supportedLanguages.map((lang) => lang.id).filter((id) => id !== document.language)
|
|
@@ -2815,7 +2430,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2815
2430
|
payload: []
|
|
2816
2431
|
});
|
|
2817
2432
|
}, [dispatch]);
|
|
2818
|
-
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: [
|
|
2819
2441
|
/* @__PURE__ */ jsxs(Flex, { justify: "flex-start", gap: 2, children: [
|
|
2820
2442
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectAll, children: "Select All" }),
|
|
2821
2443
|
/* @__PURE__ */ jsx(Button, { mode: "ghost", size: 2, onClick: handleSelectNone, children: "Select None" })
|
|
@@ -2829,16 +2451,41 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2829
2451
|
tone: "positive",
|
|
2830
2452
|
size: 2,
|
|
2831
2453
|
onClick: handleTranslate,
|
|
2832
|
-
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus),
|
|
2833
|
-
children: "Translate"
|
|
2454
|
+
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus) || releaseConfig.mode === "existing" && !releaseConfig.existingReleaseId || isCheckingConflicts,
|
|
2455
|
+
children: isCheckingConflicts ? "Checking..." : "Translate"
|
|
2834
2456
|
}
|
|
2835
2457
|
)
|
|
2836
2458
|
] })
|
|
2837
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
|
+
};
|
|
2838
2487
|
}, LanguageCheckbox = ({ lang, isLoading }) => {
|
|
2839
|
-
const { document, dispatch, selectedTargetLanguages
|
|
2840
|
-
(history) => history.language === lang.id && history.sourceRev !== document._rev
|
|
2841
|
-
), handleChange = () => {
|
|
2488
|
+
const { document, dispatch, selectedTargetLanguages } = useTranslationContext(), isDisabled = lang.id === document.language, isChecked = selectedTargetLanguages.includes(lang.id), handleChange = () => {
|
|
2842
2489
|
if (isDisabled) return;
|
|
2843
2490
|
const updatedLanguages = isChecked ? selectedTargetLanguages.filter((langId) => langId !== lang.id) : [...selectedTargetLanguages, lang.id];
|
|
2844
2491
|
dispatch({
|
|
@@ -2856,16 +2503,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2856
2503
|
disabled: isDisabled,
|
|
2857
2504
|
tone: isChecked ? "primary" : "default",
|
|
2858
2505
|
muted: !0,
|
|
2859
|
-
children: /* @__PURE__ */
|
|
2860
|
-
/* @__PURE__ */
|
|
2861
|
-
|
|
2862
|
-
/* @__PURE__ */
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
] }),
|
|
2867
|
-
!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" }) })
|
|
2868
|
-
] })
|
|
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
|
+
] }) })
|
|
2869
2513
|
}
|
|
2870
2514
|
);
|
|
2871
2515
|
}, LanguageSelectionPanel = () => {
|
|
@@ -2873,6 +2517,134 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2873
2517
|
documentId: document._id
|
|
2874
2518
|
});
|
|
2875
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
|
+
] }) });
|
|
2876
2648
|
}, TranslationModePanel = () => {
|
|
2877
2649
|
const { config, translationMode, openaiValidated, dispatch } = useTranslationContext();
|
|
2878
2650
|
if (!config?.openai?.enabled || !config?.openai?.apiKey)
|
|
@@ -2891,7 +2663,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2891
2663
|
return /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
2892
2664
|
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
2893
2665
|
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "Translation Mode" }),
|
|
2894
|
-
/* @__PURE__ */ jsx(Badge, { tone: "positive", children: "
|
|
2666
|
+
/* @__PURE__ */ jsx(Badge, { tone: "positive", children: "OpenAI Available" })
|
|
2895
2667
|
] }),
|
|
2896
2668
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
2897
2669
|
/* @__PURE__ */ jsx(
|
|
@@ -2921,32 +2693,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2921
2693
|
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "openai-only", readOnly: !0 }),
|
|
2922
2694
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
2923
2695
|
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
2924
|
-
/* @__PURE__ */ jsx(Text, { children: "
|
|
2696
|
+
/* @__PURE__ */ jsx(Text, { children: "OpenAI Only" }),
|
|
2925
2697
|
/* @__PURE__ */ jsx(Badge, { tone: "caution", children: "Beta" })
|
|
2926
2698
|
] }),
|
|
2927
2699
|
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Full style guide capacity with 1M token context window." })
|
|
2928
2700
|
] })
|
|
2929
2701
|
] })
|
|
2930
2702
|
}
|
|
2931
|
-
),
|
|
2932
|
-
/* @__PURE__ */ jsx(
|
|
2933
|
-
Button,
|
|
2934
|
-
{
|
|
2935
|
-
type: "button",
|
|
2936
|
-
mode: translationMode === "sanity-with-review" ? "ghost" : "bleed",
|
|
2937
|
-
tone: translationMode === "sanity-with-review" ? "primary" : "default",
|
|
2938
|
-
onClick: () => handleModeChange("sanity-with-review"),
|
|
2939
|
-
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
2940
|
-
/* @__PURE__ */ jsx(Radio, { checked: translationMode === "sanity-with-review", readOnly: !0 }),
|
|
2941
|
-
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
2942
|
-
/* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
2943
|
-
/* @__PURE__ */ jsx(Text, { children: "Sanity + GPT-5 Review" }),
|
|
2944
|
-
/* @__PURE__ */ jsx(Badge, { tone: "primary", children: "Highest Quality" })
|
|
2945
|
-
] }),
|
|
2946
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Sanity translation refined by GPT-5. Takes longer but best results." })
|
|
2947
|
-
] })
|
|
2948
|
-
] })
|
|
2949
|
-
}
|
|
2950
2703
|
)
|
|
2951
2704
|
] }),
|
|
2952
2705
|
translationMode !== "sanity-only" && /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 3, border: !0, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
@@ -3109,41 +2862,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3109
2862
|
] }),
|
|
3110
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
|
|
3111
2864
|
] });
|
|
3112
|
-
}, WriteSettingsPanel = () => {
|
|
3113
|
-
const { dispatch, shouldOverwriteFreshTranslations } = useTranslationContext(), handleShouldOverwriteFreshTranslationsChange = () => {
|
|
3114
|
-
dispatch({
|
|
3115
|
-
type: "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS",
|
|
3116
|
-
payload: !shouldOverwriteFreshTranslations
|
|
3117
|
-
});
|
|
3118
|
-
};
|
|
3119
|
-
return /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3120
|
-
/* @__PURE__ */ jsx(ReferenceConfigPanel, {}),
|
|
3121
|
-
/* @__PURE__ */ jsx(Flex, { justify: "space-between", align: "center", children: /* @__PURE__ */ jsx(
|
|
3122
|
-
Button,
|
|
3123
|
-
{
|
|
3124
|
-
type: "button",
|
|
3125
|
-
mode: shouldOverwriteFreshTranslations ? "ghost" : "bleed",
|
|
3126
|
-
size: 2,
|
|
3127
|
-
onClick: handleShouldOverwriteFreshTranslationsChange,
|
|
3128
|
-
tone: shouldOverwriteFreshTranslations ? "primary" : "neutral",
|
|
3129
|
-
children: /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
|
|
3130
|
-
/* @__PURE__ */ jsx(
|
|
3131
|
-
Checkbox,
|
|
3132
|
-
{
|
|
3133
|
-
checked: shouldOverwriteFreshTranslations,
|
|
3134
|
-
onChange: handleShouldOverwriteFreshTranslationsChange,
|
|
3135
|
-
label: shouldOverwriteFreshTranslations ? "Disable" : "Enable"
|
|
3136
|
-
}
|
|
3137
|
-
),
|
|
3138
|
-
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3139
|
-
/* @__PURE__ */ jsx(Text, { children: "Overwrite Fresh Translations" }),
|
|
3140
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "All existing translations will be overwritten even if they are fresh." })
|
|
3141
|
-
] })
|
|
3142
|
-
] })
|
|
3143
|
-
}
|
|
3144
|
-
) })
|
|
3145
|
-
] });
|
|
3146
|
-
}, TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
2865
|
+
}, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
3147
2866
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3148
2867
|
/* @__PURE__ */ jsx(Label, { children: "Languages" }),
|
|
3149
2868
|
/* @__PURE__ */ jsx(LanguageSelectionPanel, {})
|
|
@@ -3152,16 +2871,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3152
2871
|
/* @__PURE__ */ jsx(Label, { children: "Translation Method" }),
|
|
3153
2872
|
/* @__PURE__ */ jsx(TranslationModePanel, {})
|
|
3154
2873
|
] }),
|
|
2874
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
2875
|
+
/* @__PURE__ */ jsx(Label, { children: "Content Release" }),
|
|
2876
|
+
/* @__PURE__ */ jsx(ReleaseSelectionPanel, {})
|
|
2877
|
+
] }),
|
|
3155
2878
|
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3156
2879
|
/* @__PURE__ */ jsx(Label, { children: "Settings" }),
|
|
3157
2880
|
/* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsx(WriteSettingsPanel, {}) })
|
|
3158
2881
|
] }),
|
|
3159
2882
|
/* @__PURE__ */ jsx(ActionButtonBar, { setIsDialogOpen })
|
|
3160
2883
|
] }), TERMINAL_STATUSES = ["completed", "skipped", "error"], TranslationStatusPanel = () => {
|
|
3161
|
-
const { overallStatus, translationJobs, translationMode } = useTranslationContext(), headline = useMemo(
|
|
3162
|
-
()
|
|
3163
|
-
|
|
3164
|
-
), 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(() => {
|
|
3165
2887
|
if (translationMode === "sanity-only") return null;
|
|
3166
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);
|
|
3167
2889
|
return { totalTokens, totalCost, jobsWithUsage: completedJobs.filter((j) => j.tokensUsed).length };
|
|
@@ -3183,7 +2905,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3183
2905
|
/* @__PURE__ */ jsxs(Code, { size: 1, children: [
|
|
3184
2906
|
job._type,
|
|
3185
2907
|
" \u2014 ",
|
|
3186
|
-
job._id
|
|
2908
|
+
job._id?.slice(-6) || "unknown"
|
|
3187
2909
|
] }),
|
|
3188
2910
|
!["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
|
|
3189
2911
|
job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
@@ -3217,18 +2939,19 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3217
2939
|
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Usage Summary" }),
|
|
3218
2940
|
/* @__PURE__ */ jsxs(Inline, { space: 4, children: [
|
|
3219
2941
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3220
|
-
"
|
|
2942
|
+
"Total Tokens: ",
|
|
3221
2943
|
/* @__PURE__ */ jsx("strong", { children: openaiStats.totalTokens.toLocaleString() })
|
|
3222
2944
|
] }),
|
|
3223
2945
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3224
|
-
"
|
|
2946
|
+
"Total Cost: ",
|
|
3225
2947
|
/* @__PURE__ */ jsxs("strong", { children: [
|
|
3226
2948
|
"$",
|
|
3227
2949
|
openaiStats.totalCost.toFixed(4)
|
|
3228
2950
|
] })
|
|
3229
2951
|
] })
|
|
3230
2952
|
] })
|
|
3231
|
-
] }) })
|
|
2953
|
+
] }) }),
|
|
2954
|
+
isTerminalState && /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "primary", text: "Translate More", icon: ResetIcon, onClick: handleReset }) })
|
|
3232
2955
|
] });
|
|
3233
2956
|
}, ProgressBar = ({ progress, max = 100 }) => {
|
|
3234
2957
|
const percentage = Math.min(progress / max * 100, 100);
|
|
@@ -3292,8 +3015,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3292
3015
|
queued: "Queued...",
|
|
3293
3016
|
preparing_translation: "Preparing translation...",
|
|
3294
3017
|
translating: "Translating with Sanity Agent...",
|
|
3295
|
-
translating_with_openai: "Translating with
|
|
3296
|
-
reviewing_with_openai: "Reviewing translation with GPT-5...",
|
|
3018
|
+
translating_with_openai: "Translating with OpenAI...",
|
|
3297
3019
|
updating_metadata: "Saving metadata...",
|
|
3298
3020
|
error: "Error",
|
|
3299
3021
|
skipped: "Skipped",
|
|
@@ -3302,14 +3024,30 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3302
3024
|
"translating",
|
|
3303
3025
|
"linking_references",
|
|
3304
3026
|
"completed"
|
|
3305
|
-
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
|
|
3027
|
+
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen, onStatusChange }) => {
|
|
3306
3028
|
const { overallStatus, document } = useTranslationContext();
|
|
3307
|
-
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", {
|
|
3308
3032
|
documentId: document._id,
|
|
3309
3033
|
overallStatus
|
|
3310
3034
|
}), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
|
|
3311
|
-
}, getTranslateAction = (config) => ({ draft, published }) => {
|
|
3312
|
-
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]);
|
|
3313
3051
|
return devLog.debug("TranslateAction initialized", {
|
|
3314
3052
|
documentId: document?._id,
|
|
3315
3053
|
documentType: document?._type
|
|
@@ -3317,7 +3055,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3317
3055
|
documentType: document?._type,
|
|
3318
3056
|
translatableTypes: config.translatableSchemaTypes
|
|
3319
3057
|
}), null) : config ? {
|
|
3320
|
-
label:
|
|
3058
|
+
label: getButtonLabel(currentStatus),
|
|
3321
3059
|
icon: TranslateIcon,
|
|
3322
3060
|
onHandle: () => {
|
|
3323
3061
|
devLog.info("Translation dialog opened", {
|
|
@@ -3328,9 +3066,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3328
3066
|
},
|
|
3329
3067
|
dialog: isDialogOpen && {
|
|
3330
3068
|
type: "dialog",
|
|
3331
|
-
onClose:
|
|
3069
|
+
onClose: handleClose,
|
|
3332
3070
|
header: "Translate Document",
|
|
3333
|
-
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 }) })
|
|
3334
3072
|
}
|
|
3335
3073
|
} : (devLog.error("Missing config in TranslateAction"), null);
|
|
3336
3074
|
}, translationGlossaryTerm = defineType({
|
|
@@ -3446,7 +3184,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3446
3184
|
defineField({
|
|
3447
3185
|
name: "changeHistory",
|
|
3448
3186
|
title: "Change History",
|
|
3449
|
-
description: "History of changes made
|
|
3187
|
+
description: "History of changes made to translation settings.",
|
|
3450
3188
|
type: "array",
|
|
3451
3189
|
readOnly: !0,
|
|
3452
3190
|
group: "history",
|
|
@@ -3756,14 +3494,18 @@ const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.le
|
|
|
3756
3494
|
})), {
|
|
3757
3495
|
name: "sanity-plugin-ai-translation",
|
|
3758
3496
|
document: {
|
|
3759
|
-
actions: [getTranslateAction(config)
|
|
3497
|
+
actions: [getTranslateAction(config)]
|
|
3760
3498
|
},
|
|
3761
3499
|
schema: {
|
|
3762
3500
|
types: [translationSettings, translationGlossaryTerm]
|
|
3763
3501
|
}
|
|
3764
3502
|
})), translationHistorySchema = translationHistory;
|
|
3765
3503
|
export {
|
|
3504
|
+
COMMON_ENUM_FIELDS,
|
|
3505
|
+
PREDEFINED_EXCLUSIONS,
|
|
3766
3506
|
aiTranslationPlugin,
|
|
3507
|
+
filterExcludedFields,
|
|
3508
|
+
restoreExcludedFields,
|
|
3767
3509
|
richTextTransform,
|
|
3768
3510
|
translationHistorySchema,
|
|
3769
3511
|
vttCaptionTransform
|