@braze/sanity-plugin-ai-translation 0.2.2 → 0.2.4
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/dist/index.d.ts +448 -4
- package/dist/index.js +1753 -1283
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -1,15 +1,68 @@
|
|
|
1
|
-
import { useClient, defineType, defineField,
|
|
1
|
+
import { useClient, getPublishedId, pathToString, defineType, defineField, definePlugin } from "sanity";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { ErrorOutlineIcon, CheckmarkCircleIcon, EmptyIcon, AddIcon, TrashIcon, WarningOutlineIcon, InfoOutlineIcon, ArrowLeftIcon, EditIcon, TranslateIcon, CheckmarkIcon, CircleIcon, ArrowRightIcon, PanelRightIcon } from "@sanity/icons";
|
|
4
|
-
import { createContext,
|
|
4
|
+
import { createContext, useReducer, useEffect, useContext, useState, useCallback, useMemo } from "react";
|
|
5
|
+
import defaults from "lodash.defaults";
|
|
5
6
|
import { Card, Flex, Stack, Text, Spinner, Button, Box, Tooltip, Checkbox, Select, TextInput, TextArea, Heading, Inline, Badge, Grid, Radio, Label, Code } from "@sanity/ui";
|
|
6
7
|
import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
|
|
7
|
-
import defaults from "lodash.defaults";
|
|
8
8
|
import { toPlainText } from "@portabletext/react";
|
|
9
|
+
import { uuid } from "@sanity/uuid";
|
|
9
10
|
import cloneDeep from "lodash.clonedeep";
|
|
10
11
|
import get from "lodash.get";
|
|
11
12
|
import set from "lodash.set";
|
|
12
|
-
const
|
|
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
|
+
info: (message, context) => {
|
|
15
|
+
isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
|
|
16
|
+
},
|
|
17
|
+
warn: (message, context) => {
|
|
18
|
+
isLoggingEnabled() && console.warn("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
|
|
19
|
+
},
|
|
20
|
+
error: (message, error, context) => {
|
|
21
|
+
isLoggingEnabled() && console.error("[AI Translation]", message, error, context ? JSON.stringify(context, null, 2) : "");
|
|
22
|
+
},
|
|
23
|
+
debug: (message, data, context) => {
|
|
24
|
+
isLoggingEnabled() && console.debug("[AI Translation]", message, data, context ? JSON.stringify(context, null, 2) : "");
|
|
25
|
+
},
|
|
26
|
+
group: (label, context) => {
|
|
27
|
+
isLoggingEnabled() && console.group(`[AI Translation] ${label}`, context ? JSON.stringify(context, null, 2) : "");
|
|
28
|
+
},
|
|
29
|
+
groupEnd: () => {
|
|
30
|
+
isLoggingEnabled() && console.groupEnd();
|
|
31
|
+
},
|
|
32
|
+
time: (label) => {
|
|
33
|
+
isLoggingEnabled() && console.time(`[AI Translation] ${label}`);
|
|
34
|
+
},
|
|
35
|
+
timeEnd: (label) => {
|
|
36
|
+
isLoggingEnabled() && console.timeEnd(`[AI Translation] ${label}`);
|
|
37
|
+
}
|
|
38
|
+
}, validateOpenAIApiKey = async (apiKey, model = "gpt-5") => {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch("https://api.openai.com/v1/models", {
|
|
41
|
+
method: "GET",
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${apiKey}`
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok)
|
|
47
|
+
return {
|
|
48
|
+
valid: !1,
|
|
49
|
+
error: (await response.json()).error?.message || "Invalid API key"
|
|
50
|
+
};
|
|
51
|
+
const availableModels = (await response.json()).data.map((m) => m.id);
|
|
52
|
+
return availableModels.includes(model) ? {
|
|
53
|
+
valid: !0,
|
|
54
|
+
modelAvailable: availableModels
|
|
55
|
+
} : {
|
|
56
|
+
valid: !1,
|
|
57
|
+
error: `Model ${model} not available. Try: ${availableModels.slice(0, 5).join(", ")}`
|
|
58
|
+
};
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
valid: !1,
|
|
62
|
+
error: error instanceof Error ? error.message : "Network error"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}, TranslationContext = createContext({
|
|
13
66
|
document: null,
|
|
14
67
|
config: null,
|
|
15
68
|
overallStatus: "idle",
|
|
@@ -24,14 +77,126 @@ const TranslationContext = createContext({
|
|
|
24
77
|
openaiValidated: !1,
|
|
25
78
|
dispatch: () => {
|
|
26
79
|
}
|
|
27
|
-
}),
|
|
80
|
+
}), translationReducer = (context, action) => {
|
|
81
|
+
const logContext = {
|
|
82
|
+
documentId: context.document?._id,
|
|
83
|
+
actionType: action.type,
|
|
84
|
+
component: "translationReducer"
|
|
85
|
+
};
|
|
86
|
+
switch (devLog.debug("Translation action dispatched", {
|
|
87
|
+
...logContext,
|
|
88
|
+
payload: action.payload
|
|
89
|
+
}), action.type) {
|
|
90
|
+
case "SET_OVERALL_STATUS":
|
|
91
|
+
return { ...context, overallStatus: action.payload };
|
|
92
|
+
case "SET_TRANSLATION_METADATA":
|
|
93
|
+
return { ...context, translationMetadata: action.payload };
|
|
94
|
+
case "SET_SELECTED_TARGET_LANGUAGES":
|
|
95
|
+
return {
|
|
96
|
+
...context,
|
|
97
|
+
selectedTargetLanguages: action.payload
|
|
98
|
+
};
|
|
99
|
+
case "SET_TRANSLATION_MODE":
|
|
100
|
+
return {
|
|
101
|
+
...context,
|
|
102
|
+
translationMode: action.payload
|
|
103
|
+
};
|
|
104
|
+
case "SET_OPENAI_VALIDATED":
|
|
105
|
+
return {
|
|
106
|
+
...context,
|
|
107
|
+
openaiValidated: action.payload
|
|
108
|
+
};
|
|
109
|
+
case "SET_SHOULD_TRANSLATE_REFERENCES":
|
|
110
|
+
return {
|
|
111
|
+
...context,
|
|
112
|
+
shouldTranslateReferences: action.payload
|
|
113
|
+
};
|
|
114
|
+
case "SET_MAX_REFERENCE_DEPTH":
|
|
115
|
+
return {
|
|
116
|
+
...context,
|
|
117
|
+
maxReferenceDepth: action.payload
|
|
118
|
+
};
|
|
119
|
+
case "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS":
|
|
120
|
+
return {
|
|
121
|
+
...context,
|
|
122
|
+
shouldOverwriteFreshTranslations: action.payload
|
|
123
|
+
};
|
|
124
|
+
case "SET_REFERENCES":
|
|
125
|
+
return {
|
|
126
|
+
...context,
|
|
127
|
+
references: action.payload
|
|
128
|
+
};
|
|
129
|
+
case "SET_TRANSLATION_JOB_STATUS": {
|
|
130
|
+
const jobKey = `${action.payload._id}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs), existingJob = newTranslationJobs.get(jobKey), mergedJob = defaults(action.payload, existingJob);
|
|
131
|
+
return newTranslationJobs.set(jobKey, mergedJob), {
|
|
132
|
+
...context,
|
|
133
|
+
translationJobs: newTranslationJobs
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
default:
|
|
137
|
+
return context;
|
|
138
|
+
}
|
|
139
|
+
}, TranslationContextProvider = ({ document, config, children }) => {
|
|
140
|
+
devLog.debug("TranslationContextProvider initialized", {
|
|
141
|
+
documentId: document._id,
|
|
142
|
+
documentType: document._type,
|
|
143
|
+
supportedLanguages: config.supportedLanguages?.map((l) => l.id),
|
|
144
|
+
component: "TranslationContextProvider"
|
|
145
|
+
});
|
|
146
|
+
const initialContext = {
|
|
147
|
+
document,
|
|
148
|
+
config,
|
|
149
|
+
overallStatus: "idle",
|
|
150
|
+
translationMetadata: null,
|
|
151
|
+
selectedTargetLanguages: [],
|
|
152
|
+
shouldOverwriteFreshTranslations: !1,
|
|
153
|
+
shouldTranslateReferences: !1,
|
|
154
|
+
maxReferenceDepth: 1,
|
|
155
|
+
references: [],
|
|
156
|
+
translationJobs: /* @__PURE__ */ new Map(),
|
|
157
|
+
translationMode: config?.openai?.defaultMode || "sanity-only",
|
|
158
|
+
openaiValidated: !1
|
|
159
|
+
}, [context, dispatch] = useReducer(translationReducer, initialContext);
|
|
160
|
+
return useEffect(() => {
|
|
161
|
+
(async () => {
|
|
162
|
+
if (config?.openai?.enabled && config?.openai?.apiKey)
|
|
163
|
+
try {
|
|
164
|
+
const validation = await validateOpenAIApiKey(config.openai.apiKey, config.openai.model || "gpt-5");
|
|
165
|
+
dispatch({
|
|
166
|
+
type: "SET_OPENAI_VALIDATED",
|
|
167
|
+
payload: validation.valid
|
|
168
|
+
}), validation.valid ? devLog.info("OpenAI API key validated successfully", {
|
|
169
|
+
model: config.openai.model || "gpt-5",
|
|
170
|
+
component: "TranslationContextProvider"
|
|
171
|
+
}) : devLog.error("OpenAI API key validation failed", {
|
|
172
|
+
error: validation.error,
|
|
173
|
+
component: "TranslationContextProvider"
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
devLog.error("OpenAI validation error", error instanceof Error ? error : new Error(String(error)), {
|
|
177
|
+
component: "TranslationContextProvider"
|
|
178
|
+
}), dispatch({
|
|
179
|
+
type: "SET_OPENAI_VALIDATED",
|
|
180
|
+
payload: !1
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
})();
|
|
184
|
+
}, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), devLog.debug("Translation context state initialized", {
|
|
185
|
+
documentId: document._id,
|
|
186
|
+
overallStatus: context.overallStatus,
|
|
187
|
+
selectedLanguages: context.selectedTargetLanguages,
|
|
188
|
+
translationMode: context.translationMode,
|
|
189
|
+
openaiEnabled: config?.openai?.enabled,
|
|
190
|
+
component: "TranslationContextProvider"
|
|
191
|
+
}), /* @__PURE__ */ jsx(TranslationContext.Provider, { value: { ...context, dispatch }, children });
|
|
192
|
+
}, useTranslationContext = () => {
|
|
28
193
|
const context = useContext(TranslationContext);
|
|
29
194
|
if (!context.document || !context.config)
|
|
30
195
|
throw new Error(
|
|
31
196
|
"useTranslationContext must be used within a TranslationContextProvider with document and config props"
|
|
32
197
|
);
|
|
33
198
|
return context;
|
|
34
|
-
},
|
|
199
|
+
}, undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_TYPE = "translation.metadata", TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
|
|
35
200
|
const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
|
|
36
201
|
params: {
|
|
37
202
|
documentId: undraftId(documentId),
|
|
@@ -58,31 +223,6 @@ const TranslationContext = createContext({
|
|
|
58
223
|
loading,
|
|
59
224
|
error
|
|
60
225
|
};
|
|
61
|
-
}, isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true", devLog = {
|
|
62
|
-
info: (message, context) => {
|
|
63
|
-
isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
|
|
64
|
-
},
|
|
65
|
-
warn: (message, context) => {
|
|
66
|
-
isLoggingEnabled() && console.warn("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
|
|
67
|
-
},
|
|
68
|
-
error: (message, error, context) => {
|
|
69
|
-
isLoggingEnabled() && console.error("[AI Translation]", message, error, context ? JSON.stringify(context, null, 2) : "");
|
|
70
|
-
},
|
|
71
|
-
debug: (message, data, context) => {
|
|
72
|
-
isLoggingEnabled() && console.debug("[AI Translation]", message, data, context ? JSON.stringify(context, null, 2) : "");
|
|
73
|
-
},
|
|
74
|
-
group: (label, context) => {
|
|
75
|
-
isLoggingEnabled() && console.group(`[AI Translation] ${label}`, context ? JSON.stringify(context, null, 2) : "");
|
|
76
|
-
},
|
|
77
|
-
groupEnd: () => {
|
|
78
|
-
isLoggingEnabled() && console.groupEnd();
|
|
79
|
-
},
|
|
80
|
-
time: (label) => {
|
|
81
|
-
isLoggingEnabled() && console.time(`[AI Translation] ${label}`);
|
|
82
|
-
},
|
|
83
|
-
timeEnd: (label) => {
|
|
84
|
-
isLoggingEnabled() && console.timeEnd(`[AI Translation] ${label}`);
|
|
85
|
-
}
|
|
86
226
|
};
|
|
87
227
|
async function validateSuggestion(client, suggestion) {
|
|
88
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" };
|
|
@@ -191,12 +331,7 @@ async function analyzeFeedback({
|
|
|
191
331
|
currentDocument,
|
|
192
332
|
onProgress
|
|
193
333
|
}) {
|
|
194
|
-
|
|
195
|
-
sourceLanguage,
|
|
196
|
-
targetLanguage,
|
|
197
|
-
hasUserFeedback: !!userFeedback,
|
|
198
|
-
shouldIncludeEdits
|
|
199
|
-
}), onProgress?.({ stage: "analyzing", message: "Analyzing feedback and edits..." });
|
|
334
|
+
onProgress?.({ stage: "analyzing", message: "Analyzing feedback and edits..." });
|
|
200
335
|
const analysisPrompt = buildAnalysisPrompt({
|
|
201
336
|
sourceLanguage,
|
|
202
337
|
targetLanguage,
|
|
@@ -205,10 +340,8 @@ async function analyzeFeedback({
|
|
|
205
340
|
sourceSnapshot,
|
|
206
341
|
targetSnapshot,
|
|
207
342
|
currentDocument
|
|
208
|
-
}), analysis = await analyzeWithAI(client, analysisPrompt);
|
|
209
|
-
|
|
210
|
-
const suggestions = convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage);
|
|
211
|
-
devLog.info("Generated suggestions", { count: suggestions.length }), onProgress?.({ stage: "analyzing", message: "Validating suggestions..." });
|
|
343
|
+
}), analysis = await analyzeWithAI(client, analysisPrompt), suggestions = convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage);
|
|
344
|
+
onProgress?.({ stage: "analyzing", message: "Validating suggestions..." });
|
|
212
345
|
const validatedSuggestions = await Promise.all(
|
|
213
346
|
suggestions.map(async (suggestion) => {
|
|
214
347
|
const validation = await validateSuggestion(client, suggestion);
|
|
@@ -221,7 +354,10 @@ async function analyzeFeedback({
|
|
|
221
354
|
};
|
|
222
355
|
})
|
|
223
356
|
);
|
|
224
|
-
return devLog.info("
|
|
357
|
+
return devLog.info("Feedback analysis complete", {
|
|
358
|
+
sourceLanguage,
|
|
359
|
+
targetLanguage,
|
|
360
|
+
total: validatedSuggestions.length,
|
|
225
361
|
valid: validatedSuggestions.filter((s) => s.validationStatus === "valid").length,
|
|
226
362
|
warnings: validatedSuggestions.filter((s) => s.validationStatus === "warning").length,
|
|
227
363
|
invalid: validatedSuggestions.filter((s) => s.validationStatus === "invalid").length
|
|
@@ -235,24 +371,18 @@ async function applySuggestions({
|
|
|
235
371
|
suggestions,
|
|
236
372
|
onProgress
|
|
237
373
|
}) {
|
|
238
|
-
if (
|
|
239
|
-
sourceLanguage,
|
|
240
|
-
targetLanguage,
|
|
241
|
-
targetLanguageType: typeof targetLanguage,
|
|
242
|
-
targetLanguageValue: targetLanguage,
|
|
243
|
-
count: suggestions.filter((s) => s.approved).length
|
|
244
|
-
}), !targetLanguage)
|
|
374
|
+
if (!targetLanguage)
|
|
245
375
|
throw devLog.error("targetLanguage is undefined in applySuggestions!", {
|
|
246
376
|
sourceLanguage,
|
|
247
377
|
targetLanguage,
|
|
248
378
|
suggestionsCount: suggestions.length
|
|
249
379
|
}), new Error("targetLanguage is required but was undefined");
|
|
250
380
|
const approvedSuggestions = suggestions.filter((s) => s.approved);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
+
}));
|
|
256
386
|
}
|
|
257
387
|
function buildAnalysisPrompt({
|
|
258
388
|
sourceLanguage,
|
|
@@ -420,7 +550,7 @@ function convertAnalysisToSuggestions(analysis, sourceLanguage, targetLanguage)
|
|
|
420
550
|
type: "glossaryTerm",
|
|
421
551
|
description: `${term.sourceValue} \u2192 ${term.targetValue}`,
|
|
422
552
|
approved: !0,
|
|
423
|
-
// Use
|
|
553
|
+
// Use explicit source/target languages from history instead of AI-provided values
|
|
424
554
|
sourceLocale: sourceLanguage,
|
|
425
555
|
sourceValue: term.sourceValue,
|
|
426
556
|
targetLocale: targetLanguage,
|
|
@@ -470,49 +600,21 @@ function parseAIResponse(responseText) {
|
|
|
470
600
|
}
|
|
471
601
|
}
|
|
472
602
|
async function applyImprovementsFromSuggestions(client, schemaId, targetLanguage, suggestions, onProgress) {
|
|
473
|
-
devLog.info("Applying improvements from suggestions", {
|
|
474
|
-
count: suggestions.length,
|
|
475
|
-
targetLanguage,
|
|
476
|
-
suggestions: suggestions.map((s) => ({
|
|
477
|
-
type: s.type,
|
|
478
|
-
approved: s.approved,
|
|
479
|
-
sourceLocale: s.sourceLocale,
|
|
480
|
-
targetLocale: s.targetLocale,
|
|
481
|
-
sourceValue: s.sourceValue,
|
|
482
|
-
targetValue: s.targetValue
|
|
483
|
-
}))
|
|
484
|
-
});
|
|
485
603
|
const settingsId = "translation-settings";
|
|
486
604
|
await client.fetch("*[_id == $id][0]", { id: settingsId }) || await client.createIfNotExists({
|
|
487
605
|
_id: settingsId,
|
|
488
606
|
_type: "translationSettings"
|
|
489
607
|
});
|
|
490
|
-
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()),
|
|
491
|
-
devLog.info("Glossary terms before filtering", {
|
|
492
|
-
count: glossaryTermsBeforeFilter.length,
|
|
493
|
-
terms: glossaryTermsBeforeFilter.map((s) => ({
|
|
494
|
-
sourceLocale: s.sourceLocale,
|
|
495
|
-
sourceValue: s.sourceValue,
|
|
496
|
-
targetLocale: s.targetLocale,
|
|
497
|
-
targetValue: s.targetValue,
|
|
498
|
-
approved: s.approved
|
|
499
|
-
}))
|
|
500
|
-
});
|
|
501
|
-
const glossaryTerms = suggestions.filter(
|
|
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(
|
|
502
609
|
(s) => s.type === "glossaryTerm" && s.sourceLocale?.trim() && s.sourceValue?.trim() && s.targetLocale?.trim() && s.targetValue?.trim()
|
|
503
610
|
);
|
|
504
|
-
if (
|
|
505
|
-
globalPrompts: globalPrompts.length,
|
|
506
|
-
localePrompts: localePrompts.length,
|
|
507
|
-
protectedPhrases: protectedPhrases.length,
|
|
508
|
-
glossaryTerms: glossaryTerms.length
|
|
509
|
-
}), (globalPrompts.length > 0 || localePrompts.length > 0) && (onProgress?.({
|
|
611
|
+
if ((globalPrompts.length > 0 || localePrompts.length > 0) && (onProgress?.({
|
|
510
612
|
stage: "applying",
|
|
511
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...`
|
|
512
614
|
}), updates.push(updatePromptsWithTemplate(client, schemaId, settingsId, targetLanguage, globalPrompts, localePrompts))), protectedPhrases.length > 0) {
|
|
513
615
|
onProgress?.({ stage: "applying", message: "Adding protected phrases..." });
|
|
514
616
|
const phrases = protectedPhrases.map((s) => s.description);
|
|
515
|
-
|
|
617
|
+
updates.push(addProtectedPhrases(client, settingsId, phrases));
|
|
516
618
|
}
|
|
517
619
|
if (glossaryTerms.length > 0) {
|
|
518
620
|
onProgress?.({ stage: "applying", message: "Adding glossary terms..." });
|
|
@@ -524,22 +626,17 @@ async function applyImprovementsFromSuggestions(client, schemaId, targetLanguage
|
|
|
524
626
|
targetValue: s.targetValue
|
|
525
627
|
// biome-ignore-end lint/style/noNonNullAssertion: safely handled
|
|
526
628
|
}));
|
|
527
|
-
|
|
629
|
+
updates.push(addGlossaryTerms(client, terms));
|
|
528
630
|
}
|
|
529
631
|
updates.length === 0 && devLog.warn("No valid updates to apply - all suggestions may have been empty"), await Promise.all(updates), onProgress?.({ stage: "applying", message: "Recording changes..." }), await recordChangeHistory(
|
|
530
632
|
client,
|
|
531
633
|
settingsId,
|
|
532
634
|
targetLanguage,
|
|
533
635
|
suggestions.filter((s) => s.approved)
|
|
534
|
-
)
|
|
636
|
+
);
|
|
535
637
|
}
|
|
536
638
|
async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLanguage, globalPrompts, localePrompts) {
|
|
537
|
-
if (
|
|
538
|
-
settingsId,
|
|
539
|
-
targetLanguage,
|
|
540
|
-
hasGlobalUpdates: globalPrompts.length > 0,
|
|
541
|
-
hasLocaleUpdates: localePrompts.length > 0
|
|
542
|
-
}), !targetLanguage)
|
|
639
|
+
if (!targetLanguage)
|
|
543
640
|
throw devLog.error("targetLanguage is undefined or empty!", {
|
|
544
641
|
targetLanguage,
|
|
545
642
|
globalPrompts: globalPrompts.length,
|
|
@@ -555,11 +652,7 @@ async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLa
|
|
|
555
652
|
...updates.map((update) => `- ${update}`)
|
|
556
653
|
].filter(Boolean).join(`
|
|
557
654
|
`);
|
|
558
|
-
await client.patch(settingsId).set({ globalPrompt: newGlobalPrompt }).commit()
|
|
559
|
-
previousLength: currentGlobalPrompt.length,
|
|
560
|
-
newLength: newGlobalPrompt.length,
|
|
561
|
-
addedUpdates: updates.length
|
|
562
|
-
});
|
|
655
|
+
await client.patch(settingsId).set({ globalPrompt: newGlobalPrompt }).commit();
|
|
563
656
|
}
|
|
564
657
|
if (localePrompts.length > 0) {
|
|
565
658
|
const updates = localePrompts.map((s) => s.description.trim()).filter(Boolean), existingLocalePrompts = currentSettings?.localePrompts || [], existingLocalePrompt = existingLocalePrompts.find((lp) => lp._key === targetLanguage);
|
|
@@ -572,12 +665,7 @@ async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLa
|
|
|
572
665
|
...updates.map((update) => `- ${update}`)
|
|
573
666
|
].filter(Boolean).join(`
|
|
574
667
|
`), localeIndex = existingLocalePrompts.findIndex((lp) => lp._key === targetLanguage);
|
|
575
|
-
await client.patch(settingsId).set({ [`localePrompts[${localeIndex}].value`]: newValue }).commit()
|
|
576
|
-
targetLanguage,
|
|
577
|
-
previousLength: currentValue.length,
|
|
578
|
-
newLength: newValue.length,
|
|
579
|
-
addedUpdates: updates.length
|
|
580
|
-
});
|
|
668
|
+
await client.patch(settingsId).set({ [`localePrompts[${localeIndex}].value`]: newValue }).commit();
|
|
581
669
|
} else {
|
|
582
670
|
const newValue = [`## User Feedback (${timestamp})`, ...updates.map((update) => `- ${update}`)].filter(Boolean).join(`
|
|
583
671
|
`);
|
|
@@ -587,22 +675,17 @@ async function updatePromptsWithTemplate(client, _schemaId, settingsId, targetLa
|
|
|
587
675
|
_key: targetLanguage,
|
|
588
676
|
value: newValue
|
|
589
677
|
}
|
|
590
|
-
]).commit()
|
|
591
|
-
targetLanguage,
|
|
592
|
-
length: newValue.length,
|
|
593
|
-
addedUpdates: updates.length
|
|
594
|
-
});
|
|
678
|
+
]).commit();
|
|
595
679
|
}
|
|
596
680
|
}
|
|
597
681
|
devLog.info("Prompts updated successfully", {
|
|
682
|
+
targetLanguage,
|
|
598
683
|
globalUpdated: globalPrompts.length > 0,
|
|
599
684
|
localeUpdated: localePrompts.length > 0
|
|
600
685
|
});
|
|
601
686
|
}
|
|
602
687
|
async function addProtectedPhrases(client, settingsId, phrases) {
|
|
603
|
-
devLog.info("Adding protected phrases", { settingsId, phrasesCount: phrases.length, phrases });
|
|
604
688
|
const currentPhrases = await client.fetch("*[_id == $settingsId][0].protectedPhrases", { settingsId });
|
|
605
|
-
devLog.debug("Fetched current protected phrases", { currentCount: currentPhrases?.length });
|
|
606
689
|
let newPhrases = phrases.filter((phrase) => !currentPhrases?.includes(phrase));
|
|
607
690
|
const conflictingPhrases = [];
|
|
608
691
|
for (const phrase of newPhrases)
|
|
@@ -610,12 +693,7 @@ async function addProtectedPhrases(client, settingsId, phrases) {
|
|
|
610
693
|
'count(*[_type == "translationGlossaryTerm" && count(translations[value == $phrase]) > 0]) > 0',
|
|
611
694
|
{ phrase }
|
|
612
695
|
) && (conflictingPhrases.push(phrase), devLog.warn("Skipping protected phrase due to glossary conflict", { phrase }));
|
|
613
|
-
|
|
614
|
-
devLog.info("No new protected phrases to add (all already exist)");
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
const result = await client.patch(settingsId).setIfMissing({ protectedPhrases: [] }).append("protectedPhrases", newPhrases).commit();
|
|
618
|
-
devLog.info("Protected phrases added", { count: newPhrases.length, phrases: newPhrases, resultId: result._id });
|
|
696
|
+
newPhrases = newPhrases.filter((phrase) => !conflictingPhrases.includes(phrase)), newPhrases.length !== 0 && (await client.patch(settingsId).setIfMissing({ protectedPhrases: [] }).append("protectedPhrases", newPhrases).commit(), devLog.info("Protected phrases added", { count: newPhrases.length }));
|
|
619
697
|
}
|
|
620
698
|
async function addGlossaryTerms(client, terms) {
|
|
621
699
|
const protectedPhrasesLower = (await client.fetch('*[_id == "translation-settings"][0].protectedPhrases'))?.map((p) => p.toLowerCase()) || [], validTerms = terms.filter(({ sourceValue, targetValue }) => sourceValue === targetValue ? (devLog.warn("Skipping glossary term where source === target (should be a protected phrase)", {
|
|
@@ -643,29 +721,13 @@ async function addGlossaryTerms(client, terms) {
|
|
|
643
721
|
);
|
|
644
722
|
if (existingTerm) {
|
|
645
723
|
const existingTargetIndex = existingTerm.translations.findIndex((t) => t._key === targetLocale), existingTargetValue = existingTargetIndex >= 0 ? existingTerm.translations[existingTargetIndex].value : null;
|
|
646
|
-
existingTargetIndex === -1 ?
|
|
724
|
+
existingTargetIndex === -1 ? await client.patch(existingTerm._id).append("translations", [
|
|
647
725
|
{
|
|
648
726
|
_type: "internationalizedArrayStringValue",
|
|
649
727
|
_key: targetLocale,
|
|
650
728
|
value: targetValue
|
|
651
729
|
}
|
|
652
|
-
]).commit()
|
|
653
|
-
sourceLocale,
|
|
654
|
-
sourceValue,
|
|
655
|
-
targetLocale,
|
|
656
|
-
targetValue
|
|
657
|
-
})) : existingTargetValue !== targetValue ? (await client.patch(existingTerm._id).set({ [`translations[${existingTargetIndex}].value`]: targetValue }).commit(), devLog.info("Updated existing glossary term translation", {
|
|
658
|
-
sourceLocale,
|
|
659
|
-
sourceValue,
|
|
660
|
-
targetLocale,
|
|
661
|
-
oldValue: existingTargetValue,
|
|
662
|
-
newValue: targetValue
|
|
663
|
-
})) : devLog.info("Glossary term already has this exact translation", {
|
|
664
|
-
sourceLocale,
|
|
665
|
-
sourceValue,
|
|
666
|
-
targetLocale,
|
|
667
|
-
targetValue
|
|
668
|
-
});
|
|
730
|
+
]).commit() : existingTargetValue !== targetValue && await client.patch(existingTerm._id).set({ [`translations[${existingTargetIndex}].value`]: targetValue }).commit();
|
|
669
731
|
} else
|
|
670
732
|
await client.create({
|
|
671
733
|
_type: "translationGlossaryTerm",
|
|
@@ -681,11 +743,10 @@ async function addGlossaryTerms(client, terms) {
|
|
|
681
743
|
value: targetValue
|
|
682
744
|
}
|
|
683
745
|
]
|
|
684
|
-
})
|
|
746
|
+
});
|
|
685
747
|
}
|
|
686
748
|
devLog.info("Glossary terms processed", {
|
|
687
|
-
|
|
688
|
-
validAdded: validTerms.length,
|
|
749
|
+
processed: validTerms.length,
|
|
689
750
|
skipped: terms.length - validTerms.length
|
|
690
751
|
});
|
|
691
752
|
}
|
|
@@ -698,10 +759,7 @@ async function recordChangeHistory(client, settingsId, targetLanguage, suggestio
|
|
|
698
759
|
description: suggestion.description,
|
|
699
760
|
locale: suggestion.type === "localePrompt" ? targetLanguage : void 0
|
|
700
761
|
}));
|
|
701
|
-
historyEntries.length !== 0 &&
|
|
702
|
-
count: historyEntries.length,
|
|
703
|
-
timestamp
|
|
704
|
-
}));
|
|
762
|
+
historyEntries.length !== 0 && await client.patch(settingsId).setIfMissing({ changeHistory: [] }).append("changeHistory", historyEntries).commit();
|
|
705
763
|
}
|
|
706
764
|
const ICON_SIZE = 64, progressIconMap = {
|
|
707
765
|
in_progress: /* @__PURE__ */ jsx(Spinner, {}),
|
|
@@ -1150,145 +1208,6 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
1150
1208
|
)
|
|
1151
1209
|
] })
|
|
1152
1210
|
] });
|
|
1153
|
-
}, validateOpenAIApiKey = async (apiKey, model = "gpt-5") => {
|
|
1154
|
-
try {
|
|
1155
|
-
const response = await fetch("https://api.openai.com/v1/models", {
|
|
1156
|
-
method: "GET",
|
|
1157
|
-
headers: {
|
|
1158
|
-
Authorization: `Bearer ${apiKey}`
|
|
1159
|
-
}
|
|
1160
|
-
});
|
|
1161
|
-
if (!response.ok)
|
|
1162
|
-
return {
|
|
1163
|
-
valid: !1,
|
|
1164
|
-
error: (await response.json()).error?.message || "Invalid API key"
|
|
1165
|
-
};
|
|
1166
|
-
const availableModels = (await response.json()).data.map((m) => m.id);
|
|
1167
|
-
return availableModels.includes(model) ? {
|
|
1168
|
-
valid: !0,
|
|
1169
|
-
modelAvailable: availableModels
|
|
1170
|
-
} : {
|
|
1171
|
-
valid: !1,
|
|
1172
|
-
error: `Model ${model} not available. Try: ${availableModels.slice(0, 5).join(", ")}`
|
|
1173
|
-
};
|
|
1174
|
-
} catch (error) {
|
|
1175
|
-
return {
|
|
1176
|
-
valid: !1,
|
|
1177
|
-
error: error instanceof Error ? error.message : "Network error"
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
}, translationReducer = (context, action) => {
|
|
1181
|
-
const logContext = {
|
|
1182
|
-
documentId: context.document?._id,
|
|
1183
|
-
actionType: action.type,
|
|
1184
|
-
component: "translationReducer"
|
|
1185
|
-
};
|
|
1186
|
-
switch (devLog.debug("Translation action dispatched", {
|
|
1187
|
-
...logContext,
|
|
1188
|
-
payload: action.payload
|
|
1189
|
-
}), action.type) {
|
|
1190
|
-
case "SET_OVERALL_STATUS":
|
|
1191
|
-
return { ...context, overallStatus: action.payload };
|
|
1192
|
-
case "SET_TRANSLATION_METADATA":
|
|
1193
|
-
return { ...context, translationMetadata: action.payload };
|
|
1194
|
-
case "SET_SELECTED_TARGET_LANGUAGES":
|
|
1195
|
-
return {
|
|
1196
|
-
...context,
|
|
1197
|
-
selectedTargetLanguages: action.payload
|
|
1198
|
-
};
|
|
1199
|
-
case "SET_TRANSLATION_MODE":
|
|
1200
|
-
return {
|
|
1201
|
-
...context,
|
|
1202
|
-
translationMode: action.payload
|
|
1203
|
-
};
|
|
1204
|
-
case "SET_OPENAI_VALIDATED":
|
|
1205
|
-
return {
|
|
1206
|
-
...context,
|
|
1207
|
-
openaiValidated: action.payload
|
|
1208
|
-
};
|
|
1209
|
-
case "SET_SHOULD_TRANSLATE_REFERENCES":
|
|
1210
|
-
return {
|
|
1211
|
-
...context,
|
|
1212
|
-
shouldTranslateReferences: action.payload
|
|
1213
|
-
};
|
|
1214
|
-
case "SET_MAX_REFERENCE_DEPTH":
|
|
1215
|
-
return {
|
|
1216
|
-
...context,
|
|
1217
|
-
maxReferenceDepth: action.payload
|
|
1218
|
-
};
|
|
1219
|
-
case "SET_SHOULD_OVERWRITE_FRESH_TRANSLATIONS":
|
|
1220
|
-
return {
|
|
1221
|
-
...context,
|
|
1222
|
-
shouldOverwriteFreshTranslations: action.payload
|
|
1223
|
-
};
|
|
1224
|
-
case "SET_REFERENCES":
|
|
1225
|
-
return {
|
|
1226
|
-
...context,
|
|
1227
|
-
references: action.payload
|
|
1228
|
-
};
|
|
1229
|
-
case "SET_TRANSLATION_JOB_STATUS": {
|
|
1230
|
-
const jobKey = `${action.payload._id}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs);
|
|
1231
|
-
return newTranslationJobs.set(jobKey, defaults(action.payload, newTranslationJobs.get(jobKey))), {
|
|
1232
|
-
...context,
|
|
1233
|
-
translationJobs: newTranslationJobs
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
default:
|
|
1237
|
-
return context;
|
|
1238
|
-
}
|
|
1239
|
-
}, TranslationContextProvider = ({ document, config, children }) => {
|
|
1240
|
-
devLog.debug("TranslationContextProvider initialized", {
|
|
1241
|
-
documentId: document._id,
|
|
1242
|
-
documentType: document._type,
|
|
1243
|
-
supportedLanguages: config.supportedLanguages?.map((l) => l.id),
|
|
1244
|
-
component: "TranslationContextProvider"
|
|
1245
|
-
});
|
|
1246
|
-
const initialContext = {
|
|
1247
|
-
document,
|
|
1248
|
-
config,
|
|
1249
|
-
overallStatus: "idle",
|
|
1250
|
-
translationMetadata: null,
|
|
1251
|
-
selectedTargetLanguages: [],
|
|
1252
|
-
shouldOverwriteFreshTranslations: !1,
|
|
1253
|
-
shouldTranslateReferences: !1,
|
|
1254
|
-
maxReferenceDepth: 1,
|
|
1255
|
-
references: [],
|
|
1256
|
-
translationJobs: /* @__PURE__ */ new Map(),
|
|
1257
|
-
translationMode: config?.openai?.defaultMode || "sanity-only",
|
|
1258
|
-
openaiValidated: !1
|
|
1259
|
-
}, [context, dispatch] = useReducer(translationReducer, initialContext);
|
|
1260
|
-
return useEffect(() => {
|
|
1261
|
-
(async () => {
|
|
1262
|
-
if (config?.openai?.enabled && config?.openai?.apiKey)
|
|
1263
|
-
try {
|
|
1264
|
-
const validation = await validateOpenAIApiKey(config.openai.apiKey, config.openai.model || "gpt-5");
|
|
1265
|
-
dispatch({
|
|
1266
|
-
type: "SET_OPENAI_VALIDATED",
|
|
1267
|
-
payload: validation.valid
|
|
1268
|
-
}), validation.valid ? devLog.info("OpenAI API key validated successfully", {
|
|
1269
|
-
model: config.openai.model || "gpt-5",
|
|
1270
|
-
component: "TranslationContextProvider"
|
|
1271
|
-
}) : devLog.error("OpenAI API key validation failed", {
|
|
1272
|
-
error: validation.error,
|
|
1273
|
-
component: "TranslationContextProvider"
|
|
1274
|
-
});
|
|
1275
|
-
} catch (error) {
|
|
1276
|
-
devLog.error("OpenAI validation error", error instanceof Error ? error : new Error(String(error)), {
|
|
1277
|
-
component: "TranslationContextProvider"
|
|
1278
|
-
}), dispatch({
|
|
1279
|
-
type: "SET_OPENAI_VALIDATED",
|
|
1280
|
-
payload: !1
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
})();
|
|
1284
|
-
}, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), devLog.debug("Translation context state initialized", {
|
|
1285
|
-
documentId: document._id,
|
|
1286
|
-
overallStatus: context.overallStatus,
|
|
1287
|
-
selectedLanguages: context.selectedTargetLanguages,
|
|
1288
|
-
translationMode: context.translationMode,
|
|
1289
|
-
openaiEnabled: config?.openai?.enabled,
|
|
1290
|
-
component: "TranslationContextProvider"
|
|
1291
|
-
}), /* @__PURE__ */ jsx(TranslationContext.Provider, { value: { ...context, dispatch }, children });
|
|
1292
1211
|
}, getGiveTranslationFeedbackAction = (config) => ({ draft, published }) => {
|
|
1293
1212
|
const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
|
|
1294
1213
|
return devLog.debug("ImproveTranslationPromptsAction initialized", {
|
|
@@ -1314,181 +1233,7 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
1314
1233
|
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(GiveFeedbackDialog, { setIsDialogOpen }) })
|
|
1315
1234
|
}
|
|
1316
1235
|
} : (devLog.error("Missing config in TranslateAction"), null);
|
|
1317
|
-
}
|
|
1318
|
-
name: "translationGlossaryTerm",
|
|
1319
|
-
title: "Translation Glossary Term",
|
|
1320
|
-
icon: TranslateIcon,
|
|
1321
|
-
type: "document",
|
|
1322
|
-
fields: [
|
|
1323
|
-
defineField({
|
|
1324
|
-
name: "translations",
|
|
1325
|
-
title: "Translations",
|
|
1326
|
-
type: "internationalizedArrayString"
|
|
1327
|
-
})
|
|
1328
|
-
],
|
|
1329
|
-
preview: {
|
|
1330
|
-
select: {
|
|
1331
|
-
translations: "translations"
|
|
1332
|
-
},
|
|
1333
|
-
prepare({ translations }) {
|
|
1334
|
-
const enTranslation = translations?.find((t) => t.locale === "en-us"), firstTranslation = translations?.[0];
|
|
1335
|
-
return {
|
|
1336
|
-
title: enTranslation?.value || firstTranslation?.value || "No translation available"
|
|
1337
|
-
};
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
}), translationHistory = defineField({
|
|
1341
|
-
name: "translationHistory",
|
|
1342
|
-
type: "array",
|
|
1343
|
-
of: [
|
|
1344
|
-
{
|
|
1345
|
-
name: "targetLanguageHistory",
|
|
1346
|
-
type: "object",
|
|
1347
|
-
fields: [
|
|
1348
|
-
defineField({
|
|
1349
|
-
name: "language",
|
|
1350
|
-
title: "Language",
|
|
1351
|
-
description: "The language of the target document",
|
|
1352
|
-
type: "string"
|
|
1353
|
-
}),
|
|
1354
|
-
defineField({
|
|
1355
|
-
name: "sourceId",
|
|
1356
|
-
title: "Source ID",
|
|
1357
|
-
description: "The ID of the source document",
|
|
1358
|
-
type: "string"
|
|
1359
|
-
}),
|
|
1360
|
-
defineField({
|
|
1361
|
-
name: "sourceRev",
|
|
1362
|
-
title: "Source Revision",
|
|
1363
|
-
description: "The revision ID of the source document at the time of translation",
|
|
1364
|
-
type: "string"
|
|
1365
|
-
}),
|
|
1366
|
-
defineField({
|
|
1367
|
-
name: "sourceLanguage",
|
|
1368
|
-
title: "Source Language",
|
|
1369
|
-
description: "The language of the source document",
|
|
1370
|
-
type: "string"
|
|
1371
|
-
}),
|
|
1372
|
-
defineField({
|
|
1373
|
-
name: "sourceSnapshot",
|
|
1374
|
-
title: "Source Snapshot",
|
|
1375
|
-
description: "The snapshot of the source document at the time of translation",
|
|
1376
|
-
type: "text"
|
|
1377
|
-
}),
|
|
1378
|
-
defineField({
|
|
1379
|
-
name: "targetId",
|
|
1380
|
-
title: "Target ID",
|
|
1381
|
-
description: "The ID of the target document",
|
|
1382
|
-
type: "string"
|
|
1383
|
-
})
|
|
1384
|
-
]
|
|
1385
|
-
}
|
|
1386
|
-
]
|
|
1387
|
-
}), translationSettings = defineType({
|
|
1388
|
-
name: "translationSettings",
|
|
1389
|
-
title: "Translation Settings",
|
|
1390
|
-
icon: TranslateIcon,
|
|
1391
|
-
type: "document",
|
|
1392
|
-
groups: [
|
|
1393
|
-
{
|
|
1394
|
-
name: "general",
|
|
1395
|
-
title: "General",
|
|
1396
|
-
default: !0
|
|
1397
|
-
},
|
|
1398
|
-
{
|
|
1399
|
-
name: "history",
|
|
1400
|
-
title: "History"
|
|
1401
|
-
}
|
|
1402
|
-
],
|
|
1403
|
-
fields: [
|
|
1404
|
-
defineField({
|
|
1405
|
-
name: "globalPrompt",
|
|
1406
|
-
title: "Global Translation Prompt",
|
|
1407
|
-
description: "A prompt that will given to the Sanity AI Agent for all translation tasks.",
|
|
1408
|
-
type: "text",
|
|
1409
|
-
rows: 5,
|
|
1410
|
-
group: "general"
|
|
1411
|
-
}),
|
|
1412
|
-
defineField({
|
|
1413
|
-
name: "localePrompts",
|
|
1414
|
-
title: "Locale Prompts",
|
|
1415
|
-
description: "Prompts that will be given to the Sanity AI Agent for each locale.",
|
|
1416
|
-
type: "internationalizedArrayText",
|
|
1417
|
-
group: "general"
|
|
1418
|
-
}),
|
|
1419
|
-
defineField({
|
|
1420
|
-
name: "protectedPhrases",
|
|
1421
|
-
title: "Protected Phrases",
|
|
1422
|
-
description: "Phrases that will never be translated.",
|
|
1423
|
-
type: "array",
|
|
1424
|
-
of: [{ type: "string" }],
|
|
1425
|
-
group: "general"
|
|
1426
|
-
}),
|
|
1427
|
-
defineField({
|
|
1428
|
-
name: "changeHistory",
|
|
1429
|
-
title: "Change History",
|
|
1430
|
-
description: "History of changes made via user feedback.",
|
|
1431
|
-
type: "array",
|
|
1432
|
-
readOnly: !0,
|
|
1433
|
-
group: "history",
|
|
1434
|
-
of: [
|
|
1435
|
-
{
|
|
1436
|
-
type: "object",
|
|
1437
|
-
fields: [
|
|
1438
|
-
defineField({
|
|
1439
|
-
name: "timestamp",
|
|
1440
|
-
title: "Timestamp",
|
|
1441
|
-
type: "datetime"
|
|
1442
|
-
}),
|
|
1443
|
-
defineField({
|
|
1444
|
-
name: "changeType",
|
|
1445
|
-
title: "Change Type",
|
|
1446
|
-
type: "string",
|
|
1447
|
-
options: {
|
|
1448
|
-
list: [
|
|
1449
|
-
{ title: "Global Prompt", value: "globalPrompt" },
|
|
1450
|
-
{ title: "Locale Prompt", value: "localePrompt" },
|
|
1451
|
-
{ title: "Protected Phrase", value: "protectedPhrase" },
|
|
1452
|
-
{ title: "Glossary Term", value: "glossaryTerm" }
|
|
1453
|
-
]
|
|
1454
|
-
}
|
|
1455
|
-
}),
|
|
1456
|
-
defineField({
|
|
1457
|
-
name: "description",
|
|
1458
|
-
title: "Description",
|
|
1459
|
-
type: "text",
|
|
1460
|
-
rows: 3
|
|
1461
|
-
}),
|
|
1462
|
-
defineField({
|
|
1463
|
-
name: "locale",
|
|
1464
|
-
title: "Locale",
|
|
1465
|
-
type: "string",
|
|
1466
|
-
description: "Target locale for locale-specific changes"
|
|
1467
|
-
})
|
|
1468
|
-
],
|
|
1469
|
-
preview: {
|
|
1470
|
-
select: {
|
|
1471
|
-
timestamp: "timestamp",
|
|
1472
|
-
changeType: "changeType",
|
|
1473
|
-
description: "description"
|
|
1474
|
-
},
|
|
1475
|
-
prepare({ timestamp, changeType, description }) {
|
|
1476
|
-
return {
|
|
1477
|
-
title: `${changeType} - ${new Date(timestamp).toLocaleDateString()}`,
|
|
1478
|
-
subtitle: description
|
|
1479
|
-
};
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
]
|
|
1484
|
-
})
|
|
1485
|
-
],
|
|
1486
|
-
preview: {
|
|
1487
|
-
prepare: () => ({
|
|
1488
|
-
title: "Translation Settings"
|
|
1489
|
-
})
|
|
1490
|
-
}
|
|
1491
|
-
});
|
|
1236
|
+
};
|
|
1492
1237
|
class Semaphore {
|
|
1493
1238
|
permits;
|
|
1494
1239
|
queue = [];
|
|
@@ -1664,11 +1409,21 @@ const fetchTranslationPrompts = async ({
|
|
|
1664
1409
|
const sourceDocumentString = documentToPlainText(sourceDocument), translationSettings2 = await client.fetch(
|
|
1665
1410
|
`*[_id == "translation-settings"][0] {
|
|
1666
1411
|
globalPrompt,
|
|
1667
|
-
localePrompts[
|
|
1412
|
+
localePrompts[_key in $targetLanguages],
|
|
1668
1413
|
protectedPhrases
|
|
1669
1414
|
}`,
|
|
1670
1415
|
{ targetLanguages }
|
|
1671
|
-
)
|
|
1416
|
+
);
|
|
1417
|
+
devLog.debug("Fetched translation settings", {
|
|
1418
|
+
hasSettings: !!translationSettings2,
|
|
1419
|
+
hasGlobalPrompt: !!translationSettings2?.globalPrompt,
|
|
1420
|
+
globalPromptLength: translationSettings2?.globalPrompt?.length,
|
|
1421
|
+
localePromptsCount: translationSettings2?.localePrompts?.length,
|
|
1422
|
+
localePrompts: translationSettings2?.localePrompts,
|
|
1423
|
+
protectedPhrasesCount: translationSettings2?.protectedPhrases?.length,
|
|
1424
|
+
targetLanguages
|
|
1425
|
+
});
|
|
1426
|
+
const translationGlossaryTerms = await client.fetch(
|
|
1672
1427
|
`*[
|
|
1673
1428
|
_type == "translationGlossaryTerm"
|
|
1674
1429
|
&& count(translations[_key in $targetLanguages]) > 0
|
|
@@ -1688,9 +1443,21 @@ const fetchTranslationPrompts = async ({
|
|
|
1688
1443
|
)), glossaryTermsByLocale[targetLanguage] = relevantTranslations;
|
|
1689
1444
|
}
|
|
1690
1445
|
let filteredProtectedPhrases = translationSettings2?.protectedPhrases;
|
|
1691
|
-
return sourceDocument && filteredProtectedPhrases && (
|
|
1692
|
-
|
|
1693
|
-
|
|
1446
|
+
return sourceDocument && filteredProtectedPhrases && (devLog.debug("Filtering protected phrases", {
|
|
1447
|
+
totalPhrases: filteredProtectedPhrases.length,
|
|
1448
|
+
phrases: filteredProtectedPhrases,
|
|
1449
|
+
sourceDocumentStringLength: sourceDocumentString.length,
|
|
1450
|
+
sourceDocumentPreview: sourceDocumentString.substring(0, 500)
|
|
1451
|
+
}), filteredProtectedPhrases = filteredProtectedPhrases.filter((phrase) => {
|
|
1452
|
+
const appears = protectedPhraseAppearsInDocument(phrase, sourceDocumentString);
|
|
1453
|
+
return devLog.debug("Protected phrase match result", {
|
|
1454
|
+
phrase,
|
|
1455
|
+
appears
|
|
1456
|
+
}), appears;
|
|
1457
|
+
}), devLog.debug("Filtered protected phrases result", {
|
|
1458
|
+
remainingCount: filteredProtectedPhrases.length,
|
|
1459
|
+
remainingPhrases: filteredProtectedPhrases
|
|
1460
|
+
})), {
|
|
1694
1461
|
globalPrompt: translationSettings2?.globalPrompt,
|
|
1695
1462
|
protectedPhrases: filteredProtectedPhrases,
|
|
1696
1463
|
localeInstructions: Object.fromEntries(
|
|
@@ -1755,7 +1522,12 @@ const fetchTranslationPrompts = async ({
|
|
|
1755
1522
|
}
|
|
1756
1523
|
};
|
|
1757
1524
|
return recursiveSearch(obj, []), results;
|
|
1758
|
-
}, localizeReferences = async ({
|
|
1525
|
+
}, localizeReferences = async ({
|
|
1526
|
+
client,
|
|
1527
|
+
documentId,
|
|
1528
|
+
sourceLanguage,
|
|
1529
|
+
languageFieldName
|
|
1530
|
+
}) => {
|
|
1759
1531
|
try {
|
|
1760
1532
|
const doc = await client.fetch("*[_id == $documentId][0]", { documentId });
|
|
1761
1533
|
if (!doc) {
|
|
@@ -1777,25 +1549,26 @@ const fetchTranslationPrompts = async ({
|
|
|
1777
1549
|
const refs = findKeyValues(doc, "_ref");
|
|
1778
1550
|
if (!refs?.length)
|
|
1779
1551
|
return;
|
|
1780
|
-
await updateReferences(documentId, client, refs, targetLanguage);
|
|
1552
|
+
await updateReferences(documentId, client, refs, sourceLanguage, targetLanguage);
|
|
1781
1553
|
} catch (error) {
|
|
1782
1554
|
devLog.error("Error localizing references", error instanceof Error ? error : new Error(String(error)), {
|
|
1783
1555
|
documentId,
|
|
1784
1556
|
component: "localizeReferences"
|
|
1785
1557
|
});
|
|
1786
1558
|
}
|
|
1787
|
-
}, updateReferences = async (documentId, client, refs, targetLanguage) => {
|
|
1559
|
+
}, updateReferences = async (documentId, client, refs, sourceLanguage, targetLanguage) => {
|
|
1788
1560
|
if (!refs?.length)
|
|
1789
1561
|
return [];
|
|
1790
|
-
const refIds = refs.map((r) =>
|
|
1791
|
-
`*[_type == $translationMetadataSchemaType &&
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1562
|
+
const refIds = refs.map((r) => getPublishedId(r.value)), translations = await client.fetch(
|
|
1563
|
+
`*[_type == $translationMetadataSchemaType && array::intersects(translations[].value._ref, $refIds)] {
|
|
1564
|
+
"sourceId": translations[_key == $sourceLanguage][0].value._ref,
|
|
1565
|
+
"targetId": translations[_key == $targetLanguage][0].value._ref,
|
|
1566
|
+
"targetType": translations[_key == $sourceLanguage][0].value -> _type
|
|
1567
|
+
}`,
|
|
1796
1568
|
{
|
|
1797
1569
|
translationMetadataSchemaType: TRANSLATION_METADATA_TYPE,
|
|
1798
1570
|
refIds,
|
|
1571
|
+
sourceLanguage,
|
|
1799
1572
|
targetLanguage
|
|
1800
1573
|
}
|
|
1801
1574
|
), translationMap = /* @__PURE__ */ new Map();
|
|
@@ -1804,11 +1577,10 @@ const fetchTranslationPrompts = async ({
|
|
|
1804
1577
|
const patch = client.patch(documentId);
|
|
1805
1578
|
let hasChanges = !1;
|
|
1806
1579
|
for (const ref of refs) {
|
|
1807
|
-
const sourceId =
|
|
1808
|
-
newId && (patch.set({ [pathToString(ref.path)]:
|
|
1580
|
+
const sourceId = getPublishedId(ref.value), newId = translationMap.get(sourceId);
|
|
1581
|
+
newId && (patch.set({ [pathToString(ref.path)]: getPublishedId(newId) }), patch.set({
|
|
1809
1582
|
[`${pathToString(ref.path.slice(0, -1))}._strengthenOnPublish`]: {
|
|
1810
|
-
|
|
1811
|
-
_weak: !1
|
|
1583
|
+
type: translations.find((t) => t.targetId === getPublishedId(newId))?.targetType || void 0
|
|
1812
1584
|
}
|
|
1813
1585
|
}), hasChanges = !0);
|
|
1814
1586
|
}
|
|
@@ -1833,7 +1605,111 @@ const fetchTranslationPrompts = async ({
|
|
|
1833
1605
|
payload
|
|
1834
1606
|
});
|
|
1835
1607
|
}
|
|
1836
|
-
},
|
|
1608
|
+
}, DEFAULT_OPTIONS = {
|
|
1609
|
+
maxRetries: 3,
|
|
1610
|
+
initialDelayMs: 1e3,
|
|
1611
|
+
maxDelayMs: 3e4,
|
|
1612
|
+
backoffMultiplier: 2,
|
|
1613
|
+
jitter: !0
|
|
1614
|
+
};
|
|
1615
|
+
function isRetryableError(error) {
|
|
1616
|
+
if (error instanceof Error) {
|
|
1617
|
+
const message = error.message.toLowerCase(), name = error.name.toLowerCase(), nonRetryablePatterns = [
|
|
1618
|
+
"schema not found",
|
|
1619
|
+
"invalid schema",
|
|
1620
|
+
"not found",
|
|
1621
|
+
"unauthorized",
|
|
1622
|
+
"forbidden",
|
|
1623
|
+
"invalid api key",
|
|
1624
|
+
"missing api key",
|
|
1625
|
+
"validation failed",
|
|
1626
|
+
"invalid document",
|
|
1627
|
+
"source language not found",
|
|
1628
|
+
"invalid configuration",
|
|
1629
|
+
"type mismatch"
|
|
1630
|
+
];
|
|
1631
|
+
for (const pattern of nonRetryablePatterns)
|
|
1632
|
+
if (message.includes(pattern) || name.includes(pattern))
|
|
1633
|
+
return !1;
|
|
1634
|
+
const retryablePatterns = [
|
|
1635
|
+
"failed to fetch",
|
|
1636
|
+
"network",
|
|
1637
|
+
"timeout",
|
|
1638
|
+
"econnreset",
|
|
1639
|
+
"econnrefused",
|
|
1640
|
+
"enotfound",
|
|
1641
|
+
"socket",
|
|
1642
|
+
"rate limit",
|
|
1643
|
+
"too many requests",
|
|
1644
|
+
"429",
|
|
1645
|
+
"500",
|
|
1646
|
+
"502",
|
|
1647
|
+
"503",
|
|
1648
|
+
"504",
|
|
1649
|
+
"internal server error",
|
|
1650
|
+
"bad gateway",
|
|
1651
|
+
"service unavailable",
|
|
1652
|
+
"gateway timeout",
|
|
1653
|
+
"temporarily unavailable",
|
|
1654
|
+
"try again"
|
|
1655
|
+
];
|
|
1656
|
+
for (const pattern of retryablePatterns)
|
|
1657
|
+
if (message.includes(pattern) || name.includes(pattern))
|
|
1658
|
+
return !0;
|
|
1659
|
+
}
|
|
1660
|
+
if (typeof error == "object" && error !== null) {
|
|
1661
|
+
const statusCode = error.statusCode || error.status;
|
|
1662
|
+
if (statusCode) {
|
|
1663
|
+
if (statusCode === 429 || statusCode >= 500 && statusCode < 600)
|
|
1664
|
+
return !0;
|
|
1665
|
+
if (statusCode >= 400 && statusCode < 500)
|
|
1666
|
+
return !1;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return !0;
|
|
1670
|
+
}
|
|
1671
|
+
function calculateBackoffDelay(attempt, options) {
|
|
1672
|
+
const { initialDelayMs, maxDelayMs, backoffMultiplier, jitter } = options;
|
|
1673
|
+
let delay = initialDelayMs * backoffMultiplier ** attempt;
|
|
1674
|
+
if (delay = Math.min(delay, maxDelayMs), jitter) {
|
|
1675
|
+
const jitterFactor = 0.75 + Math.random() * 0.5;
|
|
1676
|
+
delay = Math.round(delay * jitterFactor);
|
|
1677
|
+
}
|
|
1678
|
+
return delay;
|
|
1679
|
+
}
|
|
1680
|
+
function sleep(ms) {
|
|
1681
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1682
|
+
}
|
|
1683
|
+
async function withRetry(fn, options = {}) {
|
|
1684
|
+
const opts = { ...DEFAULT_OPTIONS, ...options }, shouldRetry = opts.isRetryable ?? isRetryableError;
|
|
1685
|
+
let lastError, attempts = 0;
|
|
1686
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
1687
|
+
attempts = attempt + 1;
|
|
1688
|
+
try {
|
|
1689
|
+
return { success: !0, result: await fn(), attempts };
|
|
1690
|
+
} catch (error) {
|
|
1691
|
+
if (lastError = error, attempt >= opts.maxRetries) {
|
|
1692
|
+
devLog.debug("Max retries exhausted", { attempt: attempts, error });
|
|
1693
|
+
break;
|
|
1694
|
+
}
|
|
1695
|
+
if (!shouldRetry(error)) {
|
|
1696
|
+
devLog.debug("Error is not retryable", { attempt: attempts, error });
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
const delay = calculateBackoffDelay(attempt, opts);
|
|
1700
|
+
devLog.debug("Scheduling retry", {
|
|
1701
|
+
attempt: attempts,
|
|
1702
|
+
nextAttempt: attempt + 2,
|
|
1703
|
+
delayMs: delay,
|
|
1704
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1705
|
+
}), opts.onRetry && opts.onRetry(attempt + 1, error, delay), await sleep(delay);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return { success: !1, error: lastError, attempts };
|
|
1709
|
+
}
|
|
1710
|
+
const getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
1711
|
+
(translation) => translation._key === targetLanguage
|
|
1712
|
+
)?.value._ref || null, getTranslationMetadata = async ({
|
|
1837
1713
|
client,
|
|
1838
1714
|
sourceDocument,
|
|
1839
1715
|
sourceLanguage
|
|
@@ -1882,7 +1758,7 @@ const fetchTranslationPrompts = async ({
|
|
|
1882
1758
|
_type: "internationalizedArrayReferenceValue",
|
|
1883
1759
|
value: {
|
|
1884
1760
|
_type: "reference",
|
|
1885
|
-
_ref:
|
|
1761
|
+
_ref: getPublishedId(newDocument._id),
|
|
1886
1762
|
_weak: !0,
|
|
1887
1763
|
_strengthenOnPublish: {
|
|
1888
1764
|
_type: "reference",
|
|
@@ -1892,7 +1768,7 @@ const fetchTranslationPrompts = async ({
|
|
|
1892
1768
|
}, newTranslationHistory = {
|
|
1893
1769
|
_key: targetLanguage,
|
|
1894
1770
|
language: targetLanguage,
|
|
1895
|
-
sourceId:
|
|
1771
|
+
sourceId: getPublishedId(sourceDocument._id),
|
|
1896
1772
|
sourceRev: sourceDocument._rev,
|
|
1897
1773
|
sourceLanguage,
|
|
1898
1774
|
sourceSnapshot: JSON.stringify(sourceDocument),
|
|
@@ -1922,69 +1798,377 @@ const fetchTranslationPrompts = async ({
|
|
|
1922
1798
|
status
|
|
1923
1799
|
})
|
|
1924
1800
|
});
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1801
|
+
};
|
|
1802
|
+
function shouldExcludeField(fieldPath, excludeFields) {
|
|
1803
|
+
for (const pattern of excludeFields) {
|
|
1804
|
+
if (fieldPath === pattern)
|
|
1805
|
+
return !0;
|
|
1806
|
+
if (pattern.startsWith("**.")) {
|
|
1807
|
+
const fieldName = pattern.substring(3), segments = fieldPath.split(".");
|
|
1808
|
+
if (segments[segments.length - 1]?.split("[")[0] === fieldName)
|
|
1809
|
+
return !0;
|
|
1810
|
+
} else if (!pattern.includes(".") && !pattern.includes("[")) {
|
|
1811
|
+
const segments = fieldPath.split(".");
|
|
1812
|
+
if (segments[segments.length - 1]?.split("[")[0] === pattern)
|
|
1813
|
+
return !0;
|
|
1814
|
+
} else if (pattern.includes("[*]")) {
|
|
1815
|
+
const regexPattern = pattern.replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/\\\[\*\\\]/g, "\\[\\d+\\]");
|
|
1816
|
+
if (new RegExp(`^${regexPattern}$`).test(fieldPath))
|
|
1817
|
+
return !0;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return !1;
|
|
1821
|
+
}
|
|
1822
|
+
function filterExcludedFields(document, excludeFields = [], currentPath = "") {
|
|
1823
|
+
if (!excludeFields.length)
|
|
1824
|
+
return {
|
|
1825
|
+
filteredDocument: document,
|
|
1826
|
+
excludedValues: {}
|
|
1827
|
+
};
|
|
1828
|
+
const filteredDocument = {}, excludedValues = {};
|
|
1829
|
+
let hasExclusions = !1;
|
|
1830
|
+
for (const [key, value] of Object.entries(document)) {
|
|
1831
|
+
const fieldPath = currentPath ? `${currentPath}.${key}` : key;
|
|
1832
|
+
if (shouldExcludeField(fieldPath, excludeFields)) {
|
|
1833
|
+
excludedValues[fieldPath] = value, hasExclusions = !0, devLog.debug("Excluding field from translation", {
|
|
1834
|
+
fieldPath,
|
|
1835
|
+
fieldType: typeof value,
|
|
1836
|
+
value: typeof value == "string" ? value : "[object]"
|
|
1837
|
+
});
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
if (value && typeof value == "object" && !Array.isArray(value) && value._type !== "reference") {
|
|
1841
|
+
const nested = filterExcludedFields(value, excludeFields, fieldPath);
|
|
1842
|
+
filteredDocument[key] = nested.filteredDocument, Object.assign(excludedValues, nested.excludedValues), Object.keys(nested.excludedValues).length > 0 && (hasExclusions = !0);
|
|
1843
|
+
} else if (Array.isArray(value)) {
|
|
1844
|
+
const filteredArray = [];
|
|
1845
|
+
let arrayHasExclusions = !1;
|
|
1846
|
+
for (let i = 0; i < value.length; i++) {
|
|
1847
|
+
const arrayItem = value[i], itemPath = `${fieldPath}[${i}]`;
|
|
1848
|
+
if (arrayItem && typeof arrayItem == "object" && !Array.isArray(arrayItem) && arrayItem._type !== "reference") {
|
|
1849
|
+
const nested = filterExcludedFields(arrayItem, excludeFields, itemPath);
|
|
1850
|
+
filteredArray[i] = nested.filteredDocument, Object.assign(excludedValues, nested.excludedValues), Object.keys(nested.excludedValues).length > 0 && (arrayHasExclusions = !0);
|
|
1851
|
+
} else
|
|
1852
|
+
filteredArray[i] = arrayItem;
|
|
1853
|
+
}
|
|
1854
|
+
filteredDocument[key] = filteredArray, arrayHasExclusions && (hasExclusions = !0);
|
|
1855
|
+
} else
|
|
1856
|
+
filteredDocument[key] = value;
|
|
1857
|
+
}
|
|
1858
|
+
return hasExclusions && devLog.info("Filtered excluded fields from document", {
|
|
1859
|
+
documentId: document._id || "unknown",
|
|
1860
|
+
excludedFieldCount: Object.keys(excludedValues).length,
|
|
1861
|
+
excludedFields: Object.keys(excludedValues)
|
|
1862
|
+
}), {
|
|
1863
|
+
filteredDocument,
|
|
1864
|
+
excludedValues
|
|
1950
1865
|
};
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1866
|
+
}
|
|
1867
|
+
function restoreExcludedFields(translatedDocument, excludedValues) {
|
|
1868
|
+
if (!Object.keys(excludedValues).length)
|
|
1869
|
+
return translatedDocument;
|
|
1870
|
+
const result = { ...translatedDocument };
|
|
1871
|
+
for (const [fieldPath, value] of Object.entries(excludedValues))
|
|
1872
|
+
setNestedValue(result, fieldPath, value);
|
|
1873
|
+
return devLog.debug("Restored excluded fields to translated document", {
|
|
1874
|
+
documentId: result._id || "unknown",
|
|
1875
|
+
restoredFieldCount: Object.keys(excludedValues).length,
|
|
1876
|
+
restoredFields: Object.keys(excludedValues)
|
|
1877
|
+
}), result;
|
|
1878
|
+
}
|
|
1879
|
+
function setNestedValue(obj, path, value) {
|
|
1880
|
+
const segments = path.split(".");
|
|
1881
|
+
let current = obj;
|
|
1882
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
1883
|
+
const segment = segments[i], arrayMatch2 = segment.match(/^(.+)\[(\d+)\]$/);
|
|
1884
|
+
if (arrayMatch2) {
|
|
1885
|
+
const [, fieldName, indexStr] = arrayMatch2, index = Number.parseInt(indexStr, 10);
|
|
1886
|
+
for ((!(fieldName in current) || !Array.isArray(current[fieldName])) && (current[fieldName] = []); current[fieldName].length <= index; )
|
|
1887
|
+
current[fieldName].push({});
|
|
1888
|
+
current = current[fieldName][index];
|
|
1889
|
+
} else
|
|
1890
|
+
(!(segment in current) || typeof current[segment] != "object") && (current[segment] = {}), current = current[segment];
|
|
1891
|
+
}
|
|
1892
|
+
const finalSegment = segments[segments.length - 1], arrayMatch = finalSegment.match(/^(.+)\[(\d+)\]$/);
|
|
1893
|
+
if (arrayMatch) {
|
|
1894
|
+
const [, fieldName, indexStr] = arrayMatch, index = Number.parseInt(indexStr, 10);
|
|
1895
|
+
for ((!(fieldName in current) || !Array.isArray(current[fieldName])) && (current[fieldName] = []); current[fieldName].length <= index; )
|
|
1896
|
+
current[fieldName].push(null);
|
|
1897
|
+
current[fieldName][index] = value;
|
|
1898
|
+
} else
|
|
1899
|
+
current[finalSegment] = value;
|
|
1900
|
+
}
|
|
1901
|
+
const COMMON_ENUM_FIELDS = [
|
|
1902
|
+
"status",
|
|
1903
|
+
"state",
|
|
1904
|
+
"type",
|
|
1905
|
+
"category",
|
|
1906
|
+
"priority",
|
|
1907
|
+
"visibility",
|
|
1908
|
+
"role",
|
|
1909
|
+
"permission",
|
|
1910
|
+
"level",
|
|
1911
|
+
"mode",
|
|
1912
|
+
"format",
|
|
1913
|
+
"variant",
|
|
1914
|
+
"size",
|
|
1915
|
+
"theme",
|
|
1916
|
+
"kind",
|
|
1917
|
+
"phase",
|
|
1918
|
+
"stage"
|
|
1919
|
+
], PREDEFINED_EXCLUSIONS = {
|
|
1920
|
+
/** Common enum and status fields */
|
|
1921
|
+
enums: [...COMMON_ENUM_FIELDS],
|
|
1922
|
+
/** All metadata and system fields (starting with _) */
|
|
1923
|
+
metadata: ["_id", "_type", "_rev", "_createdAt", "_updatedAt"],
|
|
1924
|
+
/** Common technical and configuration fields */
|
|
1925
|
+
technical: [
|
|
1926
|
+
"slug",
|
|
1927
|
+
"handle",
|
|
1928
|
+
"key",
|
|
1929
|
+
"code",
|
|
1930
|
+
"id",
|
|
1931
|
+
"uuid",
|
|
1932
|
+
"token",
|
|
1933
|
+
"hash",
|
|
1934
|
+
"checksum",
|
|
1935
|
+
"version",
|
|
1936
|
+
"config",
|
|
1937
|
+
"settings"
|
|
1938
|
+
],
|
|
1939
|
+
/** SEO and URL-related fields that often shouldn't be translated */
|
|
1940
|
+
seo: ["slug", "handle", "permalink", "canonical", "redirect", "robots", "sitemap"]
|
|
1941
|
+
};
|
|
1942
|
+
function applyTransforms({
|
|
1943
|
+
document,
|
|
1944
|
+
registry,
|
|
1945
|
+
schemaType,
|
|
1946
|
+
parentPath = ""
|
|
1947
|
+
}) {
|
|
1948
|
+
if (!document)
|
|
1949
|
+
return { transformedDocument: document, originalStructure: {} };
|
|
1950
|
+
if (typeof document != "object")
|
|
1951
|
+
return { transformedDocument: document, originalStructure: {} };
|
|
1952
|
+
if (Array.isArray(document)) {
|
|
1953
|
+
const originalStructure2 = {};
|
|
1954
|
+
return { transformedDocument: document.map((item, index) => {
|
|
1955
|
+
const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`, { transformedDocument: transformedItem, originalStructure: itemOriginalStructure } = applyTransforms({
|
|
1956
|
+
document: item,
|
|
1957
|
+
registry,
|
|
1958
|
+
schemaType,
|
|
1959
|
+
parentPath: itemPath
|
|
1960
|
+
});
|
|
1961
|
+
return Object.assign(originalStructure2, itemOriginalStructure), transformedItem;
|
|
1962
|
+
}), originalStructure: originalStructure2 };
|
|
1963
|
+
}
|
|
1964
|
+
const transformedObj = {}, originalStructure = {};
|
|
1965
|
+
for (const [key, value] of Object.entries(document)) {
|
|
1966
|
+
const fieldPath = parentPath ? `${parentPath}.${key}` : key;
|
|
1967
|
+
if (key.startsWith("_") && key !== "_type") {
|
|
1968
|
+
transformedObj[key] = value;
|
|
1969
|
+
continue;
|
|
1970
|
+
}
|
|
1971
|
+
const transform = registry.findTransform(schemaType, fieldPath, value, document);
|
|
1972
|
+
if (transform) {
|
|
1973
|
+
devLog.debug("Applying transform", { fieldPath, schemaType });
|
|
1974
|
+
const context = {
|
|
1975
|
+
fieldPath,
|
|
1976
|
+
schemaType,
|
|
1977
|
+
document
|
|
1978
|
+
}, transformedValue = transform.toTranslatable(value, context);
|
|
1979
|
+
transformedObj[key] = transformedValue, originalStructure[fieldPath] = {
|
|
1980
|
+
value,
|
|
1981
|
+
transform
|
|
1982
|
+
};
|
|
1983
|
+
} else {
|
|
1984
|
+
const { transformedDocument: transformedValue, originalStructure: valueOriginalStructure } = applyTransforms({
|
|
1985
|
+
document: value,
|
|
1986
|
+
registry,
|
|
1987
|
+
schemaType,
|
|
1988
|
+
parentPath: fieldPath
|
|
1989
|
+
});
|
|
1990
|
+
Object.assign(originalStructure, valueOriginalStructure), transformedObj[key] = transformedValue;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
return { transformedDocument: transformedObj, originalStructure };
|
|
1994
|
+
}
|
|
1995
|
+
function restoreTransforms({
|
|
1996
|
+
translatedDocument,
|
|
1997
|
+
originalStructure,
|
|
1998
|
+
registry,
|
|
1999
|
+
schemaType,
|
|
2000
|
+
parentPath = ""
|
|
2001
|
+
}) {
|
|
2002
|
+
if (translatedDocument == null || typeof translatedDocument != "object")
|
|
2003
|
+
return translatedDocument;
|
|
2004
|
+
if (Array.isArray(translatedDocument))
|
|
2005
|
+
return translatedDocument.map((item, index) => {
|
|
2006
|
+
const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`;
|
|
2007
|
+
return restoreTransforms({
|
|
2008
|
+
translatedDocument: item,
|
|
2009
|
+
originalStructure,
|
|
2010
|
+
registry,
|
|
2011
|
+
schemaType,
|
|
2012
|
+
parentPath: itemPath
|
|
2013
|
+
});
|
|
2014
|
+
});
|
|
2015
|
+
const originalData = originalStructure[parentPath];
|
|
2016
|
+
if (originalData) {
|
|
2017
|
+
const { value: originalValue, transform } = originalData, context = {
|
|
2018
|
+
fieldPath: parentPath,
|
|
2019
|
+
schemaType,
|
|
2020
|
+
document: translatedDocument
|
|
2021
|
+
};
|
|
2022
|
+
return devLog.debug("Restoring transform", { fieldPath: parentPath, schemaType }), transform.fromTranslatable(translatedDocument, originalValue, context);
|
|
2023
|
+
}
|
|
2024
|
+
const restoredObj = {}, translatedObj = translatedDocument;
|
|
2025
|
+
for (const [key, value] of Object.entries(translatedObj)) {
|
|
2026
|
+
const fieldPath = parentPath ? `${parentPath}.${key}` : key, fieldOriginalData = originalStructure[fieldPath];
|
|
2027
|
+
if (fieldOriginalData) {
|
|
2028
|
+
const { value: originalValue, transform } = fieldOriginalData, context = {
|
|
2029
|
+
fieldPath,
|
|
2030
|
+
schemaType,
|
|
2031
|
+
document: translatedDocument
|
|
2032
|
+
};
|
|
2033
|
+
devLog.debug("Restoring transform for field", { fieldPath, schemaType }), restoredObj[key] = transform.fromTranslatable(value, originalValue, context);
|
|
2034
|
+
} else
|
|
2035
|
+
restoredObj[key] = restoreTransforms({
|
|
2036
|
+
translatedDocument: value,
|
|
2037
|
+
originalStructure,
|
|
2038
|
+
registry,
|
|
2039
|
+
schemaType,
|
|
2040
|
+
parentPath: fieldPath
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
return restoredObj;
|
|
2044
|
+
}
|
|
2045
|
+
class TransformRegistry {
|
|
2046
|
+
transforms = [];
|
|
2047
|
+
/**
|
|
2048
|
+
* Register a transform with the registry.
|
|
2049
|
+
* @param transform - Field transform to register
|
|
2050
|
+
*/
|
|
2051
|
+
register(transform) {
|
|
2052
|
+
devLog.debug("Registering field transform", {
|
|
2053
|
+
matcher: transform.matcher
|
|
2054
|
+
}), this.transforms.push(transform);
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Find a matching transform for the given field.
|
|
2058
|
+
* If multiple transforms match, returns the first one (order matters!).
|
|
2059
|
+
* @param schemaType - Document schema type
|
|
2060
|
+
* @param fieldPath - Path to the field
|
|
2061
|
+
* @param value - Optional field value for value-based matching
|
|
2062
|
+
* @param document - Optional full document for context
|
|
2063
|
+
* @returns Matching transform or undefined if none found
|
|
2064
|
+
*/
|
|
2065
|
+
findTransform(schemaType, fieldPath, value, document) {
|
|
2066
|
+
const matches = [];
|
|
2067
|
+
for (const transform of this.transforms) {
|
|
2068
|
+
const { matcher } = transform;
|
|
2069
|
+
if (!(matcher.schemaType && matcher.schemaType !== schemaType)) {
|
|
2070
|
+
if (matcher.fieldPath) {
|
|
2071
|
+
if (typeof matcher.fieldPath == "string") {
|
|
2072
|
+
if (matcher.fieldPath !== fieldPath)
|
|
2073
|
+
continue;
|
|
2074
|
+
} else if (matcher.fieldPath instanceof RegExp && !matcher.fieldPath.test(fieldPath))
|
|
2075
|
+
continue;
|
|
2076
|
+
}
|
|
2077
|
+
if (matcher.fieldName) {
|
|
2078
|
+
const segments = fieldPath.split(".");
|
|
2079
|
+
if (segments[segments.length - 1]?.split("[")[0] !== matcher.fieldName)
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2082
|
+
if (matcher.valueMatcher) {
|
|
2083
|
+
if (value === void 0)
|
|
2084
|
+
continue;
|
|
2085
|
+
const context = {
|
|
2086
|
+
fieldPath,
|
|
2087
|
+
schemaType,
|
|
2088
|
+
document: document || {}
|
|
2089
|
+
};
|
|
2090
|
+
if (!matcher.valueMatcher(value, context))
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
matches.push(transform);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
return matches.length > 1 && devLog.warn("Multiple transforms match the same field - using first match", {
|
|
2097
|
+
schemaType,
|
|
2098
|
+
fieldPath,
|
|
2099
|
+
matchCount: matches.length,
|
|
2100
|
+
firstMatcher: matches[0].matcher
|
|
2101
|
+
}), matches[0];
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
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) => {
|
|
2105
|
+
const model = request.model || "gpt-5", requestBody = {
|
|
2106
|
+
model: model === "gpt-5" ? "gpt-5.1" : model,
|
|
2107
|
+
// Map gpt-5 to gpt-5.1, use others as-is
|
|
2108
|
+
input: request.messages
|
|
2109
|
+
// Uses 'input' instead of 'messages'
|
|
1962
2110
|
};
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
})
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2111
|
+
request.reasoningEffort && (requestBody.reasoning = {
|
|
2112
|
+
effort: request.reasoningEffort,
|
|
2113
|
+
summary: "auto"
|
|
2114
|
+
// Enable automatic reasoning summaries
|
|
2115
|
+
}), devLog.debug("OpenAI GPT-5 Request", {
|
|
2116
|
+
model: requestBody.model,
|
|
2117
|
+
reasoningEffort: request.reasoningEffort,
|
|
2118
|
+
messages: request.messages.map((m) => ({
|
|
2119
|
+
role: m.role,
|
|
2120
|
+
contentLength: m.content.length,
|
|
2121
|
+
contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
|
|
2122
|
+
}))
|
|
2123
|
+
}), devLog.debug("OpenAI GPT-5 Full Prompt", {
|
|
2124
|
+
messages: request.messages
|
|
2125
|
+
});
|
|
2126
|
+
const response = await fetch("https://api.openai.com/v1/responses", {
|
|
2127
|
+
method: "POST",
|
|
2128
|
+
headers: {
|
|
2129
|
+
"Content-Type": "application/json",
|
|
2130
|
+
Authorization: `Bearer ${apiKey}`
|
|
1975
2131
|
},
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2132
|
+
body: JSON.stringify(requestBody)
|
|
2133
|
+
});
|
|
2134
|
+
if (!response.ok) {
|
|
2135
|
+
const error = await response.json();
|
|
2136
|
+
throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
|
|
2137
|
+
}
|
|
2138
|
+
const result = await response.json(), tokensUsed = {
|
|
2139
|
+
prompt: result.usage?.input_tokens || 0,
|
|
2140
|
+
completion: result.usage?.output_tokens || 0,
|
|
2141
|
+
reasoning: result.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
2142
|
+
total: result.usage?.total_tokens || 0,
|
|
2143
|
+
cached: result.usage?.input_tokens_details?.cached_tokens || 0
|
|
2144
|
+
}, cost = calculateCost(tokensUsed, model);
|
|
2145
|
+
let content = "";
|
|
2146
|
+
if (result.output && Array.isArray(result.output)) {
|
|
2147
|
+
const messageItem = result.output.find((item) => item.type === "message");
|
|
2148
|
+
messageItem?.content && Array.isArray(messageItem.content) && (content = messageItem.content.find((item) => item.type === "output_text")?.text || "");
|
|
2149
|
+
}
|
|
2150
|
+
return content || (content = result.output_text || result.text || ""), {
|
|
2151
|
+
content,
|
|
2152
|
+
tokensUsed,
|
|
2153
|
+
cost
|
|
1980
2154
|
};
|
|
1981
|
-
},
|
|
1982
|
-
const model = request.model || "gpt-
|
|
2155
|
+
}, callChatCompletionsAPI = async (apiKey, request) => {
|
|
2156
|
+
const model = request.model || "gpt-4o", requestBody = {
|
|
1983
2157
|
model,
|
|
1984
2158
|
messages: request.messages,
|
|
1985
|
-
|
|
2159
|
+
temperature: request.temperature ?? 0.3
|
|
1986
2160
|
};
|
|
1987
|
-
|
|
2161
|
+
devLog.debug("OpenAI Chat Completions Request", {
|
|
2162
|
+
model,
|
|
2163
|
+
temperature: request.temperature ?? 0.3,
|
|
2164
|
+
messages: request.messages.map((m) => ({
|
|
2165
|
+
role: m.role,
|
|
2166
|
+
contentLength: m.content.length,
|
|
2167
|
+
contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
|
|
2168
|
+
}))
|
|
2169
|
+
}), devLog.debug("OpenAI Chat Completions Full Prompt", {
|
|
2170
|
+
messages: request.messages
|
|
2171
|
+
});
|
|
1988
2172
|
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1989
2173
|
method: "POST",
|
|
1990
2174
|
headers: {
|
|
@@ -1995,110 +2179,116 @@ const fetchTranslationPrompts = async ({
|
|
|
1995
2179
|
});
|
|
1996
2180
|
if (!response.ok) {
|
|
1997
2181
|
const error = await response.json();
|
|
1998
|
-
throw new Error(`
|
|
2182
|
+
throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
|
|
1999
2183
|
}
|
|
2000
|
-
if (useStreaming && response.body && request.onReasoningChunk)
|
|
2001
|
-
return await handleStreamingResponse(response, request.onReasoningChunk, model);
|
|
2002
2184
|
const result = await response.json(), usage = result.usage || {}, tokensUsed = {
|
|
2003
2185
|
prompt: usage.prompt_tokens || 0,
|
|
2004
2186
|
completion: usage.completion_tokens || 0,
|
|
2005
2187
|
reasoning: usage.reasoning_tokens || 0,
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
}, cost =
|
|
2009
|
-
let reasoningSummary, actualContent = messageContent;
|
|
2010
|
-
const reasoningMatch = messageContent.match(/^<reasoning>([\s\S]*?)<\/reasoning>\n\n([\s\S]*)$/i);
|
|
2011
|
-
return reasoningMatch && (reasoningSummary = reasoningMatch[1].trim(), actualContent = reasoningMatch[2]), {
|
|
2012
|
-
content: actualContent,
|
|
2013
|
-
tokensUsed,
|
|
2014
|
-
cost,
|
|
2015
|
-
reasoningSummary
|
|
2016
|
-
};
|
|
2017
|
-
}, handleStreamingResponse = async (response, onReasoningChunk, model) => {
|
|
2018
|
-
if (!response.body)
|
|
2019
|
-
throw new Error("Response body is null");
|
|
2020
|
-
const reader = response.body.getReader(), decoder = new TextDecoder();
|
|
2021
|
-
let fullContent = "", inReasoning = !1, reasoningBuffer = "", contentBuffer = "", tokensUsed = { prompt: 0, completion: 0, reasoning: 0, total: 0 };
|
|
2022
|
-
try {
|
|
2023
|
-
for (; ; ) {
|
|
2024
|
-
const { done, value } = await reader.read();
|
|
2025
|
-
if (done) break;
|
|
2026
|
-
const lines = decoder.decode(value, { stream: !0 }).split(`
|
|
2027
|
-
`).filter((line) => line.trim() !== "");
|
|
2028
|
-
for (const line of lines)
|
|
2029
|
-
if (line.startsWith("data: ")) {
|
|
2030
|
-
const data = line.slice(6);
|
|
2031
|
-
if (data === "[DONE]") continue;
|
|
2032
|
-
try {
|
|
2033
|
-
const parsed = JSON.parse(data);
|
|
2034
|
-
parsed.usage && (tokensUsed = {
|
|
2035
|
-
prompt: parsed.usage.prompt_tokens || 0,
|
|
2036
|
-
completion: parsed.usage.completion_tokens || 0,
|
|
2037
|
-
reasoning: parsed.usage.reasoning_tokens || 0,
|
|
2038
|
-
total: parsed.usage.total_tokens || 0
|
|
2039
|
-
});
|
|
2040
|
-
const delta = parsed.choices?.[0]?.delta?.content;
|
|
2041
|
-
if (delta)
|
|
2042
|
-
if (fullContent += delta, delta.includes("<reasoning>")) {
|
|
2043
|
-
inReasoning = !0;
|
|
2044
|
-
const parts = delta.split("<reasoning>");
|
|
2045
|
-
reasoningBuffer += parts[1] || "";
|
|
2046
|
-
} else if (delta.includes("</reasoning>")) {
|
|
2047
|
-
inReasoning = !1;
|
|
2048
|
-
const parts = delta.split("</reasoning>");
|
|
2049
|
-
reasoningBuffer += parts[0] || "", contentBuffer += parts[1] || "";
|
|
2050
|
-
} else inReasoning ? (reasoningBuffer += delta, onReasoningChunk(reasoningBuffer)) : contentBuffer += delta;
|
|
2051
|
-
} catch {
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
} finally {
|
|
2056
|
-
reader.releaseLock();
|
|
2057
|
-
}
|
|
2058
|
-
const cost = calculateGPT5Cost(tokensUsed, model);
|
|
2188
|
+
total: usage.total_tokens || 0,
|
|
2189
|
+
cached: usage.prompt_tokens_details?.cached_tokens || 0
|
|
2190
|
+
}, cost = calculateCost(tokensUsed, model);
|
|
2059
2191
|
return {
|
|
2060
|
-
content:
|
|
2192
|
+
content: result.choices[0].message.content || "",
|
|
2061
2193
|
tokensUsed,
|
|
2062
|
-
cost
|
|
2063
|
-
reasoningSummary: reasoningBuffer || void 0
|
|
2194
|
+
cost
|
|
2064
2195
|
};
|
|
2065
|
-
},
|
|
2196
|
+
}, calculateCost = (tokens, model) => {
|
|
2066
2197
|
const pricing = {
|
|
2067
2198
|
"gpt-5": {
|
|
2068
|
-
input:
|
|
2069
|
-
// $0.002 per 1k input tokens
|
|
2199
|
+
input: 125e-8,
|
|
2070
2200
|
output: 1e-5,
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2201
|
+
reasoning: 0
|
|
2202
|
+
},
|
|
2203
|
+
"gpt-5.1": {
|
|
2204
|
+
input: 125e-8,
|
|
2205
|
+
output: 1e-5,
|
|
2206
|
+
reasoning: 0
|
|
2207
|
+
},
|
|
2208
|
+
"gpt-5-mini": {
|
|
2209
|
+
input: 25e-8,
|
|
2210
|
+
output: 2e-6,
|
|
2211
|
+
reasoning: 0
|
|
2212
|
+
},
|
|
2213
|
+
"gpt-5-nano": {
|
|
2214
|
+
input: 50000000000000004e-24,
|
|
2215
|
+
output: 40000000000000003e-23,
|
|
2216
|
+
reasoning: 0
|
|
2217
|
+
},
|
|
2218
|
+
"gpt-5-pro": {
|
|
2219
|
+
input: 15e-6,
|
|
2220
|
+
output: 12e-5,
|
|
2221
|
+
reasoning: 0
|
|
2074
2222
|
},
|
|
2075
2223
|
"gpt-4o": {
|
|
2076
|
-
input:
|
|
2077
|
-
output:
|
|
2224
|
+
input: 25e-7,
|
|
2225
|
+
output: 1e-5,
|
|
2078
2226
|
reasoning: 0
|
|
2079
|
-
// No reasoning tokens in GPT-4
|
|
2080
2227
|
},
|
|
2081
2228
|
"gpt-4-turbo": {
|
|
2082
2229
|
input: 1e-5,
|
|
2083
|
-
output:
|
|
2230
|
+
output: 3e-5,
|
|
2084
2231
|
reasoning: 0
|
|
2085
2232
|
},
|
|
2086
2233
|
"gpt-4": {
|
|
2087
|
-
input:
|
|
2088
|
-
output:
|
|
2234
|
+
input: 3e-5,
|
|
2235
|
+
output: 6e-5,
|
|
2089
2236
|
reasoning: 0
|
|
2090
2237
|
}
|
|
2091
|
-
}, modelPricing = pricing[model] || pricing["gpt-5"];
|
|
2092
|
-
return tokens.prompt * modelPricing.input + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
|
|
2238
|
+
}, modelPricing = pricing[model] || pricing["gpt-5"], cachedInputRate = model === "gpt-5.1" ? 0.125 / 1e6 : modelPricing.input;
|
|
2239
|
+
return Math.max(0, tokens.prompt - tokens.cached) * modelPricing.input + tokens.cached * cachedInputRate + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
|
|
2093
2240
|
}, deserializeDocument = (originalDoc, reviewedText) => {
|
|
2094
2241
|
const parsed = parseReviewFormat(reviewedText), result = cloneDeep(originalDoc);
|
|
2095
2242
|
return applyTextChanges(result, parsed), result;
|
|
2096
2243
|
}, parseReviewFormat = (text) => {
|
|
2097
2244
|
const root = [], stack = [], lines = text.split(`
|
|
2098
2245
|
`);
|
|
2099
|
-
for (
|
|
2100
|
-
const trimmed =
|
|
2246
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2247
|
+
const trimmed = lines[i].trim();
|
|
2101
2248
|
if (!trimmed) continue;
|
|
2249
|
+
const compactStringMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
2250
|
+
if (compactStringMatch && !trimmed.includes("<")) {
|
|
2251
|
+
const [, fieldName, text2] = compactStringMatch, node = {
|
|
2252
|
+
type: "field",
|
|
2253
|
+
name: fieldName,
|
|
2254
|
+
text: unescapeXml(text2),
|
|
2255
|
+
attributes: { type: "string" },
|
|
2256
|
+
children: []
|
|
2257
|
+
};
|
|
2258
|
+
if (stack.length > 0) {
|
|
2259
|
+
const parent = stack[stack.length - 1];
|
|
2260
|
+
parent && parent.children.push(node);
|
|
2261
|
+
} else
|
|
2262
|
+
root.push(node);
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
const compactStartMatch = trimmed.match(/^---\s+(\w+)\s+---$/);
|
|
2266
|
+
if (compactStartMatch) {
|
|
2267
|
+
const fieldName = compactStartMatch[1], contentLines = [];
|
|
2268
|
+
for (i++; i < lines.length; ) {
|
|
2269
|
+
const currentLine = lines[i];
|
|
2270
|
+
if (currentLine.trim().match(new RegExp(`^---\\s+/${fieldName}\\s+---$`)))
|
|
2271
|
+
break;
|
|
2272
|
+
contentLines.push(currentLine), i++;
|
|
2273
|
+
}
|
|
2274
|
+
const content = contentLines.join(`
|
|
2275
|
+
`);
|
|
2276
|
+
let contentType = "translatable.vtt.captions";
|
|
2277
|
+
(content.includes("<block") || content.includes("<span")) && (contentType = "translatable.richtext");
|
|
2278
|
+
const node = {
|
|
2279
|
+
type: "field",
|
|
2280
|
+
name: fieldName,
|
|
2281
|
+
text: content,
|
|
2282
|
+
attributes: { type: contentType },
|
|
2283
|
+
children: []
|
|
2284
|
+
};
|
|
2285
|
+
if (stack.length > 0) {
|
|
2286
|
+
const parent = stack[stack.length - 1];
|
|
2287
|
+
parent && parent.children.push(node);
|
|
2288
|
+
} else
|
|
2289
|
+
root.push(node);
|
|
2290
|
+
continue;
|
|
2291
|
+
}
|
|
2102
2292
|
const openMatch = trimmed.match(/^<(field|block|span)([^>]*)>(.*)$/);
|
|
2103
2293
|
if (openMatch) {
|
|
2104
2294
|
const [, tagName, attrsStr, content] = openMatch, attributes = parseAttributes(attrsStr), node = {
|
|
@@ -2121,6 +2311,26 @@ const fetchTranslationPrompts = async ({
|
|
|
2121
2311
|
parent && parent.children.push(node);
|
|
2122
2312
|
} else
|
|
2123
2313
|
root.push(node);
|
|
2314
|
+
else if (attributes.type === "translatable.vtt.captions" || attributes.type === "translatable.richtext") {
|
|
2315
|
+
const contentLines = [];
|
|
2316
|
+
for (i++; i < lines.length; ) {
|
|
2317
|
+
const currentLine = lines[i];
|
|
2318
|
+
if (currentLine.trim().match(/^<\/field>$/))
|
|
2319
|
+
break;
|
|
2320
|
+
contentLines.push(currentLine), i++;
|
|
2321
|
+
}
|
|
2322
|
+
if (node.text = contentLines.join(`
|
|
2323
|
+
`), stack.length > 0) {
|
|
2324
|
+
const parent = stack[stack.length - 1];
|
|
2325
|
+
parent && parent.children.push(node);
|
|
2326
|
+
} else
|
|
2327
|
+
root.push(node);
|
|
2328
|
+
} else if (attributes.type === "string" && content && content.includes(closingTag))
|
|
2329
|
+
if (node.text = unescapeXml(content.replace(closingTag, "")), stack.length > 0) {
|
|
2330
|
+
const parent = stack[stack.length - 1];
|
|
2331
|
+
parent && parent.children.push(node);
|
|
2332
|
+
} else
|
|
2333
|
+
root.push(node);
|
|
2124
2334
|
else
|
|
2125
2335
|
stack.push(node);
|
|
2126
2336
|
continue;
|
|
@@ -2149,6 +2359,16 @@ const fetchTranslationPrompts = async ({
|
|
|
2149
2359
|
applyFieldChanges(doc, path, node);
|
|
2150
2360
|
}
|
|
2151
2361
|
}, applyFieldChanges = (doc, path, node) => {
|
|
2362
|
+
if (node.attributes.type === "translatable.vtt.captions") {
|
|
2363
|
+
const fieldValue = get(doc, path);
|
|
2364
|
+
fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.vtt.captions" && node.text && set(doc, [...path, "translationText"], node.text);
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
if (node.attributes.type === "translatable.richtext") {
|
|
2368
|
+
const fieldValue = get(doc, path);
|
|
2369
|
+
fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.richtext" && node.text && set(doc, [...path, "translationText"], node.text);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2152
2372
|
if (node.attributes.type === "array") {
|
|
2153
2373
|
node.children.forEach((child, idx) => {
|
|
2154
2374
|
applyFieldChanges(doc, [...path, idx], child);
|
|
@@ -2167,7 +2387,10 @@ const fetchTranslationPrompts = async ({
|
|
|
2167
2387
|
});
|
|
2168
2388
|
return;
|
|
2169
2389
|
}
|
|
2170
|
-
node.text !== void 0 && !node.attributes.readonly && set(doc, path, node.text)
|
|
2390
|
+
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", {
|
|
2391
|
+
path: path.join("."),
|
|
2392
|
+
attemptedValue: node.text
|
|
2393
|
+
}));
|
|
2171
2394
|
}, applyBlockChanges = (block, blockNode) => {
|
|
2172
2395
|
if (blockNode.attributes._key && block._key !== blockNode.attributes._key) {
|
|
2173
2396
|
devLog.warn("Block key mismatch, skipping", {
|
|
@@ -2198,7 +2421,10 @@ const fetchTranslationPrompts = async ({
|
|
|
2198
2421
|
});
|
|
2199
2422
|
return;
|
|
2200
2423
|
}
|
|
2201
|
-
fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (block[fieldName] = fieldNode.text
|
|
2424
|
+
fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (fieldName in block ? block[fieldName] = fieldNode.text : devLog.debug("Skipping block field not present in original", {
|
|
2425
|
+
fieldName,
|
|
2426
|
+
attemptedValue: fieldNode.text
|
|
2427
|
+
}));
|
|
2202
2428
|
}, applySpanChanges = (span, spanNode) => {
|
|
2203
2429
|
if (spanNode.attributes._key && span._key !== spanNode.attributes._key) {
|
|
2204
2430
|
devLog.warn("Span key mismatch, skipping", {
|
|
@@ -2215,7 +2441,7 @@ const fetchTranslationPrompts = async ({
|
|
|
2215
2441
|
if (path.length === 0) {
|
|
2216
2442
|
if (isPlainObject(value))
|
|
2217
2443
|
for (const [key, val] of Object.entries(value))
|
|
2218
|
-
!key.startsWith("_") && key !== "language" && serialize(val, [key],
|
|
2444
|
+
!key.startsWith("_") && key !== "language" && serialize(val, [key], 0);
|
|
2219
2445
|
return;
|
|
2220
2446
|
}
|
|
2221
2447
|
const fieldName = path[path.length - 1];
|
|
@@ -2228,6 +2454,14 @@ const fetchTranslationPrompts = async ({
|
|
|
2228
2454
|
return;
|
|
2229
2455
|
}
|
|
2230
2456
|
if (isPlainObject(value)) {
|
|
2457
|
+
if (value._type === "translatable.vtt.captions" && typeof value.translationText == "string") {
|
|
2458
|
+
lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
if (value._type === "translatable.richtext" && typeof value.translationText == "string") {
|
|
2462
|
+
lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2231
2465
|
lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
|
|
2232
2466
|
for (const [key, val] of Object.entries(value))
|
|
2233
2467
|
key.startsWith("_") || serialize(val, [...path, key], indent + 1);
|
|
@@ -2235,7 +2469,7 @@ const fetchTranslationPrompts = async ({
|
|
|
2235
2469
|
return;
|
|
2236
2470
|
}
|
|
2237
2471
|
if (typeof value == "string") {
|
|
2238
|
-
value.length > 0 && lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
|
|
2472
|
+
value.length > 0 && lines.push(`${indentStr}<field name="${fieldName}" type="string">${escapeXml(value)}</field>`);
|
|
2239
2473
|
return;
|
|
2240
2474
|
}
|
|
2241
2475
|
(typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
|
|
@@ -2272,7 +2506,7 @@ const fetchTranslationPrompts = async ({
|
|
|
2272
2506
|
return;
|
|
2273
2507
|
}
|
|
2274
2508
|
if (typeof value == "string" && value.length > 0) {
|
|
2275
|
-
lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
|
|
2509
|
+
lines.push(`${indentStr}<field name="${fieldName}" type="string">${escapeXml(value)}</field>`);
|
|
2276
2510
|
return;
|
|
2277
2511
|
}
|
|
2278
2512
|
(typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
|
|
@@ -2283,491 +2517,124 @@ const fetchTranslationPrompts = async ({
|
|
|
2283
2517
|
span._type && (metadata._type = span._type), span._key && (metadata._key = span._key), span.marks && Array.isArray(span.marks) && (metadata.marks = span.marks.join(","));
|
|
2284
2518
|
const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(String(v))}"`).join(" "), text = typeof span.text == "string" ? span.text : "";
|
|
2285
2519
|
metaStr ? lines.push(`${indentStr}<span ${metaStr}>${escapeXml(text)}</span>`) : lines.push(`${indentStr}<span>${escapeXml(text)}</span>`);
|
|
2286
|
-
}, escapeXml = (str) => str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"), isPlainObject = (val) => typeof val == "object" && val !== null && !Array.isArray(val), validateStructureIntegrity = (original, modified) => {
|
|
2287
|
-
const errors = [], criticalFields = ["_id", "_type", "_rev", "_createdAt", "_updatedAt"];
|
|
2288
|
-
for (const field of criticalFields)
|
|
2289
|
-
original[field] !== modified[field] && errors.push({
|
|
2290
|
-
path: [field],
|
|
2291
|
-
expected: original[field],
|
|
2292
|
-
actual: modified[field],
|
|
2293
|
-
type: "metadata_changed"
|
|
2294
|
-
});
|
|
2295
|
-
return validateStructureRecursive(original, modified, [], errors), {
|
|
2296
|
-
valid: errors.length === 0,
|
|
2297
|
-
errors
|
|
2298
|
-
};
|
|
2299
|
-
}, validateStructureRecursive = (original, modified, path, errors) => {
|
|
2300
|
-
if (typeof original != typeof modified) {
|
|
2301
|
-
errors.push({
|
|
2302
|
-
path,
|
|
2303
|
-
expected: typeof original,
|
|
2304
|
-
actual: typeof modified,
|
|
2305
|
-
type: "type_mismatch"
|
|
2306
|
-
});
|
|
2307
|
-
return;
|
|
2308
|
-
}
|
|
2309
|
-
if (Array.isArray(original)) {
|
|
2310
|
-
if (!Array.isArray(modified)) {
|
|
2311
|
-
errors.push({
|
|
2312
|
-
path,
|
|
2313
|
-
expected: "array",
|
|
2314
|
-
actual: typeof modified,
|
|
2315
|
-
type: "type_mismatch"
|
|
2316
|
-
});
|
|
2317
|
-
return;
|
|
2318
|
-
}
|
|
2319
|
-
if (original.length !== modified.length) {
|
|
2320
|
-
errors.push({
|
|
2321
|
-
path,
|
|
2322
|
-
expected: `length ${original.length}`,
|
|
2323
|
-
actual: `length ${modified.length}`,
|
|
2324
|
-
type: "type_mismatch"
|
|
2325
|
-
});
|
|
2326
|
-
return;
|
|
2327
|
-
}
|
|
2328
|
-
original.forEach((item, idx) => {
|
|
2329
|
-
validateStructureRecursive(item, modified[idx], [...path, String(idx)], errors);
|
|
2330
|
-
});
|
|
2331
|
-
return;
|
|
2332
|
-
}
|
|
2333
|
-
if (typeof original == "object" && original !== null && typeof modified == "object" && modified !== null) {
|
|
2334
|
-
const origKeys = Object.keys(original).sort(), modKeys = Object.keys(modified).sort();
|
|
2335
|
-
for (const key of origKeys)
|
|
2336
|
-
key in modified || errors.push({
|
|
2337
|
-
path: [...path, key],
|
|
2338
|
-
expected: "present",
|
|
2339
|
-
actual: "missing",
|
|
2340
|
-
type: "missing"
|
|
2341
|
-
});
|
|
2342
|
-
for (const key of modKeys)
|
|
2343
|
-
key in original || errors.push({
|
|
2344
|
-
path: [...path, key],
|
|
2345
|
-
expected: "absent",
|
|
2346
|
-
actual: "present",
|
|
2347
|
-
type: "extra"
|
|
2348
|
-
});
|
|
2349
|
-
const origObj = original, modObj = modified;
|
|
2350
|
-
for (const key of origKeys)
|
|
2351
|
-
key in modObj && (key.startsWith("_") || key === "marks" || key === "style" || key === "markDefs" ? JSON.stringify(origObj[key]) !== JSON.stringify(modObj[key]) && errors.push({
|
|
2352
|
-
path: [...path, key],
|
|
2353
|
-
expected: origObj[key],
|
|
2354
|
-
actual: modObj[key],
|
|
2355
|
-
type: "metadata_changed"
|
|
2356
|
-
}) : key !== "text" && validateStructureRecursive(origObj[key], modObj[key], [...path, key], errors));
|
|
2357
|
-
}
|
|
2358
|
-
}, assembleTranslationPrompt = (context, components) => {
|
|
2359
|
-
const sections = [
|
|
2360
|
-
{
|
|
2361
|
-
id: "agentDirectives",
|
|
2362
|
-
content: buildAgentDirectivesSection(context),
|
|
2363
|
-
required: !0,
|
|
2364
|
-
priority: 0
|
|
2365
|
-
},
|
|
2366
|
-
{
|
|
2367
|
-
id: "criticalRules",
|
|
2368
|
-
content: buildCriticalRulesSection(context),
|
|
2369
|
-
required: !0,
|
|
2370
|
-
priority: 1
|
|
2371
|
-
}
|
|
2372
|
-
];
|
|
2373
|
-
components.glossaryTerms?.trim() && sections.push({
|
|
2374
|
-
id: "glossaryTerms",
|
|
2375
|
-
content: buildGlossarySection(components.glossaryTerms),
|
|
2376
|
-
required: !1,
|
|
2377
|
-
priority: 3
|
|
2378
|
-
});
|
|
2379
|
-
const styleSection = buildStyleGuideSection(components.globalPrompt, components.localePrompt);
|
|
2380
|
-
styleSection && sections.push({
|
|
2381
|
-
id: "styleGuide",
|
|
2382
|
-
content: styleSection,
|
|
2383
|
-
required: !1,
|
|
2384
|
-
priority: 4
|
|
2385
|
-
}), sections.sort((a, b) => a.priority - b.priority);
|
|
2386
|
-
let styleGuide = "";
|
|
2387
|
-
const omittedSections = [], truncatedSections = [];
|
|
2388
|
-
for (const section of sections) {
|
|
2389
|
-
const prefix = styleGuide.length > 0 ? `
|
|
2390
|
-
|
|
2391
|
-
---
|
|
2392
|
-
|
|
2393
|
-
` : "", availableCharacters = 2e3 - (styleGuide.length + prefix.length);
|
|
2394
|
-
if (availableCharacters <= 0) {
|
|
2395
|
-
if (section.required) {
|
|
2396
|
-
const truncatedContent = truncateSection(section.content, 0);
|
|
2397
|
-
truncatedContent && (styleGuide += prefix + truncatedContent, truncatedSections.push(section.id));
|
|
2398
|
-
} else
|
|
2399
|
-
omittedSections.push(section.id);
|
|
2400
|
-
continue;
|
|
2401
|
-
}
|
|
2402
|
-
if (section.content.length <= availableCharacters) {
|
|
2403
|
-
styleGuide += prefix + section.content;
|
|
2404
|
-
continue;
|
|
2405
|
-
}
|
|
2406
|
-
if (section.required) {
|
|
2407
|
-
const truncatedContent = truncateSection(section.content, availableCharacters);
|
|
2408
|
-
styleGuide += prefix + truncatedContent, truncatedSections.push(section.id);
|
|
2409
|
-
} else
|
|
2410
|
-
omittedSections.push(section.id);
|
|
2411
|
-
}
|
|
2412
|
-
return {
|
|
2413
|
-
styleGuide,
|
|
2414
|
-
omittedSections,
|
|
2415
|
-
truncatedSections
|
|
2416
|
-
};
|
|
2417
|
-
}, truncateSection = (content, limit) => {
|
|
2418
|
-
if (limit <= 0)
|
|
2419
|
-
return "";
|
|
2420
|
-
if (content.length <= limit)
|
|
2421
|
-
return content;
|
|
2422
|
-
if (limit <= 1)
|
|
2423
|
-
return content.slice(0, limit);
|
|
2424
|
-
const ellipsis = "\u2026";
|
|
2425
|
-
if (limit <= ellipsis.length)
|
|
2426
|
-
return content.slice(0, limit);
|
|
2427
|
-
const sliceEnd = limit - ellipsis.length, sliced = content.slice(0, sliceEnd), trimmed = sliced.replace(/\s+\S*$/, "").trimEnd();
|
|
2428
|
-
return `${trimmed.length >= sliceEnd / 2 ? trimmed : sliced}${ellipsis}`;
|
|
2429
|
-
}, buildAgentDirectivesSection = (context) => {
|
|
2430
|
-
const { targetLanguage, sourceLanguage } = context;
|
|
2431
|
-
return `# AGENT WORKFLOW DIRECTIVES
|
|
2432
|
-
|
|
2433
|
-
- Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
|
|
2434
|
-
- Scan each field once, then move directly into translation work.
|
|
2435
|
-
- Plan updates before editing so document structure remains intact.
|
|
2436
|
-
- Default to medium reasoning effort and escalate only for ambiguous passages.
|
|
2437
|
-
- Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`;
|
|
2438
|
-
}, buildCriticalRulesSection = (context) => {
|
|
2439
|
-
const { sourceLanguage, targetLanguage } = context, fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
|
|
2440
|
-
return `# CRITICAL TRANSLATION RULES
|
|
2441
|
-
|
|
2442
|
-
Follow these without exception:
|
|
2443
|
-
|
|
2444
|
-
1. Translate from ${fromLanguage} into ${toLanguage}; every output token must be ${toLanguage} unless flagged as protected.
|
|
2445
|
-
2. Preserve meaning exactly\u2014no additions, deletions, or tone shifts.
|
|
2446
|
-
3. Maintain all layout, markdown, rich-text marks, and data structure.
|
|
2447
|
-
4. Never invent metadata or field values that are absent in the source.
|
|
2448
|
-
5. Leave technical tokens (code, IDs, filenames) untouched.
|
|
2449
|
-
6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
|
|
2450
|
-
}, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
|
|
2451
|
-
|
|
2452
|
-
Apply these mappings immediately after protected phrases and before style preferences.
|
|
2453
|
-
|
|
2454
|
-
${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
|
|
2455
|
-
const components = [];
|
|
2456
|
-
return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
|
|
2457
|
-
"Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
|
|
2458
|
-
), components.push("")), globalPrompt && (components.push("## General Guidelines"), components.push(globalPrompt), components.push("")), localePrompt && (components.push("## Language-Specific Guidelines"), components.push(localePrompt)), components.length > 0 ? components.join(`
|
|
2459
|
-
`) : null;
|
|
2460
|
-
}, translateEntireDocument = async ({
|
|
2461
|
-
dispatch,
|
|
2462
|
-
client,
|
|
2463
|
-
schemaId,
|
|
2464
|
-
sourceDocument,
|
|
2465
|
-
targetLanguage,
|
|
2466
|
-
languageFieldName,
|
|
2467
|
-
translationTarget,
|
|
2468
|
-
translationPrompts,
|
|
2469
|
-
translationMode,
|
|
2470
|
-
openaiConfig,
|
|
2471
|
-
releaseId,
|
|
2472
|
-
isNewTranslation
|
|
2473
|
-
}) => {
|
|
2474
|
-
const logContext = {
|
|
2475
|
-
documentId: sourceDocument._id,
|
|
2476
|
-
targetLanguage,
|
|
2477
|
-
schemaId,
|
|
2478
|
-
operation: translationTarget.operation,
|
|
2479
|
-
targetId: translationTarget._id,
|
|
2480
|
-
translationMode,
|
|
2481
|
-
releaseId,
|
|
2482
|
-
isNewTranslation
|
|
2483
|
-
};
|
|
2484
|
-
devLog.group("translateEntireDocument", logContext), devLog.time("translateEntireDocument");
|
|
2485
|
-
try {
|
|
2486
|
-
let versionedDocumentId, baseDocumentId;
|
|
2487
|
-
if (isNewTranslation) {
|
|
2488
|
-
const { _id, _createdAt, _updatedAt, _rev, ...docWithoutMeta } = sourceDocument, newTranslationDoc = {
|
|
2489
|
-
...docWithoutMeta,
|
|
2490
|
-
_type: sourceDocument._type,
|
|
2491
|
-
[languageFieldName]: targetLanguage
|
|
2492
|
-
};
|
|
2493
|
-
devLog.info("Creating new translation document in release", logContext), baseDocumentId = translationTarget._id || `new-translation-${Date.now()}`, await client.action({
|
|
2494
|
-
actionType: "sanity.action.document.version.create",
|
|
2495
|
-
publishedId: baseDocumentId,
|
|
2496
|
-
document: {
|
|
2497
|
-
...newTranslationDoc,
|
|
2498
|
-
_type: sourceDocument._type,
|
|
2499
|
-
_id: `versions.${releaseId}.${baseDocumentId}`
|
|
2500
|
-
}
|
|
2501
|
-
}), versionedDocumentId = `versions.${releaseId}.${baseDocumentId}`, devLog.info("New translation version created in release", {
|
|
2502
|
-
...logContext,
|
|
2503
|
-
versionedDocumentId,
|
|
2504
|
-
baseDocumentId
|
|
2505
|
-
});
|
|
2506
|
-
} else {
|
|
2507
|
-
const existingTranslationId = translationTarget._id;
|
|
2508
|
-
if (!existingTranslationId)
|
|
2509
|
-
throw new Error("Translation target ID not found for update operation");
|
|
2510
|
-
devLog.info("Creating version of existing translation in release", {
|
|
2511
|
-
...logContext,
|
|
2512
|
-
existingTranslationId
|
|
2513
|
-
});
|
|
2514
|
-
const existingDoc = await client.getDocument(existingTranslationId);
|
|
2515
|
-
if (!existingDoc)
|
|
2516
|
-
throw new Error(`Existing translation document not found: ${existingTranslationId}`);
|
|
2517
|
-
await client.action({
|
|
2518
|
-
actionType: "sanity.action.document.version.create",
|
|
2519
|
-
publishedId: existingTranslationId,
|
|
2520
|
-
document: {
|
|
2521
|
-
...existingDoc,
|
|
2522
|
-
_id: `versions.${releaseId}.${existingTranslationId}`
|
|
2523
|
-
}
|
|
2524
|
-
}), versionedDocumentId = `versions.${releaseId}.${existingTranslationId}`, baseDocumentId = existingTranslationId, devLog.info("Existing translation version created", {
|
|
2525
|
-
...logContext,
|
|
2526
|
-
versionedDocumentId
|
|
2527
|
-
});
|
|
2528
|
-
const { _id, _createdAt, _updatedAt, _rev, ...sourceContent } = sourceDocument;
|
|
2529
|
-
await client.createOrReplace({
|
|
2530
|
-
...sourceContent,
|
|
2531
|
-
_id: versionedDocumentId,
|
|
2532
|
-
[languageFieldName]: targetLanguage
|
|
2533
|
-
}), devLog.info("Source content cloned onto version", {
|
|
2534
|
-
...logContext,
|
|
2535
|
-
versionedDocumentId
|
|
2536
|
-
});
|
|
2537
|
-
}
|
|
2538
|
-
let translatedDocument;
|
|
2539
|
-
switch (translationMode) {
|
|
2540
|
-
case "openai-only": {
|
|
2541
|
-
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2542
|
-
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2543
|
-
devLog.info("Using OpenAI-only translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai");
|
|
2544
|
-
const versionedDoc = await client.getDocument(versionedDocumentId);
|
|
2545
|
-
if (!versionedDoc)
|
|
2546
|
-
throw new Error(`Versioned document not found: ${versionedDocumentId}`);
|
|
2547
|
-
const translatedDoc = await translateInPlaceWithOpenAI({
|
|
2548
|
-
dispatch,
|
|
2549
|
-
sourceDocument,
|
|
2550
|
-
targetDocument: versionedDoc,
|
|
2551
|
-
targetLanguage,
|
|
2552
|
-
languageFieldName,
|
|
2553
|
-
translationPrompts,
|
|
2554
|
-
openaiConfig,
|
|
2555
|
-
isReview: !1
|
|
2556
|
-
});
|
|
2557
|
-
translatedDoc && (translatedDocument = await client.createOrReplace({
|
|
2558
|
-
...translatedDoc,
|
|
2559
|
-
_id: versionedDocumentId
|
|
2560
|
-
}));
|
|
2561
|
-
break;
|
|
2562
|
-
}
|
|
2563
|
-
case "sanity-with-review":
|
|
2564
|
-
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2565
|
-
throw new Error("OpenAI is not configured but sanity-with-review mode was selected");
|
|
2566
|
-
if (devLog.info("Using Sanity + OpenAI review", logContext), translatedDocument = await translateWithSanityAgent({
|
|
2567
|
-
dispatch,
|
|
2568
|
-
client,
|
|
2569
|
-
schemaId,
|
|
2570
|
-
sourceDocument,
|
|
2571
|
-
targetLanguage,
|
|
2572
|
-
languageFieldName,
|
|
2573
|
-
versionedDocumentId,
|
|
2574
|
-
translationPrompts
|
|
2575
|
-
}), translatedDocument) {
|
|
2576
|
-
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "reviewing_with_openai");
|
|
2577
|
-
const reviewedDoc = await translateInPlaceWithOpenAI({
|
|
2578
|
-
dispatch,
|
|
2579
|
-
sourceDocument,
|
|
2580
|
-
targetDocument: translatedDocument,
|
|
2581
|
-
targetLanguage,
|
|
2582
|
-
languageFieldName,
|
|
2583
|
-
translationPrompts,
|
|
2584
|
-
openaiConfig,
|
|
2585
|
-
isReview: !0
|
|
2586
|
-
});
|
|
2587
|
-
reviewedDoc && (translatedDocument = await client.createOrReplace({
|
|
2588
|
-
...reviewedDoc,
|
|
2589
|
-
_id: versionedDocumentId
|
|
2590
|
-
}));
|
|
2591
|
-
}
|
|
2592
|
-
break;
|
|
2593
|
-
default:
|
|
2594
|
-
devLog.info("Using Sanity-only translation", logContext), translatedDocument = await translateWithSanityAgent({
|
|
2595
|
-
dispatch,
|
|
2596
|
-
client,
|
|
2597
|
-
schemaId,
|
|
2598
|
-
sourceDocument,
|
|
2599
|
-
targetLanguage,
|
|
2600
|
-
languageFieldName,
|
|
2601
|
-
versionedDocumentId,
|
|
2602
|
-
translationPrompts
|
|
2603
|
-
});
|
|
2604
|
-
break;
|
|
2605
|
-
}
|
|
2606
|
-
return devLog.timeEnd("translateEntireDocument"), devLog.groupEnd(), translatedDocument ? {
|
|
2607
|
-
...translatedDocument,
|
|
2608
|
-
_id: baseDocumentId
|
|
2609
|
-
} : void 0;
|
|
2610
|
-
} catch (error) {
|
|
2611
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2612
|
-
devLog.error(
|
|
2613
|
-
"translateEntireDocument failed",
|
|
2614
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
2615
|
-
logContext
|
|
2616
|
-
), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
2617
|
-
errorMessage
|
|
2618
|
-
}), devLog.timeEnd("translateEntireDocument"), devLog.groupEnd();
|
|
2619
|
-
return;
|
|
2620
|
-
}
|
|
2621
|
-
}, translateWithSanityAgent = async ({
|
|
2622
|
-
dispatch,
|
|
2623
|
-
client,
|
|
2624
|
-
schemaId,
|
|
2625
|
-
sourceDocument,
|
|
2626
|
-
targetLanguage,
|
|
2627
|
-
languageFieldName,
|
|
2628
|
-
versionedDocumentId,
|
|
2629
|
-
translationPrompts
|
|
2630
|
-
}) => {
|
|
2631
|
-
const logContext = {
|
|
2632
|
-
documentId: sourceDocument._id,
|
|
2633
|
-
targetLanguage,
|
|
2634
|
-
versionedDocumentId
|
|
2635
|
-
};
|
|
2636
|
-
try {
|
|
2637
|
-
const { toLanguage, fromLanguage } = assembleLanguageSpecs({
|
|
2638
|
-
sourceDocument,
|
|
2639
|
-
languageFieldName,
|
|
2640
|
-
targetLanguage
|
|
2641
|
-
});
|
|
2642
|
-
devLog.debug("Language specs assembled", {
|
|
2643
|
-
...logContext,
|
|
2644
|
-
toLanguage,
|
|
2645
|
-
fromLanguage
|
|
2646
|
-
});
|
|
2647
|
-
const documentAnalysis = analyzeDocumentContent(sourceDocument);
|
|
2648
|
-
devLog.debug("Document analysis completed", {
|
|
2649
|
-
...logContext,
|
|
2650
|
-
analysis: documentAnalysis
|
|
2651
|
-
}), devLog.info("Starting Sanity agent translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating");
|
|
2652
|
-
const { styleGuide, omittedSections, truncatedSections } = assembleTranslationPrompt(
|
|
2653
|
-
{
|
|
2654
|
-
documentAnalysis,
|
|
2655
|
-
isUpdate: !1,
|
|
2656
|
-
// Always fresh translation in a version
|
|
2657
|
-
targetLanguage,
|
|
2658
|
-
sourceLanguage: sourceDocument[languageFieldName]
|
|
2659
|
-
},
|
|
2660
|
-
{
|
|
2661
|
-
globalPrompt: translationPrompts.globalPrompt,
|
|
2662
|
-
localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
|
|
2663
|
-
glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
|
|
2664
|
-
protectedPhrases: translationPrompts.protectedPhrases
|
|
2665
|
-
}
|
|
2666
|
-
);
|
|
2667
|
-
devLog.info("Structured style guide generated", {
|
|
2668
|
-
...logContext,
|
|
2669
|
-
styleGuideLength: styleGuide.length,
|
|
2670
|
-
omittedSections,
|
|
2671
|
-
truncatedSections
|
|
2672
|
-
}), (omittedSections.length > 0 || truncatedSections.length > 0) && devLog.warn("Style guide adjusted to fit character limit", {
|
|
2673
|
-
...logContext,
|
|
2674
|
-
omittedSections,
|
|
2675
|
-
truncatedSections
|
|
2520
|
+
}, escapeXml = (str) => str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"), isPlainObject = (val) => typeof val == "object" && val !== null && !Array.isArray(val), validateStructureIntegrity = (original, modified) => {
|
|
2521
|
+
const errors = [], criticalFields = ["_id", "_type", "_rev", "_createdAt", "_updatedAt"];
|
|
2522
|
+
for (const field of criticalFields)
|
|
2523
|
+
original[field] !== modified[field] && errors.push({
|
|
2524
|
+
path: [field],
|
|
2525
|
+
expected: original[field],
|
|
2526
|
+
actual: modified[field],
|
|
2527
|
+
type: "metadata_changed"
|
|
2676
2528
|
});
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
defaultReadOnly: !1
|
|
2689
|
-
}
|
|
2529
|
+
return validateStructureRecursive(original, modified, [], errors), {
|
|
2530
|
+
valid: errors.length === 0,
|
|
2531
|
+
errors
|
|
2532
|
+
};
|
|
2533
|
+
}, validateStructureRecursive = (original, modified, path, errors) => {
|
|
2534
|
+
if (typeof original != typeof modified) {
|
|
2535
|
+
errors.push({
|
|
2536
|
+
path,
|
|
2537
|
+
expected: typeof original,
|
|
2538
|
+
actual: typeof modified,
|
|
2539
|
+
type: "type_mismatch"
|
|
2690
2540
|
});
|
|
2691
|
-
return
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
if (Array.isArray(original)) {
|
|
2544
|
+
if (!Array.isArray(modified)) {
|
|
2545
|
+
errors.push({
|
|
2546
|
+
path,
|
|
2547
|
+
expected: "array",
|
|
2548
|
+
actual: typeof modified,
|
|
2549
|
+
type: "type_mismatch"
|
|
2550
|
+
});
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
if (original.length !== modified.length) {
|
|
2554
|
+
errors.push({
|
|
2555
|
+
path,
|
|
2556
|
+
expected: `length ${original.length}`,
|
|
2557
|
+
actual: `length ${modified.length}`,
|
|
2558
|
+
type: "type_mismatch"
|
|
2559
|
+
});
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
original.forEach((item, idx) => {
|
|
2563
|
+
validateStructureRecursive(item, modified[idx], [...path, String(idx)], errors);
|
|
2704
2564
|
});
|
|
2705
2565
|
return;
|
|
2706
2566
|
}
|
|
2707
|
-
|
|
2567
|
+
if (typeof original == "object" && original !== null && typeof modified == "object" && modified !== null) {
|
|
2568
|
+
const origKeys = Object.keys(original).sort(), modKeys = Object.keys(modified).sort();
|
|
2569
|
+
for (const key of origKeys)
|
|
2570
|
+
key in modified || errors.push({
|
|
2571
|
+
path: [...path, key],
|
|
2572
|
+
expected: "present",
|
|
2573
|
+
actual: "missing",
|
|
2574
|
+
type: "missing"
|
|
2575
|
+
});
|
|
2576
|
+
for (const key of modKeys)
|
|
2577
|
+
key in original || errors.push({
|
|
2578
|
+
path: [...path, key],
|
|
2579
|
+
expected: "absent",
|
|
2580
|
+
actual: "present",
|
|
2581
|
+
type: "extra"
|
|
2582
|
+
});
|
|
2583
|
+
const origObj = original, modObj = modified;
|
|
2584
|
+
for (const key of origKeys)
|
|
2585
|
+
key in modObj && (key.startsWith("_") || key === "marks" || key === "style" || key === "markDefs" ? JSON.stringify(origObj[key]) !== JSON.stringify(modObj[key]) && errors.push({
|
|
2586
|
+
path: [...path, key],
|
|
2587
|
+
expected: origObj[key],
|
|
2588
|
+
actual: modObj[key],
|
|
2589
|
+
type: "metadata_changed"
|
|
2590
|
+
}) : key !== "text" && validateStructureRecursive(origObj[key], modObj[key], [...path, key], errors));
|
|
2591
|
+
}
|
|
2592
|
+
}, getOpenAITranslation = async ({
|
|
2708
2593
|
dispatch,
|
|
2709
2594
|
sourceDocument,
|
|
2710
|
-
targetDocument,
|
|
2711
|
-
targetLanguage,
|
|
2712
2595
|
languageFieldName,
|
|
2713
2596
|
translationPrompts,
|
|
2714
|
-
|
|
2715
|
-
|
|
2597
|
+
targetLanguage,
|
|
2598
|
+
openaiConfig
|
|
2716
2599
|
}) => {
|
|
2717
2600
|
const sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
2718
2601
|
sourceId: sourceDocument._id,
|
|
2719
|
-
|
|
2720
|
-
targetLanguage,
|
|
2721
|
-
sourceLanguage,
|
|
2722
|
-
isReview
|
|
2602
|
+
sourceLanguage
|
|
2723
2603
|
};
|
|
2724
2604
|
try {
|
|
2725
|
-
const sourceText = serializeDocument(sourceDocument)
|
|
2726
|
-
devLog.
|
|
2605
|
+
const sourceText = serializeDocument(sourceDocument);
|
|
2606
|
+
devLog.info("Serialized document for OpenAI", {
|
|
2727
2607
|
...logContext,
|
|
2728
|
-
|
|
2729
|
-
|
|
2608
|
+
characterCount: sourceText.length,
|
|
2609
|
+
estimatedTokens: Math.ceil(sourceText.length / 4),
|
|
2610
|
+
preview: sourceText.substring(0, 500)
|
|
2730
2611
|
});
|
|
2731
|
-
const prompt =
|
|
2732
|
-
sourceText,
|
|
2733
|
-
translatedText: targetText,
|
|
2734
|
-
sourceLanguage,
|
|
2735
|
-
targetLanguage,
|
|
2736
|
-
translationPrompts
|
|
2737
|
-
}) : buildTranslationPrompt({
|
|
2612
|
+
const prompt = buildTranslationPrompt$1({
|
|
2738
2613
|
sourceText,
|
|
2739
2614
|
sourceLanguage,
|
|
2740
2615
|
targetLanguage,
|
|
2741
2616
|
translationPrompts
|
|
2742
|
-
})
|
|
2743
|
-
devLog.debug("Calling GPT-5", {
|
|
2744
|
-
...logContext,
|
|
2745
|
-
promptLength: prompt.length
|
|
2746
|
-
});
|
|
2747
|
-
const response = await callGPT5(openaiConfig.apiKey, {
|
|
2617
|
+
}), response = await callGPT5(openaiConfig.apiKey, {
|
|
2748
2618
|
messages: [
|
|
2749
2619
|
{
|
|
2750
|
-
role: "
|
|
2751
|
-
content:
|
|
2620
|
+
role: "developer",
|
|
2621
|
+
content: "You are a translation quality reviewer. Review and refine translations while preserving structure."
|
|
2752
2622
|
},
|
|
2753
2623
|
{ role: "user", content: prompt }
|
|
2754
2624
|
],
|
|
2755
2625
|
model: openaiConfig.model,
|
|
2756
|
-
reasoningEffort: openaiConfig.reasoningEffort
|
|
2757
|
-
verbosity: openaiConfig.verbosity,
|
|
2758
|
-
onReasoningChunk: (reasoning) => {
|
|
2759
|
-
const status2 = isReview ? "reviewing_with_openai" : "translating_with_openai";
|
|
2760
|
-
setJobStatus(dispatch, sourceDocument._id, targetLanguage, status2, {
|
|
2761
|
-
reasoningProgress: reasoning
|
|
2762
|
-
});
|
|
2763
|
-
}
|
|
2626
|
+
reasoningEffort: openaiConfig.reasoningEffort
|
|
2764
2627
|
});
|
|
2765
|
-
devLog.info("OpenAI
|
|
2628
|
+
devLog.info("OpenAI translation response received", {
|
|
2766
2629
|
...logContext,
|
|
2767
|
-
tokensUsed: response.tokensUsed
|
|
2768
|
-
cost: response.cost
|
|
2630
|
+
tokensUsed: response.tokensUsed,
|
|
2631
|
+
cost: response.cost,
|
|
2632
|
+
responseLength: response.content.length,
|
|
2633
|
+
responsePreview: response.content.substring(0, 500)
|
|
2769
2634
|
});
|
|
2770
|
-
const result = deserializeDocument(
|
|
2635
|
+
const result = deserializeDocument(sourceDocument, response.content);
|
|
2636
|
+
result[languageFieldName] = targetLanguage;
|
|
2637
|
+
const validation = validateStructureIntegrity(sourceDocument, result);
|
|
2771
2638
|
if (!validation.valid) {
|
|
2772
2639
|
devLog.error("Structure validation failed", {
|
|
2773
2640
|
...logContext,
|
|
@@ -2781,17 +2648,26 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
|
|
|
2781
2648
|
`Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
|
|
2782
2649
|
);
|
|
2783
2650
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2651
|
+
const translationValidation = validateTranslationCompleteness(sourceDocument, result, languageFieldName);
|
|
2652
|
+
return translationValidation.valid || (devLog.warn("Translation completeness warning", {
|
|
2653
|
+
...logContext,
|
|
2654
|
+
skippedFields: translationValidation.skippedFields,
|
|
2655
|
+
warning: "GPT-5 may have skipped some important fields"
|
|
2656
|
+
}), devLog.info("Skipped field details", {
|
|
2657
|
+
...logContext,
|
|
2658
|
+
skippedFields: translationValidation.skippedFields.map((field) => ({
|
|
2659
|
+
path: field.path,
|
|
2660
|
+
originalValue: field.originalValue,
|
|
2661
|
+
translatedValue: field.translatedValue
|
|
2662
|
+
}))
|
|
2663
|
+
})), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
|
|
2787
2664
|
tokensUsed: response.tokensUsed.total,
|
|
2788
|
-
estimatedCost: response.cost
|
|
2789
|
-
reasoningSummary: response.reasoningSummary
|
|
2665
|
+
estimatedCost: response.cost
|
|
2790
2666
|
}), result;
|
|
2791
2667
|
} catch (error) {
|
|
2792
2668
|
throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
|
|
2793
2669
|
}
|
|
2794
|
-
}, buildTranslationPrompt = ({
|
|
2670
|
+
}, buildTranslationPrompt$1 = ({
|
|
2795
2671
|
sourceText,
|
|
2796
2672
|
sourceLanguage,
|
|
2797
2673
|
targetLanguage,
|
|
@@ -2825,12 +2701,19 @@ ${sourceText}
|
|
|
2825
2701
|
|
|
2826
2702
|
## Instructions
|
|
2827
2703
|
|
|
2828
|
-
1.
|
|
2829
|
-
2.
|
|
2830
|
-
3.
|
|
2831
|
-
4.
|
|
2832
|
-
5.
|
|
2833
|
-
6.
|
|
2704
|
+
1. **TRANSLATE ALL TEXT FIELDS**: Every field marked with type="string" or containing text must be translated
|
|
2705
|
+
2. **DO NOT SKIP SIMPLE FIELDS**: Pay special attention to simple fields like "title", "name", "description" - these are often the most important
|
|
2706
|
+
3. Follow the guidelines and terminology requirements above
|
|
2707
|
+
4. Preserve protected phrases exactly as they appear
|
|
2708
|
+
5. Return the document in THE EXACT SAME FORMAT
|
|
2709
|
+
6. **CRITICAL**: Only modify text content between tags
|
|
2710
|
+
7. Never add, remove, or change any tags, attributes, or structure
|
|
2711
|
+
|
|
2712
|
+
**Common fields that must always be translated:**
|
|
2713
|
+
- title, name, heading, label
|
|
2714
|
+
- description, caption, alt text
|
|
2715
|
+
- All text within <field> tags
|
|
2716
|
+
- All text within <span> tags
|
|
2834
2717
|
|
|
2835
2718
|
If you used significant reasoning to make translation decisions, you may include a brief summary:
|
|
2836
2719
|
<reasoning>
|
|
@@ -2838,73 +2721,267 @@ Brief summary of key translation decisions and reasoning
|
|
|
2838
2721
|
</reasoning>
|
|
2839
2722
|
|
|
2840
2723
|
Then return the translated document:`;
|
|
2841
|
-
},
|
|
2842
|
-
|
|
2843
|
-
|
|
2724
|
+
}, validateTranslationCompleteness = (sourceDoc, translatedDoc, languageFieldName) => {
|
|
2725
|
+
const skippedFields = [], checkField = (source, translated, path = []) => {
|
|
2726
|
+
if (path.length > 0) {
|
|
2727
|
+
const fieldName = path[path.length - 1];
|
|
2728
|
+
if (fieldName.startsWith("_") || fieldName === languageFieldName)
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
if (typeof source == "string" && typeof translated == "string") {
|
|
2732
|
+
if (source.length > 0 && source === translated) {
|
|
2733
|
+
const fieldName = path[path.length - 1] || "unknown";
|
|
2734
|
+
(["title", "name", "heading", "label", "description", "caption", "alt"].includes(
|
|
2735
|
+
fieldName
|
|
2736
|
+
) || source.length > 10) && skippedFields.push({
|
|
2737
|
+
path: path.join("."),
|
|
2738
|
+
originalValue: source,
|
|
2739
|
+
translatedValue: translated
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
if (source && translated && typeof source == "object" && typeof translated == "object") {
|
|
2745
|
+
if (Array.isArray(source) && Array.isArray(translated)) {
|
|
2746
|
+
const maxLength = Math.min(source.length, translated.length);
|
|
2747
|
+
for (let i = 0; i < maxLength; i++)
|
|
2748
|
+
checkField(source[i], translated[i], [...path, String(i)]);
|
|
2749
|
+
} else if (!Array.isArray(source) && !Array.isArray(translated))
|
|
2750
|
+
for (const key of Object.keys(source))
|
|
2751
|
+
!key.startsWith("_") && key !== languageFieldName && checkField(source[key], translated[key], [
|
|
2752
|
+
...path,
|
|
2753
|
+
key
|
|
2754
|
+
]);
|
|
2755
|
+
}
|
|
2756
|
+
};
|
|
2757
|
+
return checkField(sourceDoc, translatedDoc), {
|
|
2758
|
+
valid: skippedFields.length === 0,
|
|
2759
|
+
skippedFields
|
|
2760
|
+
};
|
|
2761
|
+
}, assembleLanguageSpecs = ({
|
|
2762
|
+
sourceDocument,
|
|
2763
|
+
languageFieldName,
|
|
2764
|
+
targetLanguage
|
|
2765
|
+
}) => {
|
|
2766
|
+
const sourceLanugageId = sourceDocument[languageFieldName];
|
|
2767
|
+
if (!sourceLanugageId)
|
|
2768
|
+
throw new Error("[assembleLanguages] Source language ID is missing");
|
|
2769
|
+
return {
|
|
2770
|
+
fromLanguage: {
|
|
2771
|
+
id: sourceLanugageId,
|
|
2772
|
+
title: sourceLanugageId
|
|
2773
|
+
},
|
|
2774
|
+
toLanguage: {
|
|
2775
|
+
id: targetLanguage,
|
|
2776
|
+
title: targetLanguage
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
}, buildTranslationPrompt = ({
|
|
2844
2780
|
sourceLanguage,
|
|
2845
2781
|
targetLanguage,
|
|
2846
|
-
|
|
2782
|
+
components,
|
|
2783
|
+
isUpdate = !1
|
|
2847
2784
|
}) => {
|
|
2848
|
-
const
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
## Review Criteria
|
|
2785
|
+
const sections = [];
|
|
2786
|
+
sections.push(buildAgentDirectivesSection(sourceLanguage, targetLanguage)), sections.push(buildCriticalRulesSection(sourceLanguage, targetLanguage)), isUpdate && sections.push(buildUpdateInstructionsSection()), components.glossaryTerms?.trim() && sections.push(buildGlossarySection(components.glossaryTerms)), components.protectedPhrases?.length && sections.push(buildProtectedPhrasesSection(components.protectedPhrases));
|
|
2787
|
+
const styleSection = buildStyleGuideSection(components.globalPrompt, components.localePrompt);
|
|
2788
|
+
return styleSection && sections.push(styleSection), sections.join(`
|
|
2854
2789
|
|
|
2855
|
-
|
|
2856
|
-
2. **Terminology**: Are glossary terms used correctly?
|
|
2857
|
-
3. **Protected Phrases**: Are protected phrases preserved?
|
|
2858
|
-
4. **Fluency**: Is it natural and fluent in ${targetLanguage}?
|
|
2859
|
-
5. **Style**: Does it follow the style guide?
|
|
2860
|
-
6. **Completeness**: Is all content translated?
|
|
2790
|
+
---
|
|
2861
2791
|
|
|
2862
|
-
|
|
2792
|
+
`);
|
|
2793
|
+
}, buildAgentDirectivesSection = (sourceLanguage, targetLanguage) => `# AGENT WORKFLOW DIRECTIVES
|
|
2863
2794
|
|
|
2864
|
-
${
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
${
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
${glossaryTerms}
|
|
2872
|
-
` : ""}
|
|
2873
|
-
${protectedPhrases?.length ? `### Protected Phrases (must preserve)
|
|
2874
|
-
${protectedPhrases.join(", ")}
|
|
2875
|
-
` : ""}
|
|
2795
|
+
- Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
|
|
2796
|
+
- Scan each field once, then move directly into translation work.
|
|
2797
|
+
- Plan updates before editing so document structure remains intact.
|
|
2798
|
+
- Default to medium reasoning effort and escalate only for ambiguous passages.
|
|
2799
|
+
- Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`, buildCriticalRulesSection = (sourceLanguage, targetLanguage) => {
|
|
2800
|
+
const fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
|
|
2801
|
+
return `# CRITICAL TRANSLATION RULES
|
|
2876
2802
|
|
|
2877
|
-
|
|
2803
|
+
Follow these without exception:
|
|
2878
2804
|
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2805
|
+
1. Translate from ${fromLanguage} into ${toLanguage}; every output token must be ${toLanguage} unless flagged as protected.
|
|
2806
|
+
2. Preserve meaning exactly\u2014no additions, deletions, or tone shifts.
|
|
2807
|
+
3. Maintain all layout, markdown, rich-text marks, and data structure.
|
|
2808
|
+
4. Never invent metadata or field values that are absent in the source.
|
|
2809
|
+
5. Leave technical tokens (code, IDs, filenames) untouched.
|
|
2810
|
+
6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
|
|
2811
|
+
}, buildUpdateInstructionsSection = () => `# UPDATING EXISTING TRANSLATION
|
|
2882
2812
|
|
|
2883
|
-
|
|
2813
|
+
This document already has a translation; only adjust fields that changed.
|
|
2884
2814
|
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2815
|
+
- Compare the latest source to the existing target text.
|
|
2816
|
+
- Keep prior terminology and style unless the source demands a change.
|
|
2817
|
+
- Edit minimally while guaranteeing accuracy and completeness.`, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
|
|
2888
2818
|
|
|
2889
|
-
|
|
2819
|
+
Apply these mappings immediately after protected phrases and before style preferences.
|
|
2890
2820
|
|
|
2891
|
-
|
|
2892
|
-
2. Make corrections where needed to improve accuracy, fluency, or adherence to guidelines
|
|
2893
|
-
3. Keep good translations as-is - only fix what needs fixing
|
|
2894
|
-
4. Return the corrected translation in THE EXACT SAME FORMAT
|
|
2895
|
-
5. **CRITICAL**: Only modify text content between tags
|
|
2896
|
-
6. Never add, remove, or change any tags, attributes, or structure
|
|
2821
|
+
${glossaryTerms}`, buildProtectedPhrasesSection = (protectedPhrases) => `# PROTECTED PHRASES
|
|
2897
2822
|
|
|
2898
|
-
|
|
2899
|
-
<reasoning>
|
|
2900
|
-
Brief summary of review findings and corrections made
|
|
2901
|
-
</reasoning>
|
|
2823
|
+
These phrases must NEVER be translated. Keep them exactly as they appear:
|
|
2902
2824
|
|
|
2903
|
-
|
|
2825
|
+
${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
|
|
2826
|
+
const components = [];
|
|
2827
|
+
return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
|
|
2828
|
+
"Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
|
|
2829
|
+
), components.push("")), globalPrompt && (components.push("## General Guidelines"), components.push(globalPrompt), components.push("")), localePrompt && (components.push("## Language-Specific Guidelines"), components.push(localePrompt)), components.length > 0 ? components.join(`
|
|
2830
|
+
`) : null;
|
|
2831
|
+
}, getSanityAgentTranslation = async ({
|
|
2832
|
+
sourceDocument,
|
|
2833
|
+
targetLanguage,
|
|
2834
|
+
languageFieldName,
|
|
2835
|
+
schemaId,
|
|
2836
|
+
client,
|
|
2837
|
+
translationPrompts
|
|
2838
|
+
}) => {
|
|
2839
|
+
const styleGuide = buildTranslationPrompt({
|
|
2840
|
+
sourceLanguage: sourceDocument[languageFieldName],
|
|
2841
|
+
targetLanguage,
|
|
2842
|
+
isUpdate: !1,
|
|
2843
|
+
// Always fresh translation in a version
|
|
2844
|
+
components: {
|
|
2845
|
+
globalPrompt: translationPrompts.globalPrompt,
|
|
2846
|
+
localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
|
|
2847
|
+
glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
|
|
2848
|
+
protectedPhrases: translationPrompts.protectedPhrases
|
|
2849
|
+
}
|
|
2850
|
+
}), { fromLanguage, toLanguage } = assembleLanguageSpecs({
|
|
2851
|
+
sourceDocument,
|
|
2852
|
+
languageFieldName,
|
|
2853
|
+
targetLanguage
|
|
2854
|
+
});
|
|
2855
|
+
return await client.agent.action.translate({
|
|
2856
|
+
schemaId,
|
|
2857
|
+
toLanguage,
|
|
2858
|
+
fromLanguage,
|
|
2859
|
+
documentId: sourceDocument._id,
|
|
2860
|
+
noWrite: !0,
|
|
2861
|
+
// Do not write to the document, just return the translated content
|
|
2862
|
+
languageFieldPath: [languageFieldName],
|
|
2863
|
+
styleGuide,
|
|
2864
|
+
protectedPhrases: translationPrompts.protectedPhrases,
|
|
2865
|
+
conditionalPaths: {
|
|
2866
|
+
defaultHidden: !1,
|
|
2867
|
+
defaultReadOnly: !1
|
|
2868
|
+
}
|
|
2869
|
+
});
|
|
2870
|
+
}, handleTranslation = async ({
|
|
2871
|
+
dispatch,
|
|
2872
|
+
client,
|
|
2873
|
+
schemaId,
|
|
2874
|
+
sourceDocument,
|
|
2875
|
+
targetLanguage,
|
|
2876
|
+
existingTranslationId,
|
|
2877
|
+
languageFieldName,
|
|
2878
|
+
translationPrompts,
|
|
2879
|
+
translationMode,
|
|
2880
|
+
openaiConfig,
|
|
2881
|
+
releaseId,
|
|
2882
|
+
excludeFields = []
|
|
2883
|
+
}) => {
|
|
2884
|
+
const safeExcludeFields = excludeFields.filter((field) => field !== languageFieldName);
|
|
2885
|
+
excludeFields.length !== safeExcludeFields.length && devLog.warn("Language field was in excludeFields - removing it to prevent translation errors", {
|
|
2886
|
+
documentId: sourceDocument._id,
|
|
2887
|
+
languageFieldName,
|
|
2888
|
+
originalExcludeFields: excludeFields,
|
|
2889
|
+
safeExcludeFields
|
|
2890
|
+
}), devLog.info("Filtering excluded fields before translation", {
|
|
2891
|
+
documentId: sourceDocument._id,
|
|
2892
|
+
documentType: sourceDocument._type,
|
|
2893
|
+
excludeFields: safeExcludeFields,
|
|
2894
|
+
excludeFieldsLength: safeExcludeFields?.length ?? 0,
|
|
2895
|
+
sourceHasTitle: "title" in sourceDocument,
|
|
2896
|
+
sourceTitleValue: sourceDocument.title
|
|
2897
|
+
});
|
|
2898
|
+
const { filteredDocument, excludedValues } = filterExcludedFields(sourceDocument, safeExcludeFields);
|
|
2899
|
+
devLog.info("After filtering excluded fields", {
|
|
2900
|
+
documentId: sourceDocument._id,
|
|
2901
|
+
filteredHasTitle: "title" in filteredDocument,
|
|
2902
|
+
filteredTitleValue: filteredDocument.title,
|
|
2903
|
+
excludedValuesKeys: Object.keys(excludedValues),
|
|
2904
|
+
excludedValues
|
|
2905
|
+
}), devLog.debug("Applying field transforms before translation", {
|
|
2906
|
+
documentId: sourceDocument._id,
|
|
2907
|
+
documentType: sourceDocument._type
|
|
2908
|
+
});
|
|
2909
|
+
const { transformedDocument, originalStructure } = applyTransforms({
|
|
2910
|
+
document: filteredDocument,
|
|
2911
|
+
registry: globalTransformRegistry,
|
|
2912
|
+
schemaType: sourceDocument._type
|
|
2913
|
+
});
|
|
2914
|
+
let translatedDoc;
|
|
2915
|
+
switch (translationMode) {
|
|
2916
|
+
case "sanity-only": {
|
|
2917
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
|
|
2918
|
+
sourceDocument: transformedDocument,
|
|
2919
|
+
targetLanguage,
|
|
2920
|
+
languageFieldName,
|
|
2921
|
+
schemaId,
|
|
2922
|
+
client,
|
|
2923
|
+
translationPrompts
|
|
2924
|
+
});
|
|
2925
|
+
break;
|
|
2926
|
+
}
|
|
2927
|
+
case "openai-only": {
|
|
2928
|
+
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2929
|
+
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2930
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), translatedDoc = await getOpenAITranslation({
|
|
2931
|
+
dispatch,
|
|
2932
|
+
sourceDocument: transformedDocument,
|
|
2933
|
+
targetLanguage,
|
|
2934
|
+
languageFieldName,
|
|
2935
|
+
translationPrompts,
|
|
2936
|
+
openaiConfig
|
|
2937
|
+
});
|
|
2938
|
+
break;
|
|
2939
|
+
}
|
|
2940
|
+
case "sanity-with-review":
|
|
2941
|
+
throw new Error("Sanity with review translation method is not implemented");
|
|
2942
|
+
default:
|
|
2943
|
+
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2944
|
+
}
|
|
2945
|
+
if (!translatedDoc)
|
|
2946
|
+
return null;
|
|
2947
|
+
devLog.debug("Restoring field transforms after translation", {
|
|
2948
|
+
documentId: sourceDocument._id,
|
|
2949
|
+
documentType: sourceDocument._type
|
|
2950
|
+
});
|
|
2951
|
+
const transformedResult = restoreTransforms({
|
|
2952
|
+
translatedDocument: translatedDoc,
|
|
2953
|
+
originalStructure,
|
|
2954
|
+
registry: globalTransformRegistry,
|
|
2955
|
+
schemaType: sourceDocument._type
|
|
2956
|
+
}), excludedValuesToRestore = { ...excludedValues };
|
|
2957
|
+
delete excludedValuesToRestore[languageFieldName], devLog.info("Before restoring excluded fields", {
|
|
2958
|
+
documentId: sourceDocument._id,
|
|
2959
|
+
excludedFieldCount: Object.keys(excludedValues).length,
|
|
2960
|
+
excludedValuesKeys: Object.keys(excludedValues),
|
|
2961
|
+
languageFieldExcluded: languageFieldName in excludedValues,
|
|
2962
|
+
transformedResultHasTitle: "title" in transformedResult,
|
|
2963
|
+
transformedResultTitle: transformedResult.title
|
|
2964
|
+
});
|
|
2965
|
+
const finalDoc = restoreExcludedFields(transformedResult, excludedValuesToRestore);
|
|
2966
|
+
devLog.info("After restoring excluded fields", {
|
|
2967
|
+
documentId: sourceDocument._id,
|
|
2968
|
+
finalDocHasTitle: "title" in finalDoc,
|
|
2969
|
+
finalDocTitle: finalDoc.title
|
|
2970
|
+
});
|
|
2971
|
+
const targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
|
|
2972
|
+
...finalDoc,
|
|
2973
|
+
_id: targetDocumentVersionId
|
|
2974
|
+
};
|
|
2975
|
+
return await client.action({
|
|
2976
|
+
actionType: "sanity.action.document.version.create",
|
|
2977
|
+
publishedId: targetDocumentPublishedId,
|
|
2978
|
+
document: newDoc
|
|
2979
|
+
}), newDoc;
|
|
2904
2980
|
}, translateDocument = async ({
|
|
2905
2981
|
dispatch,
|
|
2906
2982
|
client,
|
|
2907
2983
|
sourceDocument,
|
|
2984
|
+
sourceLanguage,
|
|
2908
2985
|
targetLanguage,
|
|
2909
2986
|
languageFieldName,
|
|
2910
2987
|
shouldOverwriteFreshTranslations,
|
|
@@ -2912,7 +2989,8 @@ Then return the corrected translation document:`;
|
|
|
2912
2989
|
translationPrompts,
|
|
2913
2990
|
translationMode,
|
|
2914
2991
|
openaiConfig,
|
|
2915
|
-
releaseId
|
|
2992
|
+
releaseId,
|
|
2993
|
+
excludeFields
|
|
2916
2994
|
}) => {
|
|
2917
2995
|
const logContext = {
|
|
2918
2996
|
documentId: sourceDocument._id,
|
|
@@ -2923,62 +3001,65 @@ Then return the corrected translation document:`;
|
|
|
2923
3001
|
};
|
|
2924
3002
|
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
|
|
2925
3003
|
try {
|
|
2926
|
-
const sourceLanguage = sourceDocument[languageFieldName];
|
|
2927
|
-
if (devLog.debug("Checking source document language", {
|
|
2928
|
-
...logContext,
|
|
2929
|
-
sourceLanguage,
|
|
2930
|
-
languageFieldName
|
|
2931
|
-
}), !sourceLanguage) {
|
|
2932
|
-
const error = `Source document ${sourceDocument._id} does not have a language`;
|
|
2933
|
-
throw devLog.error(error, void 0, logContext), new Error(error);
|
|
2934
|
-
}
|
|
2935
|
-
devLog.debug("Getting translation metadata", logContext);
|
|
2936
3004
|
const translationMetadata = await getTranslationMetadata({
|
|
2937
3005
|
client,
|
|
2938
3006
|
sourceDocument,
|
|
2939
3007
|
sourceLanguage
|
|
2940
3008
|
}), isAlreadyFreshTranslation = translationMetadata.translationHistory?.some(
|
|
2941
3009
|
(history) => history.language === targetLanguage && history.sourceRev === sourceDocument._rev
|
|
2942
|
-
)
|
|
2943
|
-
if (
|
|
2944
|
-
|
|
2945
|
-
isAlreadyFreshTranslation,
|
|
2946
|
-
currentRev: sourceDocument._rev
|
|
2947
|
-
}), !shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
|
|
2948
|
-
devLog.info("Skipping fresh translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
|
|
3010
|
+
);
|
|
3011
|
+
if (!shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
|
|
3012
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
|
|
2949
3013
|
targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
|
|
3014
|
+
});
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), translationResult = await withRetry(
|
|
3018
|
+
() => handleTranslation({
|
|
3019
|
+
dispatch,
|
|
3020
|
+
client,
|
|
3021
|
+
schemaId,
|
|
3022
|
+
sourceDocument,
|
|
3023
|
+
targetLanguage,
|
|
3024
|
+
existingTranslationId,
|
|
3025
|
+
languageFieldName,
|
|
3026
|
+
translationPrompts,
|
|
3027
|
+
translationMode,
|
|
3028
|
+
openaiConfig,
|
|
3029
|
+
releaseId,
|
|
3030
|
+
excludeFields
|
|
3031
|
+
}),
|
|
3032
|
+
{
|
|
3033
|
+
maxRetries: 3,
|
|
3034
|
+
initialDelayMs: 1e3,
|
|
3035
|
+
isRetryable: isRetryableError,
|
|
3036
|
+
onRetry: (attempt, error, delayMs) => {
|
|
3037
|
+
devLog.warn("Translation failed, scheduling retry", {
|
|
3038
|
+
...logContext,
|
|
3039
|
+
attempt,
|
|
3040
|
+
delayMs,
|
|
3041
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3042
|
+
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "retrying", {
|
|
3043
|
+
retryCount: attempt,
|
|
3044
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
);
|
|
3049
|
+
if (!translationResult.success || !translationResult.result) {
|
|
3050
|
+
const errorMessage = translationResult.error instanceof Error ? translationResult.error.message : String(translationResult.error || "Translation failed");
|
|
3051
|
+
devLog.error("Translation failed after retries", translationResult.error, {
|
|
3052
|
+
...logContext,
|
|
3053
|
+
attempts: translationResult.attempts
|
|
3054
|
+
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
3055
|
+
errorMessage,
|
|
3056
|
+
retryCount: translationResult.attempts - 1
|
|
3057
|
+
// -1 because attempts includes initial try
|
|
2950
3058
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
2951
3059
|
return;
|
|
2952
3060
|
}
|
|
2953
|
-
const
|
|
2954
|
-
|
|
2955
|
-
targetLanguage
|
|
2956
|
-
}), isNewTranslation = translationTarget.operation === "create";
|
|
2957
|
-
devLog.debug("Translation target determined", {
|
|
2958
|
-
...logContext,
|
|
2959
|
-
operation: translationTarget.operation,
|
|
2960
|
-
isNewTranslation
|
|
2961
|
-
});
|
|
2962
|
-
const newDocument = await translateEntireDocument({
|
|
2963
|
-
dispatch,
|
|
2964
|
-
client,
|
|
2965
|
-
schemaId,
|
|
2966
|
-
sourceDocument,
|
|
2967
|
-
targetLanguage,
|
|
2968
|
-
languageFieldName,
|
|
2969
|
-
translationTarget,
|
|
2970
|
-
translationPrompts,
|
|
2971
|
-
translationMode,
|
|
2972
|
-
openaiConfig,
|
|
2973
|
-
releaseId,
|
|
2974
|
-
isNewTranslation
|
|
2975
|
-
});
|
|
2976
|
-
if (!newDocument)
|
|
2977
|
-
throw devLog.error("Failed to create translation document", void 0, logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), devLog.timeEnd("translateDocument"), devLog.groupEnd(), new Error("Failed to create translation document");
|
|
2978
|
-
return devLog.debug("Translation document created", {
|
|
2979
|
-
...logContext,
|
|
2980
|
-
newDocumentId: newDocument._id
|
|
2981
|
-
}), devLog.debug("Updating translation metadata", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
|
|
3061
|
+
const newDocument = translationResult.result;
|
|
3062
|
+
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
|
|
2982
3063
|
targetId: newDocument._id
|
|
2983
3064
|
}), await updateTranslationMetadata({
|
|
2984
3065
|
client,
|
|
@@ -2987,8 +3068,10 @@ Then return the corrected translation document:`;
|
|
|
2987
3068
|
newDocument,
|
|
2988
3069
|
targetLanguage,
|
|
2989
3070
|
translationMetadata,
|
|
2990
|
-
isNewTranslation
|
|
2991
|
-
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed"
|
|
3071
|
+
isNewTranslation: !existingTranslationId
|
|
3072
|
+
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed", {
|
|
3073
|
+
targetId: newDocument._id
|
|
3074
|
+
}), devLog.info("Translation completed successfully", {
|
|
2992
3075
|
...logContext,
|
|
2993
3076
|
newDocumentId: newDocument._id
|
|
2994
3077
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
@@ -3010,81 +3093,66 @@ Then return the corrected translation document:`;
|
|
|
3010
3093
|
references = [],
|
|
3011
3094
|
maxConcurrentTranslationJobs,
|
|
3012
3095
|
translationMode,
|
|
3013
|
-
openaiConfig
|
|
3096
|
+
openaiConfig,
|
|
3097
|
+
excludeFields
|
|
3014
3098
|
}) => {
|
|
3015
3099
|
const logContext = {
|
|
3016
3100
|
documentId: sourceDocument._id,
|
|
3101
|
+
documentRev: sourceDocument._rev,
|
|
3017
3102
|
targetLanguages,
|
|
3018
3103
|
referencesCount: references.length,
|
|
3019
3104
|
maxConcurrentJobs: maxConcurrentTranslationJobs,
|
|
3020
3105
|
translationMode,
|
|
3021
3106
|
shouldOverwriteFreshTranslations
|
|
3022
3107
|
};
|
|
3023
|
-
devLog.group("translateDocumentSet", logContext), devLog.time("translateDocumentSet");
|
|
3024
3108
|
let releaseId;
|
|
3025
3109
|
try {
|
|
3026
3110
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
3027
3111
|
if (!sourceLanguage)
|
|
3028
3112
|
throw devLog.error("Source language not found"), new Error("Source language not found");
|
|
3029
|
-
maxConcurrentTranslationJobs &&
|
|
3030
|
-
...logContext,
|
|
3031
|
-
newPermits: maxConcurrentTranslationJobs
|
|
3032
|
-
}), translationSemaphore.updatePermits(maxConcurrentTranslationJobs)), devLog.info("Starting document set translation", logContext), dispatch({
|
|
3113
|
+
maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
3033
3114
|
type: "SET_OVERALL_STATUS",
|
|
3034
3115
|
payload: "translating"
|
|
3035
3116
|
});
|
|
3036
|
-
const releaseTitle = `[AI Translation] ${sourceDocument.title || sourceDocument._id.split("-").slice(0, 2).join("-")} (${sourceDocument._type}) ${sourceLanguage} \u2192 ${targetLanguages.join(", ")}`, releaseDescription = `AI translation batch created on ${(/* @__PURE__ */ new Date()).toLocaleDateString()} at ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}
|
|
3037
|
-
devLog.info("Creating Content Release for translation batch", {
|
|
3038
|
-
...logContext,
|
|
3039
|
-
releaseTitle
|
|
3040
|
-
});
|
|
3041
|
-
const { releaseId: newReleaseId } = await client.releases.create({
|
|
3117
|
+
const releaseTitle = `[AI Translation] ${sourceDocument.title || sourceDocument._id.split("-").slice(0, 2).join("-")} (${sourceDocument._type}) ${sourceLanguage} \u2192 ${targetLanguages.join(", ")}`, releaseDescription = `AI translation batch created on ${(/* @__PURE__ */ new Date()).toLocaleDateString()} at ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`, { releaseId: releaseId2 } = await client.releases.create({
|
|
3042
3118
|
metadata: {
|
|
3043
3119
|
title: releaseTitle,
|
|
3044
3120
|
description: releaseDescription,
|
|
3045
|
-
releaseType: "
|
|
3121
|
+
releaseType: "undecided"
|
|
3046
3122
|
}
|
|
3047
3123
|
});
|
|
3048
|
-
|
|
3124
|
+
devLog.info("Content Release created", {
|
|
3049
3125
|
...logContext,
|
|
3050
|
-
releaseId
|
|
3126
|
+
releaseId: releaseId2
|
|
3051
3127
|
});
|
|
3052
3128
|
const sourceDocuments = [
|
|
3053
3129
|
{
|
|
3054
3130
|
_id: sourceDocument._id,
|
|
3055
3131
|
_type: sourceDocument._type,
|
|
3056
|
-
// TODO:
|
|
3132
|
+
// TODO: Extract document title more robustly across schema types
|
|
3057
3133
|
title: sourceDocument.name || sourceDocument.title
|
|
3058
3134
|
},
|
|
3059
3135
|
...references
|
|
3060
|
-
], sourceDocumentIds = sourceDocuments.map((doc) => doc._id),
|
|
3061
|
-
devLog.debug("Prepared document set for translation", {
|
|
3062
|
-
...logContext,
|
|
3063
|
-
sourceDocumentIds: sourceDocumentIds.length,
|
|
3064
|
-
totalTranslationJobs: sourceDocumentIds.length * targetLanguages.length
|
|
3065
|
-
});
|
|
3066
|
-
const translationPrompts = await fetchTranslationPrompts({
|
|
3136
|
+
], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), versionedTargetDocumentIds = [], translationPrompts = await fetchTranslationPrompts({
|
|
3067
3137
|
client,
|
|
3068
3138
|
sourceLanguage,
|
|
3069
3139
|
targetLanguages,
|
|
3070
3140
|
sourceDocument
|
|
3071
3141
|
});
|
|
3072
|
-
|
|
3142
|
+
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
|
|
3073
3143
|
const translationTasks = sourceDocumentIds.flatMap(
|
|
3074
3144
|
(documentId) => targetLanguages.map(async (targetLanguage) => {
|
|
3075
3145
|
try {
|
|
3076
3146
|
await translationSemaphore.withPermit(async () => {
|
|
3077
3147
|
const document = await client.withConfig({
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
});
|
|
3083
|
-
if (document && releaseId) {
|
|
3084
|
-
const targetDocumentId = await translateDocument({
|
|
3148
|
+
perspective: "drafts"
|
|
3149
|
+
}).getDocument(documentId);
|
|
3150
|
+
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId2) {
|
|
3151
|
+
const versionedTargetDocumentId = await translateDocument({
|
|
3085
3152
|
dispatch,
|
|
3086
3153
|
client,
|
|
3087
3154
|
sourceDocument: document,
|
|
3155
|
+
sourceLanguage,
|
|
3088
3156
|
targetLanguage,
|
|
3089
3157
|
languageFieldName,
|
|
3090
3158
|
shouldOverwriteFreshTranslations,
|
|
@@ -3092,9 +3160,10 @@ Then return the corrected translation document:`;
|
|
|
3092
3160
|
translationPrompts,
|
|
3093
3161
|
translationMode,
|
|
3094
3162
|
openaiConfig,
|
|
3095
|
-
releaseId
|
|
3163
|
+
releaseId: releaseId2,
|
|
3164
|
+
excludeFields
|
|
3096
3165
|
});
|
|
3097
|
-
|
|
3166
|
+
versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
|
|
3098
3167
|
}
|
|
3099
3168
|
});
|
|
3100
3169
|
} catch (error) {
|
|
@@ -3110,25 +3179,21 @@ Then return the corrected translation document:`;
|
|
|
3110
3179
|
}
|
|
3111
3180
|
})
|
|
3112
3181
|
), results = await Promise.allSettled(translationTasks), succeeded = results.filter((r) => r.status === "fulfilled").length, failed = results.filter((r) => r.status === "rejected").length;
|
|
3113
|
-
devLog.info("Translation tasks
|
|
3182
|
+
devLog.info("Translation tasks completed", {
|
|
3114
3183
|
...logContext,
|
|
3115
3184
|
total: results.length,
|
|
3116
3185
|
succeeded,
|
|
3117
|
-
failed
|
|
3118
|
-
|
|
3119
|
-
...logContext,
|
|
3120
|
-
targetDocumentIds: targetDocumentIds.length
|
|
3186
|
+
failed,
|
|
3187
|
+
targetDocuments: versionedTargetDocumentIds.length
|
|
3121
3188
|
}), dispatch({
|
|
3122
3189
|
type: "SET_OVERALL_STATUS",
|
|
3123
3190
|
payload: "linking_references"
|
|
3124
|
-
}), devLog.debug("Starting reference localization", {
|
|
3125
|
-
...logContext,
|
|
3126
|
-
documentsToLocalize: targetDocumentIds.length
|
|
3127
3191
|
}), await Promise.all(
|
|
3128
|
-
|
|
3192
|
+
versionedTargetDocumentIds.map(async (documentId) => {
|
|
3129
3193
|
await localizeReferences({
|
|
3130
3194
|
client,
|
|
3131
3195
|
documentId,
|
|
3196
|
+
sourceLanguage,
|
|
3132
3197
|
languageFieldName
|
|
3133
3198
|
});
|
|
3134
3199
|
})
|
|
@@ -3138,26 +3203,13 @@ Then return the corrected translation document:`;
|
|
|
3138
3203
|
...logContext,
|
|
3139
3204
|
succeeded,
|
|
3140
3205
|
failed
|
|
3141
|
-
}), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) :
|
|
3206
|
+
}), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
|
|
3142
3207
|
} catch (error) {
|
|
3143
|
-
|
|
3208
|
+
throw devLog.error(
|
|
3144
3209
|
"translateDocumentSet failed with unexpected error",
|
|
3145
3210
|
error instanceof Error ? error : new Error(String(error)),
|
|
3146
3211
|
logContext
|
|
3147
|
-
),
|
|
3148
|
-
try {
|
|
3149
|
-
await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
|
|
3150
|
-
...logContext,
|
|
3151
|
-
releaseId
|
|
3152
|
-
});
|
|
3153
|
-
} catch (cleanupError) {
|
|
3154
|
-
devLog.error(
|
|
3155
|
-
"Failed to cleanup release",
|
|
3156
|
-
cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
|
|
3157
|
-
logContext
|
|
3158
|
-
);
|
|
3159
|
-
}
|
|
3160
|
-
throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
|
|
3212
|
+
), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
|
|
3161
3213
|
}
|
|
3162
3214
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
3163
3215
|
const {
|
|
@@ -3182,7 +3234,8 @@ Then return the corrected translation document:`;
|
|
|
3182
3234
|
maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
|
|
3183
3235
|
maxReferenceDepth: config.maxReferenceDepth,
|
|
3184
3236
|
translationMode,
|
|
3185
|
-
openaiConfig: config.openai
|
|
3237
|
+
openaiConfig: config.openai,
|
|
3238
|
+
excludeFields: config.excludeFields
|
|
3186
3239
|
});
|
|
3187
3240
|
}, [
|
|
3188
3241
|
document,
|
|
@@ -3194,6 +3247,7 @@ Then return the corrected translation document:`;
|
|
|
3194
3247
|
config.maxConcurrentTranslationJobs,
|
|
3195
3248
|
config.maxReferenceDepth,
|
|
3196
3249
|
config.openai,
|
|
3250
|
+
config.excludeFields,
|
|
3197
3251
|
references,
|
|
3198
3252
|
shouldOverwriteFreshTranslations,
|
|
3199
3253
|
translationMode
|
|
@@ -3576,36 +3630,29 @@ Then return the corrected translation document:`;
|
|
|
3576
3630
|
/* @__PURE__ */ jsxs(Code, { size: 1, children: [
|
|
3577
3631
|
job._type,
|
|
3578
3632
|
" \u2014 ",
|
|
3579
|
-
job._id
|
|
3580
|
-
] }),
|
|
3581
|
-
!["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3582
|
-
/* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
|
|
3583
|
-
job.reasoningProgress && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 3, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3584
|
-
/* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "\u{1F914} GPT-5 Reasoning" }),
|
|
3585
|
-
/* @__PURE__ */ jsx(Text, { size: 1, style: { fontStyle: "italic" }, children: job.reasoningProgress })
|
|
3586
|
-
] }) })
|
|
3633
|
+
job._id?.slice(-6) || "unknown"
|
|
3587
3634
|
] }),
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
] }),
|
|
3595
|
-
job.reasoningSummary && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3596
|
-
"\u{1F4A1} ",
|
|
3597
|
-
job.reasoningSummary
|
|
3598
|
-
] }) })
|
|
3635
|
+
!["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
|
|
3636
|
+
job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3637
|
+
job.tokensUsed.toLocaleString(),
|
|
3638
|
+
" tokens (~$",
|
|
3639
|
+
job.estimatedCost?.toFixed(4),
|
|
3640
|
+
")"
|
|
3599
3641
|
] }) : null,
|
|
3600
3642
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3601
3643
|
"Error: ",
|
|
3602
3644
|
job.errorMessage
|
|
3645
|
+
] }) }) : null,
|
|
3646
|
+
job.status === "retrying" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3647
|
+
"Retrying after: ",
|
|
3648
|
+
job.errorMessage
|
|
3603
3649
|
] }) }) : null
|
|
3604
3650
|
] })
|
|
3605
3651
|
] }),
|
|
3606
3652
|
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
|
|
3607
3653
|
job.status === "skipped" ? /* @__PURE__ */ jsx(SkippedBadge, {}) : null,
|
|
3608
|
-
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, {}) : null,
|
|
3654
|
+
job.status === "error" ? /* @__PURE__ */ jsx(ErrorBadge, { retryCount: job.retryCount }) : null,
|
|
3655
|
+
job.status === "retrying" ? /* @__PURE__ */ jsx(RetryingBadge, { retryCount: job.retryCount }) : null,
|
|
3609
3656
|
job.targetId ? /* @__PURE__ */ jsx(
|
|
3610
3657
|
TranslationOpenerButton,
|
|
3611
3658
|
{
|
|
@@ -3685,12 +3732,25 @@ Then return the corrected translation document:`;
|
|
|
3685
3732
|
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "There was already a fresh translation for this document and language." }) }),
|
|
3686
3733
|
children: /* @__PURE__ */ jsx(Badge, { tone: "neutral", children: "Skipped" })
|
|
3687
3734
|
}
|
|
3688
|
-
), ErrorBadge = () => /* @__PURE__ */ jsx(
|
|
3735
|
+
), ErrorBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
|
|
3736
|
+
Tooltip,
|
|
3737
|
+
{
|
|
3738
|
+
portal: !0,
|
|
3739
|
+
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: retryCount && retryCount > 0 ? `This translation failed after ${retryCount} retry attempt${retryCount > 1 ? "s" : ""}.` : "This translation failed. Check the error message for details." }) }),
|
|
3740
|
+
children: /* @__PURE__ */ jsxs(Badge, { tone: "critical", children: [
|
|
3741
|
+
"Failed",
|
|
3742
|
+
retryCount && retryCount > 0 ? ` (${retryCount} retries)` : ""
|
|
3743
|
+
] })
|
|
3744
|
+
}
|
|
3745
|
+
), RetryingBadge = ({ retryCount }) => /* @__PURE__ */ jsx(
|
|
3689
3746
|
Tooltip,
|
|
3690
3747
|
{
|
|
3691
3748
|
portal: !0,
|
|
3692
|
-
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "
|
|
3693
|
-
children: /* @__PURE__ */
|
|
3749
|
+
content: /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Retrying translation after a temporary failure." }) }),
|
|
3750
|
+
children: /* @__PURE__ */ jsxs(Badge, { tone: "caution", children: [
|
|
3751
|
+
"Retry ",
|
|
3752
|
+
retryCount || 1
|
|
3753
|
+
] })
|
|
3694
3754
|
}
|
|
3695
3755
|
), getStatusLabel = (status) => ({
|
|
3696
3756
|
idle: "Idle",
|
|
@@ -3700,6 +3760,7 @@ Then return the corrected translation document:`;
|
|
|
3700
3760
|
translating_with_openai: "Translating with GPT-5...",
|
|
3701
3761
|
reviewing_with_openai: "Reviewing translation with GPT-5...",
|
|
3702
3762
|
updating_metadata: "Saving metadata...",
|
|
3763
|
+
retrying: "Retrying...",
|
|
3703
3764
|
error: "Error",
|
|
3704
3765
|
skipped: "Skipped",
|
|
3705
3766
|
completed: "Completed"
|
|
@@ -3709,26 +3770,9 @@ Then return the corrected translation document:`;
|
|
|
3709
3770
|
"completed"
|
|
3710
3771
|
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
|
|
3711
3772
|
const { overallStatus, document } = useTranslationContext();
|
|
3712
|
-
return devLog.
|
|
3713
|
-
documentId: document._id,
|
|
3714
|
-
overallStatus,
|
|
3715
|
-
component: "TranslationDialog"
|
|
3716
|
-
}), STATES_IN_WHICH_TO_SHOW_CONFIG.includes(overallStatus) ? (devLog.debug("Showing translation config view", {
|
|
3717
|
-
documentId: document._id,
|
|
3718
|
-
overallStatus,
|
|
3719
|
-
component: "TranslationDialog"
|
|
3720
|
-
}), /* @__PURE__ */ jsx(TranslationConfigView, { setIsDialogOpen })) : STATES_IN_WHICH_TO_SHOW_TRANSLATION_PROGRESS.includes(overallStatus) ? (devLog.debug("Showing translation progress panel", {
|
|
3773
|
+
return 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", {
|
|
3721
3774
|
documentId: document._id,
|
|
3722
|
-
overallStatus
|
|
3723
|
-
component: "TranslationDialog"
|
|
3724
|
-
}), /* @__PURE__ */ jsx(TranslationStatusPanel, {})) : STATES_IN_WHICH_TO_SHOW_ERROR.includes(overallStatus) ? (devLog.warn("Translation error state reached", {
|
|
3725
|
-
documentId: document._id,
|
|
3726
|
-
overallStatus,
|
|
3727
|
-
component: "TranslationDialog"
|
|
3728
|
-
}), /* @__PURE__ */ jsx("div", { children: "Error occurred" })) : (devLog.warn("Unhandled translation dialog state", {
|
|
3729
|
-
documentId: document._id,
|
|
3730
|
-
overallStatus,
|
|
3731
|
-
component: "TranslationDialog"
|
|
3775
|
+
overallStatus
|
|
3732
3776
|
}), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
|
|
3733
3777
|
}, getTranslateAction = (config) => ({ draft, published }) => {
|
|
3734
3778
|
const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
|
|
@@ -3755,7 +3799,427 @@ Then return the corrected translation document:`;
|
|
|
3755
3799
|
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
|
|
3756
3800
|
}
|
|
3757
3801
|
} : (devLog.error("Missing config in TranslateAction"), null);
|
|
3758
|
-
},
|
|
3802
|
+
}, translationGlossaryTerm = defineType({
|
|
3803
|
+
name: "translationGlossaryTerm",
|
|
3804
|
+
title: "Translation Glossary Term",
|
|
3805
|
+
icon: TranslateIcon,
|
|
3806
|
+
type: "document",
|
|
3807
|
+
fields: [
|
|
3808
|
+
defineField({
|
|
3809
|
+
name: "translations",
|
|
3810
|
+
title: "Translations",
|
|
3811
|
+
type: "internationalizedArrayString"
|
|
3812
|
+
})
|
|
3813
|
+
],
|
|
3814
|
+
preview: {
|
|
3815
|
+
select: {
|
|
3816
|
+
translations: "translations"
|
|
3817
|
+
},
|
|
3818
|
+
prepare({ translations }) {
|
|
3819
|
+
const enTranslation = translations?.find((t) => t.locale === "en-us"), firstTranslation = translations?.[0];
|
|
3820
|
+
return {
|
|
3821
|
+
title: enTranslation?.value || firstTranslation?.value || "No translation available"
|
|
3822
|
+
};
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
}), translationHistory = defineField({
|
|
3826
|
+
name: "translationHistory",
|
|
3827
|
+
type: "array",
|
|
3828
|
+
of: [
|
|
3829
|
+
{
|
|
3830
|
+
name: "targetLanguageHistory",
|
|
3831
|
+
type: "object",
|
|
3832
|
+
fields: [
|
|
3833
|
+
defineField({
|
|
3834
|
+
name: "language",
|
|
3835
|
+
title: "Language",
|
|
3836
|
+
description: "The language of the target document",
|
|
3837
|
+
type: "string"
|
|
3838
|
+
}),
|
|
3839
|
+
defineField({
|
|
3840
|
+
name: "sourceId",
|
|
3841
|
+
title: "Source ID",
|
|
3842
|
+
description: "The ID of the source document",
|
|
3843
|
+
type: "string"
|
|
3844
|
+
}),
|
|
3845
|
+
defineField({
|
|
3846
|
+
name: "sourceRev",
|
|
3847
|
+
title: "Source Revision",
|
|
3848
|
+
description: "The revision ID of the source document at the time of translation",
|
|
3849
|
+
type: "string"
|
|
3850
|
+
}),
|
|
3851
|
+
defineField({
|
|
3852
|
+
name: "sourceLanguage",
|
|
3853
|
+
title: "Source Language",
|
|
3854
|
+
description: "The language of the source document",
|
|
3855
|
+
type: "string"
|
|
3856
|
+
}),
|
|
3857
|
+
defineField({
|
|
3858
|
+
name: "sourceSnapshot",
|
|
3859
|
+
title: "Source Snapshot",
|
|
3860
|
+
description: "The snapshot of the source document at the time of translation",
|
|
3861
|
+
type: "text"
|
|
3862
|
+
}),
|
|
3863
|
+
defineField({
|
|
3864
|
+
name: "targetId",
|
|
3865
|
+
title: "Target ID",
|
|
3866
|
+
description: "The ID of the target document",
|
|
3867
|
+
type: "string"
|
|
3868
|
+
})
|
|
3869
|
+
]
|
|
3870
|
+
}
|
|
3871
|
+
]
|
|
3872
|
+
}), translationSettings = defineType({
|
|
3873
|
+
name: "translationSettings",
|
|
3874
|
+
title: "Translation Settings",
|
|
3875
|
+
icon: TranslateIcon,
|
|
3876
|
+
type: "document",
|
|
3877
|
+
groups: [
|
|
3878
|
+
{
|
|
3879
|
+
name: "general",
|
|
3880
|
+
title: "General",
|
|
3881
|
+
default: !0
|
|
3882
|
+
},
|
|
3883
|
+
{
|
|
3884
|
+
name: "history",
|
|
3885
|
+
title: "History"
|
|
3886
|
+
}
|
|
3887
|
+
],
|
|
3888
|
+
fields: [
|
|
3889
|
+
defineField({
|
|
3890
|
+
name: "globalPrompt",
|
|
3891
|
+
title: "Global Translation Prompt",
|
|
3892
|
+
description: "A prompt that will given to the Sanity AI Agent for all translation tasks.",
|
|
3893
|
+
type: "text",
|
|
3894
|
+
rows: 5,
|
|
3895
|
+
group: "general"
|
|
3896
|
+
}),
|
|
3897
|
+
defineField({
|
|
3898
|
+
name: "localePrompts",
|
|
3899
|
+
title: "Locale Prompts",
|
|
3900
|
+
description: "Prompts that will be given to the Sanity AI Agent for each locale.",
|
|
3901
|
+
type: "internationalizedArrayText",
|
|
3902
|
+
group: "general"
|
|
3903
|
+
}),
|
|
3904
|
+
defineField({
|
|
3905
|
+
name: "protectedPhrases",
|
|
3906
|
+
title: "Protected Phrases",
|
|
3907
|
+
description: "Phrases that will never be translated.",
|
|
3908
|
+
type: "array",
|
|
3909
|
+
of: [{ type: "string" }],
|
|
3910
|
+
group: "general"
|
|
3911
|
+
}),
|
|
3912
|
+
defineField({
|
|
3913
|
+
name: "changeHistory",
|
|
3914
|
+
title: "Change History",
|
|
3915
|
+
description: "History of changes made via user feedback.",
|
|
3916
|
+
type: "array",
|
|
3917
|
+
readOnly: !0,
|
|
3918
|
+
group: "history",
|
|
3919
|
+
of: [
|
|
3920
|
+
{
|
|
3921
|
+
type: "object",
|
|
3922
|
+
fields: [
|
|
3923
|
+
defineField({
|
|
3924
|
+
name: "timestamp",
|
|
3925
|
+
title: "Timestamp",
|
|
3926
|
+
type: "datetime"
|
|
3927
|
+
}),
|
|
3928
|
+
defineField({
|
|
3929
|
+
name: "changeType",
|
|
3930
|
+
title: "Change Type",
|
|
3931
|
+
type: "string",
|
|
3932
|
+
options: {
|
|
3933
|
+
list: [
|
|
3934
|
+
{ title: "Global Prompt", value: "globalPrompt" },
|
|
3935
|
+
{ title: "Locale Prompt", value: "localePrompt" },
|
|
3936
|
+
{ title: "Protected Phrase", value: "protectedPhrase" },
|
|
3937
|
+
{ title: "Glossary Term", value: "glossaryTerm" }
|
|
3938
|
+
]
|
|
3939
|
+
}
|
|
3940
|
+
}),
|
|
3941
|
+
defineField({
|
|
3942
|
+
name: "description",
|
|
3943
|
+
title: "Description",
|
|
3944
|
+
type: "text",
|
|
3945
|
+
rows: 3
|
|
3946
|
+
}),
|
|
3947
|
+
defineField({
|
|
3948
|
+
name: "locale",
|
|
3949
|
+
title: "Locale",
|
|
3950
|
+
type: "string",
|
|
3951
|
+
description: "Target locale for locale-specific changes"
|
|
3952
|
+
})
|
|
3953
|
+
],
|
|
3954
|
+
preview: {
|
|
3955
|
+
select: {
|
|
3956
|
+
timestamp: "timestamp",
|
|
3957
|
+
changeType: "changeType",
|
|
3958
|
+
description: "description"
|
|
3959
|
+
},
|
|
3960
|
+
prepare({ timestamp, changeType, description }) {
|
|
3961
|
+
return {
|
|
3962
|
+
title: `${changeType} - ${new Date(timestamp).toLocaleDateString()}`,
|
|
3963
|
+
subtitle: description
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
]
|
|
3969
|
+
})
|
|
3970
|
+
],
|
|
3971
|
+
preview: {
|
|
3972
|
+
prepare: () => ({
|
|
3973
|
+
title: "Translation Settings"
|
|
3974
|
+
})
|
|
3975
|
+
}
|
|
3976
|
+
});
|
|
3977
|
+
function isPortableText(value) {
|
|
3978
|
+
return Array.isArray(value) && value.some((item) => item?._type === "block");
|
|
3979
|
+
}
|
|
3980
|
+
const richTextTransform = {
|
|
3981
|
+
// Matcher combines field name pattern with value-based detection
|
|
3982
|
+
// fieldPath pattern acts as a pre-filter for performance
|
|
3983
|
+
// valueMatcher definitively checks if the value is actually Portable Text
|
|
3984
|
+
matcher: {
|
|
3985
|
+
// Pre-filter: Match all fields to enable detection based solely on value
|
|
3986
|
+
fieldPath: /.*/,
|
|
3987
|
+
// Definitive check: Verify the value is actually Portable Text
|
|
3988
|
+
valueMatcher: (value) => Array.isArray(value) && isPortableText(value)
|
|
3989
|
+
},
|
|
3990
|
+
toTranslatable: (value, context) => {
|
|
3991
|
+
if (!Array.isArray(value) || !isPortableText(value))
|
|
3992
|
+
return {
|
|
3993
|
+
_type: "translatable.richtext",
|
|
3994
|
+
translationText: JSON.stringify(value),
|
|
3995
|
+
_originalValue: value
|
|
3996
|
+
};
|
|
3997
|
+
devLog.info("Applying rich text transform with XML serialization", {
|
|
3998
|
+
fieldPath: context.fieldPath,
|
|
3999
|
+
blockCount: value.length
|
|
4000
|
+
});
|
|
4001
|
+
try {
|
|
4002
|
+
const tempDoc = {
|
|
4003
|
+
_id: "temp",
|
|
4004
|
+
_type: "richTextWrapper",
|
|
4005
|
+
_createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4006
|
+
_updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4007
|
+
_rev: "temp",
|
|
4008
|
+
content: value
|
|
4009
|
+
}, serialized = serializeDocument(tempDoc);
|
|
4010
|
+
devLog.debug("Serialized rich text document", {
|
|
4011
|
+
fieldPath: context.fieldPath,
|
|
4012
|
+
serializedLength: serialized.length,
|
|
4013
|
+
preview: serialized.substring(0, 200)
|
|
4014
|
+
});
|
|
4015
|
+
const contentRegex = /<field name="content"[^>]*>([\s\S]*)<\/field>/s, match = serialized.match(contentRegex);
|
|
4016
|
+
if (match?.[1]) {
|
|
4017
|
+
const result = {
|
|
4018
|
+
_type: "translatable.richtext",
|
|
4019
|
+
translationText: match[1].trim(),
|
|
4020
|
+
_originalValue: value
|
|
4021
|
+
};
|
|
4022
|
+
return devLog.debug("Successfully extracted and wrapped rich text", {
|
|
4023
|
+
fieldPath: context.fieldPath,
|
|
4024
|
+
translationTextLength: result.translationText.length
|
|
4025
|
+
}), result;
|
|
4026
|
+
}
|
|
4027
|
+
return devLog.warn("Failed to serialize rich text content - regex did not match", {
|
|
4028
|
+
fieldPath: context.fieldPath,
|
|
4029
|
+
serializedPreview: serialized.substring(0, 500)
|
|
4030
|
+
}), {
|
|
4031
|
+
_type: "translatable.richtext",
|
|
4032
|
+
translationText: JSON.stringify(value),
|
|
4033
|
+
_originalValue: value
|
|
4034
|
+
};
|
|
4035
|
+
} catch (error) {
|
|
4036
|
+
return devLog.error("Error serializing rich text", error, {
|
|
4037
|
+
fieldPath: context.fieldPath
|
|
4038
|
+
}), {
|
|
4039
|
+
_type: "translatable.richtext",
|
|
4040
|
+
translationText: JSON.stringify(value),
|
|
4041
|
+
_originalValue: value
|
|
4042
|
+
};
|
|
4043
|
+
}
|
|
4044
|
+
},
|
|
4045
|
+
fromTranslatable: (translatedValue, originalValue, context) => {
|
|
4046
|
+
if (!translatedValue || translatedValue._type !== "translatable.richtext" || !translatedValue.translationText)
|
|
4047
|
+
return devLog.debug("Value is not a translatable rich text object, returning original", {
|
|
4048
|
+
fieldPath: context.fieldPath,
|
|
4049
|
+
hasCorrectType: translatedValue._type === "translatable.richtext"
|
|
4050
|
+
}), originalValue;
|
|
4051
|
+
devLog.info("Deserializing rich text content", {
|
|
4052
|
+
fieldPath: context.fieldPath,
|
|
4053
|
+
translationTextLength: translatedValue.translationText.length
|
|
4054
|
+
});
|
|
4055
|
+
try {
|
|
4056
|
+
const { translationText, _originalValue } = translatedValue, wrappedSerialized = `<field name="content">
|
|
4057
|
+
${translationText}
|
|
4058
|
+
</field>`;
|
|
4059
|
+
devLog.debug("Prepared wrapped serialized content", {
|
|
4060
|
+
fieldPath: context.fieldPath,
|
|
4061
|
+
wrappedLength: wrappedSerialized.length,
|
|
4062
|
+
preview: wrappedSerialized.substring(0, 300)
|
|
4063
|
+
});
|
|
4064
|
+
const tempOriginalDoc = {
|
|
4065
|
+
_id: "temp",
|
|
4066
|
+
_type: "richTextWrapper",
|
|
4067
|
+
_createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4068
|
+
_updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4069
|
+
_rev: "temp",
|
|
4070
|
+
content: originalValue
|
|
4071
|
+
}, result = deserializeDocument(tempOriginalDoc, wrappedSerialized);
|
|
4072
|
+
return devLog.debug("Deserialization result", {
|
|
4073
|
+
fieldPath: context.fieldPath,
|
|
4074
|
+
hasContent: !!result.content,
|
|
4075
|
+
contentType: Array.isArray(result.content) ? "array" : typeof result.content,
|
|
4076
|
+
contentLength: Array.isArray(result.content) ? result.content.length : void 0
|
|
4077
|
+
}), result.content ? result.content : (devLog.warn("Deserialization returned no content, returning original", {
|
|
4078
|
+
fieldPath: context.fieldPath,
|
|
4079
|
+
resultKeys: Object.keys(result)
|
|
4080
|
+
}), originalValue);
|
|
4081
|
+
} catch (error) {
|
|
4082
|
+
return devLog.error("Failed to deserialize rich text", error, {
|
|
4083
|
+
fieldPath: context.fieldPath
|
|
4084
|
+
}), originalValue;
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
}, vttCaptionTransform = {
|
|
4088
|
+
matcher: {
|
|
4089
|
+
fieldName: "captions",
|
|
4090
|
+
schemaType: "video"
|
|
4091
|
+
},
|
|
4092
|
+
toTranslatable: (captionsArray, context) => {
|
|
4093
|
+
if (devLog.info("VTT captions toTranslatable - input:", {
|
|
4094
|
+
fieldPath: context.fieldPath,
|
|
4095
|
+
isArray: Array.isArray(captionsArray),
|
|
4096
|
+
count: Array.isArray(captionsArray) ? captionsArray.length : 0,
|
|
4097
|
+
firstCaption: Array.isArray(captionsArray) && captionsArray.length > 0 ? captionsArray[0] : null
|
|
4098
|
+
}), !Array.isArray(captionsArray))
|
|
4099
|
+
return devLog.debug("VTT caption field is not an array, returning empty transform", {
|
|
4100
|
+
fieldPath: context.fieldPath,
|
|
4101
|
+
value: captionsArray
|
|
4102
|
+
}), {
|
|
4103
|
+
_type: "translatable.vtt.captions",
|
|
4104
|
+
translationText: "",
|
|
4105
|
+
_originalMetadata: []
|
|
4106
|
+
};
|
|
4107
|
+
devLog.info("Applying VTT caption hybrid transform", {
|
|
4108
|
+
fieldPath: context.fieldPath,
|
|
4109
|
+
count: captionsArray.length
|
|
4110
|
+
});
|
|
4111
|
+
const translationText = serializeVTTCaptionText(captionsArray), originalMetadata = captionsArray.map((caption, index) => {
|
|
4112
|
+
const { text: _text, ...metadata } = caption;
|
|
4113
|
+
return {
|
|
4114
|
+
...metadata,
|
|
4115
|
+
_originalIndex: index
|
|
4116
|
+
};
|
|
4117
|
+
});
|
|
4118
|
+
return devLog.info("VTT captions serialized:", {
|
|
4119
|
+
fieldPath: context.fieldPath,
|
|
4120
|
+
translationTextLength: translationText.length,
|
|
4121
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
|
|
4122
|
+
metadataCount: originalMetadata.length,
|
|
4123
|
+
metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
|
|
4124
|
+
}), {
|
|
4125
|
+
_type: "translatable.vtt.captions",
|
|
4126
|
+
translationText,
|
|
4127
|
+
_originalMetadata: originalMetadata
|
|
4128
|
+
};
|
|
4129
|
+
},
|
|
4130
|
+
fromTranslatable: (translatedValue, _originalValue, context) => {
|
|
4131
|
+
if (devLog.info("VTT captions fromTranslatable - input:", {
|
|
4132
|
+
fieldPath: context.fieldPath,
|
|
4133
|
+
valueType: typeof translatedValue,
|
|
4134
|
+
isCorrectType: translatedValue._type === "translatable.vtt.captions",
|
|
4135
|
+
hasText: !!translatedValue.translationText,
|
|
4136
|
+
hasMetadata: Array.isArray(translatedValue._originalMetadata)
|
|
4137
|
+
}), !translatedValue || translatedValue._type !== "translatable.vtt.captions" || !translatedValue.translationText || !Array.isArray(translatedValue._originalMetadata))
|
|
4138
|
+
return devLog.debug("Value is not a translatable VTT caption object, skipping transform", {
|
|
4139
|
+
fieldPath: context.fieldPath,
|
|
4140
|
+
valueType: typeof translatedValue,
|
|
4141
|
+
translatedValue
|
|
4142
|
+
}), [];
|
|
4143
|
+
devLog.info("Processing translated VTT captions", {
|
|
4144
|
+
fieldPath: context.fieldPath,
|
|
4145
|
+
metadataCount: translatedValue._originalMetadata.length
|
|
4146
|
+
});
|
|
4147
|
+
const { translationText, _originalMetadata } = translatedValue;
|
|
4148
|
+
devLog.info("VTT captions translationText:", {
|
|
4149
|
+
fieldPath: context.fieldPath,
|
|
4150
|
+
translationTextLength: translationText.length,
|
|
4151
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
|
|
4152
|
+
});
|
|
4153
|
+
const result = deserializeVTTCaptionText(translationText, _originalMetadata);
|
|
4154
|
+
return devLog.info("VTT captions final result:", {
|
|
4155
|
+
fieldPath: context.fieldPath,
|
|
4156
|
+
resultCount: result.length,
|
|
4157
|
+
resultExample: result.length > 0 ? result[0] : null
|
|
4158
|
+
}), result;
|
|
4159
|
+
}
|
|
4160
|
+
};
|
|
4161
|
+
function serializeVTTCaptionText(captions) {
|
|
4162
|
+
if (devLog.debug("serializeVTTCaptionText - input:", {
|
|
4163
|
+
captionsCount: captions.length,
|
|
4164
|
+
firstCaption: captions.length > 0 ? captions[0] : null
|
|
4165
|
+
}), !Array.isArray(captions) || captions.length === 0)
|
|
4166
|
+
return "";
|
|
4167
|
+
const result = captions.map((caption, index) => (devLog.debug(`Processing caption ${index}:`, {
|
|
4168
|
+
text: caption.text,
|
|
4169
|
+
hasText: typeof caption.text == "string"
|
|
4170
|
+
}), `[${index}] ${caption.text || ""}`)).join(`
|
|
4171
|
+
`);
|
|
4172
|
+
return devLog.debug("serializeVTTCaptionText - output:", {
|
|
4173
|
+
resultLength: result.length,
|
|
4174
|
+
resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
|
|
4175
|
+
}), result;
|
|
4176
|
+
}
|
|
4177
|
+
function deserializeVTTCaptionText(translationText, originalMetadata) {
|
|
4178
|
+
if (devLog.debug("deserializeVTTCaptionText - input:", {
|
|
4179
|
+
translationTextLength: translationText.length,
|
|
4180
|
+
translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
|
|
4181
|
+
metadataCount: originalMetadata.length,
|
|
4182
|
+
metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
|
|
4183
|
+
}), !translationText || translationText.trim() === "")
|
|
4184
|
+
return [];
|
|
4185
|
+
const result = [], lines = translationText.split(`
|
|
4186
|
+
`), lineRegex = /^\[(\d+)\]\s*(.*)$/;
|
|
4187
|
+
for (const line of lines) {
|
|
4188
|
+
const match = line.match(lineRegex);
|
|
4189
|
+
if (!match) {
|
|
4190
|
+
devLog.warn("Failed to parse caption line", { line });
|
|
4191
|
+
continue;
|
|
4192
|
+
}
|
|
4193
|
+
const index = Number.parseInt(match[1], 10), text = match[2];
|
|
4194
|
+
devLog.debug("Found caption:", {
|
|
4195
|
+
index,
|
|
4196
|
+
text
|
|
4197
|
+
});
|
|
4198
|
+
const metadata = originalMetadata.find((m) => m._originalIndex === index);
|
|
4199
|
+
if (metadata) {
|
|
4200
|
+
devLog.debug(`Found matching metadata for index ${index}`);
|
|
4201
|
+
const { _originalIndex, ...metadataWithoutIndex } = metadata;
|
|
4202
|
+
result.push({
|
|
4203
|
+
...metadataWithoutIndex,
|
|
4204
|
+
text
|
|
4205
|
+
});
|
|
4206
|
+
} else
|
|
4207
|
+
devLog.warn("Missing metadata for caption", { index }), result.push({
|
|
4208
|
+
_type: "vtt.cue",
|
|
4209
|
+
_key: `caption-${index}`,
|
|
4210
|
+
text,
|
|
4211
|
+
startTime: 0,
|
|
4212
|
+
endTime: 0
|
|
4213
|
+
});
|
|
4214
|
+
}
|
|
4215
|
+
return result.length < originalMetadata.length && devLog.warn(`Missing captions in translation: found ${result.length} out of ${originalMetadata.length}`), devLog.debug("deserializeVTTCaptionText - output:", {
|
|
4216
|
+
resultCount: result.length,
|
|
4217
|
+
resultExample: result.length > 0 ? result[0] : null
|
|
4218
|
+
}), result;
|
|
4219
|
+
}
|
|
4220
|
+
const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
|
|
4221
|
+
globalTransformRegistry.register(transform);
|
|
4222
|
+
})), {
|
|
3759
4223
|
name: "sanity-plugin-ai-translation",
|
|
3760
4224
|
document: {
|
|
3761
4225
|
actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
|
|
@@ -3765,7 +4229,13 @@ Then return the corrected translation document:`;
|
|
|
3765
4229
|
}
|
|
3766
4230
|
})), translationHistorySchema = translationHistory;
|
|
3767
4231
|
export {
|
|
4232
|
+
COMMON_ENUM_FIELDS,
|
|
4233
|
+
PREDEFINED_EXCLUSIONS,
|
|
3768
4234
|
aiTranslationPlugin,
|
|
3769
|
-
|
|
4235
|
+
filterExcludedFields,
|
|
4236
|
+
restoreExcludedFields,
|
|
4237
|
+
richTextTransform,
|
|
4238
|
+
translationHistorySchema,
|
|
4239
|
+
vttCaptionTransform
|
|
3770
4240
|
};
|
|
3771
4241
|
//# sourceMappingURL=index.js.map
|