@braze/sanity-plugin-ai-translation 0.2.2 → 0.2.3
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 +303 -4
- package/dist/index.js +1265 -1265
- package/dist/index.js.map +1 -1
- package/package.json +5 -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 = [];
|
|
@@ -1755,7 +1500,12 @@ const fetchTranslationPrompts = async ({
|
|
|
1755
1500
|
}
|
|
1756
1501
|
};
|
|
1757
1502
|
return recursiveSearch(obj, []), results;
|
|
1758
|
-
}, localizeReferences = async ({
|
|
1503
|
+
}, localizeReferences = async ({
|
|
1504
|
+
client,
|
|
1505
|
+
documentId,
|
|
1506
|
+
sourceLanguage,
|
|
1507
|
+
languageFieldName
|
|
1508
|
+
}) => {
|
|
1759
1509
|
try {
|
|
1760
1510
|
const doc = await client.fetch("*[_id == $documentId][0]", { documentId });
|
|
1761
1511
|
if (!doc) {
|
|
@@ -1777,25 +1527,26 @@ const fetchTranslationPrompts = async ({
|
|
|
1777
1527
|
const refs = findKeyValues(doc, "_ref");
|
|
1778
1528
|
if (!refs?.length)
|
|
1779
1529
|
return;
|
|
1780
|
-
await updateReferences(documentId, client, refs, targetLanguage);
|
|
1530
|
+
await updateReferences(documentId, client, refs, sourceLanguage, targetLanguage);
|
|
1781
1531
|
} catch (error) {
|
|
1782
1532
|
devLog.error("Error localizing references", error instanceof Error ? error : new Error(String(error)), {
|
|
1783
1533
|
documentId,
|
|
1784
1534
|
component: "localizeReferences"
|
|
1785
1535
|
});
|
|
1786
1536
|
}
|
|
1787
|
-
}, updateReferences = async (documentId, client, refs, targetLanguage) => {
|
|
1537
|
+
}, updateReferences = async (documentId, client, refs, sourceLanguage, targetLanguage) => {
|
|
1788
1538
|
if (!refs?.length)
|
|
1789
1539
|
return [];
|
|
1790
|
-
const refIds = refs.map((r) =>
|
|
1791
|
-
`*[_type == $translationMetadataSchemaType &&
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1540
|
+
const refIds = refs.map((r) => getPublishedId(r.value)), translations = await client.fetch(
|
|
1541
|
+
`*[_type == $translationMetadataSchemaType && array::intersects(translations[].value._ref, $refIds)] {
|
|
1542
|
+
"sourceId": translations[_key == $sourceLanguage][0].value._ref,
|
|
1543
|
+
"targetId": translations[_key == $targetLanguage][0].value._ref,
|
|
1544
|
+
"targetType": translations[_key == $sourceLanguage][0].value -> _type
|
|
1545
|
+
}`,
|
|
1796
1546
|
{
|
|
1797
1547
|
translationMetadataSchemaType: TRANSLATION_METADATA_TYPE,
|
|
1798
1548
|
refIds,
|
|
1549
|
+
sourceLanguage,
|
|
1799
1550
|
targetLanguage
|
|
1800
1551
|
}
|
|
1801
1552
|
), translationMap = /* @__PURE__ */ new Map();
|
|
@@ -1804,11 +1555,10 @@ const fetchTranslationPrompts = async ({
|
|
|
1804
1555
|
const patch = client.patch(documentId);
|
|
1805
1556
|
let hasChanges = !1;
|
|
1806
1557
|
for (const ref of refs) {
|
|
1807
|
-
const sourceId =
|
|
1808
|
-
newId && (patch.set({ [pathToString(ref.path)]:
|
|
1558
|
+
const sourceId = getPublishedId(ref.value), newId = translationMap.get(sourceId);
|
|
1559
|
+
newId && (patch.set({ [pathToString(ref.path)]: getPublishedId(newId) }), patch.set({
|
|
1809
1560
|
[`${pathToString(ref.path.slice(0, -1))}._strengthenOnPublish`]: {
|
|
1810
|
-
|
|
1811
|
-
_weak: !1
|
|
1561
|
+
type: translations.find((t) => t.targetId === getPublishedId(newId))?.targetType || void 0
|
|
1812
1562
|
}
|
|
1813
1563
|
}), hasChanges = !0);
|
|
1814
1564
|
}
|
|
@@ -1833,7 +1583,9 @@ const fetchTranslationPrompts = async ({
|
|
|
1833
1583
|
payload
|
|
1834
1584
|
});
|
|
1835
1585
|
}
|
|
1836
|
-
},
|
|
1586
|
+
}, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
1587
|
+
(translation) => translation._key === targetLanguage
|
|
1588
|
+
)?.value._ref || null, getTranslationMetadata = async ({
|
|
1837
1589
|
client,
|
|
1838
1590
|
sourceDocument,
|
|
1839
1591
|
sourceLanguage
|
|
@@ -1882,7 +1634,7 @@ const fetchTranslationPrompts = async ({
|
|
|
1882
1634
|
_type: "internationalizedArrayReferenceValue",
|
|
1883
1635
|
value: {
|
|
1884
1636
|
_type: "reference",
|
|
1885
|
-
_ref:
|
|
1637
|
+
_ref: getPublishedId(newDocument._id),
|
|
1886
1638
|
_weak: !0,
|
|
1887
1639
|
_strengthenOnPublish: {
|
|
1888
1640
|
_type: "reference",
|
|
@@ -1892,7 +1644,7 @@ const fetchTranslationPrompts = async ({
|
|
|
1892
1644
|
}, newTranslationHistory = {
|
|
1893
1645
|
_key: targetLanguage,
|
|
1894
1646
|
language: targetLanguage,
|
|
1895
|
-
sourceId:
|
|
1647
|
+
sourceId: getPublishedId(sourceDocument._id),
|
|
1896
1648
|
sourceRev: sourceDocument._rev,
|
|
1897
1649
|
sourceLanguage,
|
|
1898
1650
|
sourceSnapshot: JSON.stringify(sourceDocument),
|
|
@@ -1913,79 +1665,223 @@ const fetchTranslationPrompts = async ({
|
|
|
1913
1665
|
component: "updateTranslationMetadata"
|
|
1914
1666
|
}), error;
|
|
1915
1667
|
}
|
|
1916
|
-
}, setJobStatus = (dispatch, _id, language, status, payload) => {
|
|
1917
|
-
dispatch({
|
|
1918
|
-
type: "SET_TRANSLATION_JOB_STATUS",
|
|
1919
|
-
payload: defaults(payload, {
|
|
1920
|
-
_id,
|
|
1921
|
-
language,
|
|
1922
|
-
status
|
|
1923
|
-
})
|
|
1924
|
-
});
|
|
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
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1668
|
+
}, setJobStatus = (dispatch, _id, language, status, payload) => {
|
|
1669
|
+
dispatch({
|
|
1670
|
+
type: "SET_TRANSLATION_JOB_STATUS",
|
|
1671
|
+
payload: defaults(payload, {
|
|
1672
|
+
_id,
|
|
1673
|
+
language,
|
|
1674
|
+
status
|
|
1675
|
+
})
|
|
1676
|
+
});
|
|
1677
|
+
};
|
|
1678
|
+
function applyTransforms({
|
|
1679
|
+
document,
|
|
1680
|
+
registry,
|
|
1681
|
+
schemaType,
|
|
1682
|
+
parentPath = ""
|
|
1683
|
+
}) {
|
|
1684
|
+
if (!document)
|
|
1685
|
+
return { transformedDocument: document, originalStructure: {} };
|
|
1686
|
+
if (typeof document != "object")
|
|
1687
|
+
return { transformedDocument: document, originalStructure: {} };
|
|
1688
|
+
if (Array.isArray(document)) {
|
|
1689
|
+
const originalStructure2 = {};
|
|
1690
|
+
return { transformedDocument: document.map((item, index) => {
|
|
1691
|
+
const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`, { transformedDocument: transformedItem, originalStructure: itemOriginalStructure } = applyTransforms({
|
|
1692
|
+
document: item,
|
|
1693
|
+
registry,
|
|
1694
|
+
schemaType,
|
|
1695
|
+
parentPath: itemPath
|
|
1696
|
+
});
|
|
1697
|
+
return Object.assign(originalStructure2, itemOriginalStructure), transformedItem;
|
|
1698
|
+
}), originalStructure: originalStructure2 };
|
|
1699
|
+
}
|
|
1700
|
+
const transformedObj = {}, originalStructure = {};
|
|
1701
|
+
for (const [key, value] of Object.entries(document)) {
|
|
1702
|
+
const fieldPath = parentPath ? `${parentPath}.${key}` : key;
|
|
1703
|
+
if (key.startsWith("_") && key !== "_type") {
|
|
1704
|
+
transformedObj[key] = value;
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
const transform = registry.findTransform(schemaType, fieldPath, value, document);
|
|
1708
|
+
if (transform) {
|
|
1709
|
+
devLog.debug("Applying transform", { fieldPath, schemaType });
|
|
1710
|
+
const context = {
|
|
1711
|
+
fieldPath,
|
|
1712
|
+
schemaType,
|
|
1713
|
+
document
|
|
1714
|
+
}, transformedValue = transform.toTranslatable(value, context);
|
|
1715
|
+
transformedObj[key] = transformedValue, originalStructure[fieldPath] = {
|
|
1716
|
+
value,
|
|
1717
|
+
transform
|
|
1718
|
+
};
|
|
1719
|
+
} else {
|
|
1720
|
+
const { transformedDocument: transformedValue, originalStructure: valueOriginalStructure } = applyTransforms({
|
|
1721
|
+
document: value,
|
|
1722
|
+
registry,
|
|
1723
|
+
schemaType,
|
|
1724
|
+
parentPath: fieldPath
|
|
1725
|
+
});
|
|
1726
|
+
Object.assign(originalStructure, valueOriginalStructure), transformedObj[key] = transformedValue;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return { transformedDocument: transformedObj, originalStructure };
|
|
1730
|
+
}
|
|
1731
|
+
function restoreTransforms({
|
|
1732
|
+
translatedDocument,
|
|
1733
|
+
originalStructure,
|
|
1734
|
+
registry,
|
|
1735
|
+
schemaType,
|
|
1736
|
+
parentPath = ""
|
|
1737
|
+
}) {
|
|
1738
|
+
if (translatedDocument == null || typeof translatedDocument != "object")
|
|
1739
|
+
return translatedDocument;
|
|
1740
|
+
if (Array.isArray(translatedDocument))
|
|
1741
|
+
return translatedDocument.map((item, index) => {
|
|
1742
|
+
const itemPath = parentPath ? `${parentPath}[${index}]` : `[${index}]`;
|
|
1743
|
+
return restoreTransforms({
|
|
1744
|
+
translatedDocument: item,
|
|
1745
|
+
originalStructure,
|
|
1746
|
+
registry,
|
|
1747
|
+
schemaType,
|
|
1748
|
+
parentPath: itemPath
|
|
1749
|
+
});
|
|
1750
|
+
});
|
|
1751
|
+
const originalData = originalStructure[parentPath];
|
|
1752
|
+
if (originalData) {
|
|
1753
|
+
const { value: originalValue, transform } = originalData, context = {
|
|
1754
|
+
fieldPath: parentPath,
|
|
1755
|
+
schemaType,
|
|
1756
|
+
document: translatedDocument
|
|
1757
|
+
};
|
|
1758
|
+
return devLog.debug("Restoring transform", { fieldPath: parentPath, schemaType }), transform.fromTranslatable(translatedDocument, originalValue, context);
|
|
1759
|
+
}
|
|
1760
|
+
const restoredObj = {}, translatedObj = translatedDocument;
|
|
1761
|
+
for (const [key, value] of Object.entries(translatedObj)) {
|
|
1762
|
+
const fieldPath = parentPath ? `${parentPath}.${key}` : key, fieldOriginalData = originalStructure[fieldPath];
|
|
1763
|
+
if (fieldOriginalData) {
|
|
1764
|
+
const { value: originalValue, transform } = fieldOriginalData, context = {
|
|
1765
|
+
fieldPath,
|
|
1766
|
+
schemaType,
|
|
1767
|
+
document: translatedDocument
|
|
1768
|
+
};
|
|
1769
|
+
devLog.debug("Restoring transform for field", { fieldPath, schemaType }), restoredObj[key] = transform.fromTranslatable(value, originalValue, context);
|
|
1770
|
+
} else
|
|
1771
|
+
restoredObj[key] = restoreTransforms({
|
|
1772
|
+
translatedDocument: value,
|
|
1773
|
+
originalStructure,
|
|
1774
|
+
registry,
|
|
1775
|
+
schemaType,
|
|
1776
|
+
parentPath: fieldPath
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
return restoredObj;
|
|
1780
|
+
}
|
|
1781
|
+
class TransformRegistry {
|
|
1782
|
+
transforms = [];
|
|
1783
|
+
/**
|
|
1784
|
+
* Register a transform with the registry.
|
|
1785
|
+
* @param transform - Field transform to register
|
|
1786
|
+
*/
|
|
1787
|
+
register(transform) {
|
|
1788
|
+
devLog.debug("Registering field transform", {
|
|
1789
|
+
matcher: transform.matcher
|
|
1790
|
+
}), this.transforms.push(transform);
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Find a matching transform for the given field.
|
|
1794
|
+
* If multiple transforms match, returns the first one (order matters!).
|
|
1795
|
+
* @param schemaType - Document schema type
|
|
1796
|
+
* @param fieldPath - Path to the field
|
|
1797
|
+
* @param value - Optional field value for value-based matching
|
|
1798
|
+
* @param document - Optional full document for context
|
|
1799
|
+
* @returns Matching transform or undefined if none found
|
|
1800
|
+
*/
|
|
1801
|
+
findTransform(schemaType, fieldPath, value, document) {
|
|
1802
|
+
const matches = [];
|
|
1803
|
+
for (const transform of this.transforms) {
|
|
1804
|
+
const { matcher } = transform;
|
|
1805
|
+
if (!(matcher.schemaType && matcher.schemaType !== schemaType)) {
|
|
1806
|
+
if (matcher.fieldPath) {
|
|
1807
|
+
if (typeof matcher.fieldPath == "string") {
|
|
1808
|
+
if (matcher.fieldPath !== fieldPath)
|
|
1809
|
+
continue;
|
|
1810
|
+
} else if (matcher.fieldPath instanceof RegExp && !matcher.fieldPath.test(fieldPath))
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
if (matcher.fieldName) {
|
|
1814
|
+
const segments = fieldPath.split(".");
|
|
1815
|
+
if (segments[segments.length - 1]?.split("[")[0] !== matcher.fieldName)
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
if (matcher.valueMatcher) {
|
|
1819
|
+
if (value === void 0)
|
|
1820
|
+
continue;
|
|
1821
|
+
const context = {
|
|
1822
|
+
fieldPath,
|
|
1823
|
+
schemaType,
|
|
1824
|
+
document: document || {}
|
|
1825
|
+
};
|
|
1826
|
+
if (!matcher.valueMatcher(value, context))
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
matches.push(transform);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
return matches.length > 1 && devLog.warn("Multiple transforms match the same field - using first match", {
|
|
1833
|
+
schemaType,
|
|
1834
|
+
fieldPath,
|
|
1835
|
+
matchCount: matches.length,
|
|
1836
|
+
firstMatcher: matches[0].matcher
|
|
1837
|
+
}), matches[0];
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKey, request) => (request.model || "gpt-5") === "gpt-5" ? await callGPT5ResponsesAPI(apiKey, request) : await callChatCompletionsAPI(apiKey, request), callGPT5ResponsesAPI = async (apiKey, request) => {
|
|
1841
|
+
const model = request.model || "gpt-5", requestBody = {
|
|
1842
|
+
model: "gpt-5.1",
|
|
1843
|
+
input: request.messages
|
|
1844
|
+
// Uses 'input' instead of 'messages'
|
|
1962
1845
|
};
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
})
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
id: sourceLanugageId,
|
|
1974
|
-
title: sourceLanugageId
|
|
1846
|
+
request.reasoningEffort && (requestBody.reasoning = {
|
|
1847
|
+
effort: request.reasoningEffort,
|
|
1848
|
+
summary: "auto"
|
|
1849
|
+
// Enable automatic reasoning summaries
|
|
1850
|
+
});
|
|
1851
|
+
const response = await fetch("https://api.openai.com/v1/responses", {
|
|
1852
|
+
method: "POST",
|
|
1853
|
+
headers: {
|
|
1854
|
+
"Content-Type": "application/json",
|
|
1855
|
+
Authorization: `Bearer ${apiKey}`
|
|
1975
1856
|
},
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1857
|
+
body: JSON.stringify(requestBody)
|
|
1858
|
+
});
|
|
1859
|
+
if (!response.ok) {
|
|
1860
|
+
const error = await response.json();
|
|
1861
|
+
throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
|
|
1862
|
+
}
|
|
1863
|
+
const result = await response.json(), tokensUsed = {
|
|
1864
|
+
prompt: result.usage?.input_tokens || 0,
|
|
1865
|
+
completion: result.usage?.output_tokens || 0,
|
|
1866
|
+
reasoning: result.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
1867
|
+
total: result.usage?.total_tokens || 0
|
|
1868
|
+
}, cost = calculateCost(tokensUsed, model);
|
|
1869
|
+
let content = "";
|
|
1870
|
+
if (result.output && Array.isArray(result.output)) {
|
|
1871
|
+
const messageItem = result.output.find((item) => item.type === "message");
|
|
1872
|
+
messageItem?.content && Array.isArray(messageItem.content) && (content = messageItem.content.find((item) => item.type === "output_text")?.text || "");
|
|
1873
|
+
}
|
|
1874
|
+
return content || (content = result.output_text || result.text || ""), {
|
|
1875
|
+
content,
|
|
1876
|
+
tokensUsed,
|
|
1877
|
+
cost
|
|
1980
1878
|
};
|
|
1981
|
-
},
|
|
1982
|
-
const model = request.model || "gpt-
|
|
1879
|
+
}, callChatCompletionsAPI = async (apiKey, request) => {
|
|
1880
|
+
const model = request.model || "gpt-4o", requestBody = {
|
|
1983
1881
|
model,
|
|
1984
1882
|
messages: request.messages,
|
|
1985
|
-
|
|
1986
|
-
}
|
|
1987
|
-
isGPT5 || (requestBody.temperature = request.temperature ?? 0.3), isGPT5 && (requestBody.reasoning_effort = request.reasoningEffort || "medium", requestBody.verbosity = request.verbosity || "medium");
|
|
1988
|
-
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1883
|
+
temperature: request.temperature ?? 0.3
|
|
1884
|
+
}, response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1989
1885
|
method: "POST",
|
|
1990
1886
|
headers: {
|
|
1991
1887
|
"Content-Type": "application/json",
|
|
@@ -1995,88 +1891,30 @@ const fetchTranslationPrompts = async ({
|
|
|
1995
1891
|
});
|
|
1996
1892
|
if (!response.ok) {
|
|
1997
1893
|
const error = await response.json();
|
|
1998
|
-
throw new Error(`
|
|
1894
|
+
throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
|
|
1999
1895
|
}
|
|
2000
|
-
if (useStreaming && response.body && request.onReasoningChunk)
|
|
2001
|
-
return await handleStreamingResponse(response, request.onReasoningChunk, model);
|
|
2002
1896
|
const result = await response.json(), usage = result.usage || {}, tokensUsed = {
|
|
2003
1897
|
prompt: usage.prompt_tokens || 0,
|
|
2004
1898
|
completion: usage.completion_tokens || 0,
|
|
2005
1899
|
reasoning: usage.reasoning_tokens || 0,
|
|
2006
|
-
// New in GPT-5
|
|
2007
1900
|
total: usage.total_tokens || 0
|
|
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);
|
|
1901
|
+
}, cost = calculateCost(tokensUsed, model);
|
|
2059
1902
|
return {
|
|
2060
|
-
content:
|
|
1903
|
+
content: result.choices[0].message.content || "",
|
|
2061
1904
|
tokensUsed,
|
|
2062
|
-
cost
|
|
2063
|
-
reasoningSummary: reasoningBuffer || void 0
|
|
1905
|
+
cost
|
|
2064
1906
|
};
|
|
2065
|
-
},
|
|
1907
|
+
}, calculateCost = (tokens, model) => {
|
|
2066
1908
|
const pricing = {
|
|
2067
1909
|
"gpt-5": {
|
|
2068
1910
|
input: 2e-6,
|
|
2069
|
-
// $0.002 per 1k input tokens
|
|
2070
1911
|
output: 1e-5,
|
|
2071
|
-
// $0.010 per 1k output tokens
|
|
2072
1912
|
reasoning: 5e-6
|
|
2073
|
-
// $0.005 per 1k reasoning tokens
|
|
2074
1913
|
},
|
|
2075
1914
|
"gpt-4o": {
|
|
2076
1915
|
input: 5e-6,
|
|
2077
1916
|
output: 14999999999999999e-21,
|
|
2078
1917
|
reasoning: 0
|
|
2079
|
-
// No reasoning tokens in GPT-4
|
|
2080
1918
|
},
|
|
2081
1919
|
"gpt-4-turbo": {
|
|
2082
1920
|
input: 1e-5,
|
|
@@ -2096,9 +1934,52 @@ const fetchTranslationPrompts = async ({
|
|
|
2096
1934
|
}, parseReviewFormat = (text) => {
|
|
2097
1935
|
const root = [], stack = [], lines = text.split(`
|
|
2098
1936
|
`);
|
|
2099
|
-
for (
|
|
2100
|
-
const trimmed =
|
|
1937
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1938
|
+
const trimmed = lines[i].trim();
|
|
2101
1939
|
if (!trimmed) continue;
|
|
1940
|
+
const compactStringMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
1941
|
+
if (compactStringMatch && !trimmed.includes("<")) {
|
|
1942
|
+
const [, fieldName, text2] = compactStringMatch, node = {
|
|
1943
|
+
type: "field",
|
|
1944
|
+
name: fieldName,
|
|
1945
|
+
text: unescapeXml(text2),
|
|
1946
|
+
attributes: {},
|
|
1947
|
+
children: []
|
|
1948
|
+
};
|
|
1949
|
+
if (stack.length > 0) {
|
|
1950
|
+
const parent = stack[stack.length - 1];
|
|
1951
|
+
parent && parent.children.push(node);
|
|
1952
|
+
} else
|
|
1953
|
+
root.push(node);
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
const compactStartMatch = trimmed.match(/^---\s+(\w+)\s+---$/);
|
|
1957
|
+
if (compactStartMatch) {
|
|
1958
|
+
const fieldName = compactStartMatch[1], contentLines = [];
|
|
1959
|
+
for (i++; i < lines.length; ) {
|
|
1960
|
+
const currentLine = lines[i];
|
|
1961
|
+
if (currentLine.trim().match(new RegExp(`^---\\s+/${fieldName}\\s+---$`)))
|
|
1962
|
+
break;
|
|
1963
|
+
contentLines.push(currentLine), i++;
|
|
1964
|
+
}
|
|
1965
|
+
const content = contentLines.join(`
|
|
1966
|
+
`);
|
|
1967
|
+
let contentType = "translatable.vtt.captions";
|
|
1968
|
+
(content.includes("<block") || content.includes("<span")) && (contentType = "translatable.richtext");
|
|
1969
|
+
const node = {
|
|
1970
|
+
type: "field",
|
|
1971
|
+
name: fieldName,
|
|
1972
|
+
text: content,
|
|
1973
|
+
attributes: { type: contentType },
|
|
1974
|
+
children: []
|
|
1975
|
+
};
|
|
1976
|
+
if (stack.length > 0) {
|
|
1977
|
+
const parent = stack[stack.length - 1];
|
|
1978
|
+
parent && parent.children.push(node);
|
|
1979
|
+
} else
|
|
1980
|
+
root.push(node);
|
|
1981
|
+
continue;
|
|
1982
|
+
}
|
|
2102
1983
|
const openMatch = trimmed.match(/^<(field|block|span)([^>]*)>(.*)$/);
|
|
2103
1984
|
if (openMatch) {
|
|
2104
1985
|
const [, tagName, attrsStr, content] = openMatch, attributes = parseAttributes(attrsStr), node = {
|
|
@@ -2121,7 +2002,21 @@ const fetchTranslationPrompts = async ({
|
|
|
2121
2002
|
parent && parent.children.push(node);
|
|
2122
2003
|
} else
|
|
2123
2004
|
root.push(node);
|
|
2124
|
-
else
|
|
2005
|
+
else if (attributes.type === "translatable.vtt.captions" || attributes.type === "translatable.richtext") {
|
|
2006
|
+
const contentLines = [];
|
|
2007
|
+
for (i++; i < lines.length; ) {
|
|
2008
|
+
const currentLine = lines[i];
|
|
2009
|
+
if (currentLine.trim().match(/^<\/field>$/))
|
|
2010
|
+
break;
|
|
2011
|
+
contentLines.push(currentLine), i++;
|
|
2012
|
+
}
|
|
2013
|
+
if (node.text = contentLines.join(`
|
|
2014
|
+
`), stack.length > 0) {
|
|
2015
|
+
const parent = stack[stack.length - 1];
|
|
2016
|
+
parent && parent.children.push(node);
|
|
2017
|
+
} else
|
|
2018
|
+
root.push(node);
|
|
2019
|
+
} else
|
|
2125
2020
|
stack.push(node);
|
|
2126
2021
|
continue;
|
|
2127
2022
|
}
|
|
@@ -2149,6 +2044,16 @@ const fetchTranslationPrompts = async ({
|
|
|
2149
2044
|
applyFieldChanges(doc, path, node);
|
|
2150
2045
|
}
|
|
2151
2046
|
}, applyFieldChanges = (doc, path, node) => {
|
|
2047
|
+
if (node.attributes.type === "translatable.vtt.captions") {
|
|
2048
|
+
const fieldValue = get(doc, path);
|
|
2049
|
+
fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.vtt.captions" && node.text && set(doc, [...path, "translationText"], node.text);
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
if (node.attributes.type === "translatable.richtext") {
|
|
2053
|
+
const fieldValue = get(doc, path);
|
|
2054
|
+
fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.richtext" && node.text && set(doc, [...path, "translationText"], node.text);
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2152
2057
|
if (node.attributes.type === "array") {
|
|
2153
2058
|
node.children.forEach((child, idx) => {
|
|
2154
2059
|
applyFieldChanges(doc, [...path, idx], child);
|
|
@@ -2215,7 +2120,7 @@ const fetchTranslationPrompts = async ({
|
|
|
2215
2120
|
if (path.length === 0) {
|
|
2216
2121
|
if (isPlainObject(value))
|
|
2217
2122
|
for (const [key, val] of Object.entries(value))
|
|
2218
|
-
!key.startsWith("_") && key !== "language" && serialize(val, [key],
|
|
2123
|
+
!key.startsWith("_") && key !== "language" && serialize(val, [key], 0);
|
|
2219
2124
|
return;
|
|
2220
2125
|
}
|
|
2221
2126
|
const fieldName = path[path.length - 1];
|
|
@@ -2228,6 +2133,14 @@ const fetchTranslationPrompts = async ({
|
|
|
2228
2133
|
return;
|
|
2229
2134
|
}
|
|
2230
2135
|
if (isPlainObject(value)) {
|
|
2136
|
+
if (value._type === "translatable.vtt.captions" && typeof value.translationText == "string") {
|
|
2137
|
+
lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
if (value._type === "translatable.richtext" && typeof value.translationText == "string") {
|
|
2141
|
+
lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2231
2144
|
lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
|
|
2232
2145
|
for (const [key, val] of Object.entries(value))
|
|
2233
2146
|
key.startsWith("_") || serialize(val, [...path, key], indent + 1);
|
|
@@ -2235,7 +2148,7 @@ const fetchTranslationPrompts = async ({
|
|
|
2235
2148
|
return;
|
|
2236
2149
|
}
|
|
2237
2150
|
if (typeof value == "string") {
|
|
2238
|
-
value.length > 0 && lines.push(`${
|
|
2151
|
+
value.length > 0 && lines.push(`${fieldName}: ${escapeXml(value)}`);
|
|
2239
2152
|
return;
|
|
2240
2153
|
}
|
|
2241
2154
|
(typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
|
|
@@ -2275,499 +2188,132 @@ const fetchTranslationPrompts = async ({
|
|
|
2275
2188
|
lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
|
|
2276
2189
|
return;
|
|
2277
2190
|
}
|
|
2278
|
-
(typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
|
|
2279
|
-
}, serializeSpan = (span, _path, indent, lines) => {
|
|
2280
|
-
const indentStr = " ".repeat(indent);
|
|
2281
|
-
if (!span || typeof span != "object") return;
|
|
2282
|
-
const metadata = {};
|
|
2283
|
-
span._type && (metadata._type = span._type), span._key && (metadata._key = span._key), span.marks && Array.isArray(span.marks) && (metadata.marks = span.marks.join(","));
|
|
2284
|
-
const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(String(v))}"`).join(" "), text = typeof span.text == "string" ? span.text : "";
|
|
2285
|
-
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
|
|
2191
|
+
(typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
|
|
2192
|
+
}, serializeSpan = (span, _path, indent, lines) => {
|
|
2193
|
+
const indentStr = " ".repeat(indent);
|
|
2194
|
+
if (!span || typeof span != "object") return;
|
|
2195
|
+
const metadata = {};
|
|
2196
|
+
span._type && (metadata._type = span._type), span._key && (metadata._key = span._key), span.marks && Array.isArray(span.marks) && (metadata.marks = span.marks.join(","));
|
|
2197
|
+
const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(String(v))}"`).join(" "), text = typeof span.text == "string" ? span.text : "";
|
|
2198
|
+
metaStr ? lines.push(`${indentStr}<span ${metaStr}>${escapeXml(text)}</span>`) : lines.push(`${indentStr}<span>${escapeXml(text)}</span>`);
|
|
2199
|
+
}, 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) => {
|
|
2200
|
+
const errors = [], criticalFields = ["_id", "_type", "_rev", "_createdAt", "_updatedAt"];
|
|
2201
|
+
for (const field of criticalFields)
|
|
2202
|
+
original[field] !== modified[field] && errors.push({
|
|
2203
|
+
path: [field],
|
|
2204
|
+
expected: original[field],
|
|
2205
|
+
actual: modified[field],
|
|
2206
|
+
type: "metadata_changed"
|
|
2676
2207
|
});
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
defaultReadOnly: !1
|
|
2689
|
-
}
|
|
2208
|
+
return validateStructureRecursive(original, modified, [], errors), {
|
|
2209
|
+
valid: errors.length === 0,
|
|
2210
|
+
errors
|
|
2211
|
+
};
|
|
2212
|
+
}, validateStructureRecursive = (original, modified, path, errors) => {
|
|
2213
|
+
if (typeof original != typeof modified) {
|
|
2214
|
+
errors.push({
|
|
2215
|
+
path,
|
|
2216
|
+
expected: typeof original,
|
|
2217
|
+
actual: typeof modified,
|
|
2218
|
+
type: "type_mismatch"
|
|
2690
2219
|
});
|
|
2691
|
-
return
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
if (Array.isArray(original)) {
|
|
2223
|
+
if (!Array.isArray(modified)) {
|
|
2224
|
+
errors.push({
|
|
2225
|
+
path,
|
|
2226
|
+
expected: "array",
|
|
2227
|
+
actual: typeof modified,
|
|
2228
|
+
type: "type_mismatch"
|
|
2229
|
+
});
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
if (original.length !== modified.length) {
|
|
2233
|
+
errors.push({
|
|
2234
|
+
path,
|
|
2235
|
+
expected: `length ${original.length}`,
|
|
2236
|
+
actual: `length ${modified.length}`,
|
|
2237
|
+
type: "type_mismatch"
|
|
2238
|
+
});
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
original.forEach((item, idx) => {
|
|
2242
|
+
validateStructureRecursive(item, modified[idx], [...path, String(idx)], errors);
|
|
2704
2243
|
});
|
|
2705
2244
|
return;
|
|
2706
2245
|
}
|
|
2707
|
-
|
|
2246
|
+
if (typeof original == "object" && original !== null && typeof modified == "object" && modified !== null) {
|
|
2247
|
+
const origKeys = Object.keys(original).sort(), modKeys = Object.keys(modified).sort();
|
|
2248
|
+
for (const key of origKeys)
|
|
2249
|
+
key in modified || errors.push({
|
|
2250
|
+
path: [...path, key],
|
|
2251
|
+
expected: "present",
|
|
2252
|
+
actual: "missing",
|
|
2253
|
+
type: "missing"
|
|
2254
|
+
});
|
|
2255
|
+
for (const key of modKeys)
|
|
2256
|
+
key in original || errors.push({
|
|
2257
|
+
path: [...path, key],
|
|
2258
|
+
expected: "absent",
|
|
2259
|
+
actual: "present",
|
|
2260
|
+
type: "extra"
|
|
2261
|
+
});
|
|
2262
|
+
const origObj = original, modObj = modified;
|
|
2263
|
+
for (const key of origKeys)
|
|
2264
|
+
key in modObj && (key.startsWith("_") || key === "marks" || key === "style" || key === "markDefs" ? JSON.stringify(origObj[key]) !== JSON.stringify(modObj[key]) && errors.push({
|
|
2265
|
+
path: [...path, key],
|
|
2266
|
+
expected: origObj[key],
|
|
2267
|
+
actual: modObj[key],
|
|
2268
|
+
type: "metadata_changed"
|
|
2269
|
+
}) : key !== "text" && validateStructureRecursive(origObj[key], modObj[key], [...path, key], errors));
|
|
2270
|
+
}
|
|
2271
|
+
}, getOpenAITranslation = async ({
|
|
2708
2272
|
dispatch,
|
|
2709
2273
|
sourceDocument,
|
|
2710
|
-
targetDocument,
|
|
2711
|
-
targetLanguage,
|
|
2712
2274
|
languageFieldName,
|
|
2713
2275
|
translationPrompts,
|
|
2714
|
-
|
|
2715
|
-
|
|
2276
|
+
targetLanguage,
|
|
2277
|
+
openaiConfig
|
|
2716
2278
|
}) => {
|
|
2717
2279
|
const sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
2718
2280
|
sourceId: sourceDocument._id,
|
|
2719
|
-
|
|
2720
|
-
targetLanguage,
|
|
2721
|
-
sourceLanguage,
|
|
2722
|
-
isReview
|
|
2281
|
+
sourceLanguage
|
|
2723
2282
|
};
|
|
2724
2283
|
try {
|
|
2725
|
-
const sourceText = serializeDocument(sourceDocument)
|
|
2726
|
-
devLog.
|
|
2284
|
+
const sourceText = serializeDocument(sourceDocument);
|
|
2285
|
+
devLog.info("Serialized document for OpenAI", {
|
|
2727
2286
|
...logContext,
|
|
2728
|
-
|
|
2729
|
-
|
|
2287
|
+
characterCount: sourceText.length,
|
|
2288
|
+
estimatedTokens: Math.ceil(sourceText.length / 4),
|
|
2289
|
+
preview: sourceText.substring(0, 500)
|
|
2730
2290
|
});
|
|
2731
|
-
const prompt =
|
|
2732
|
-
sourceText,
|
|
2733
|
-
translatedText: targetText,
|
|
2734
|
-
sourceLanguage,
|
|
2735
|
-
targetLanguage,
|
|
2736
|
-
translationPrompts
|
|
2737
|
-
}) : buildTranslationPrompt({
|
|
2291
|
+
const prompt = buildTranslationPrompt$1({
|
|
2738
2292
|
sourceText,
|
|
2739
2293
|
sourceLanguage,
|
|
2740
2294
|
targetLanguage,
|
|
2741
2295
|
translationPrompts
|
|
2742
|
-
})
|
|
2743
|
-
devLog.debug("Calling GPT-5", {
|
|
2744
|
-
...logContext,
|
|
2745
|
-
promptLength: prompt.length
|
|
2746
|
-
});
|
|
2747
|
-
const response = await callGPT5(openaiConfig.apiKey, {
|
|
2296
|
+
}), response = await callGPT5(openaiConfig.apiKey, {
|
|
2748
2297
|
messages: [
|
|
2749
2298
|
{
|
|
2750
|
-
role: "
|
|
2751
|
-
content:
|
|
2299
|
+
role: "developer",
|
|
2300
|
+
content: "You are a translation quality reviewer. Review and refine translations while preserving structure."
|
|
2752
2301
|
},
|
|
2753
2302
|
{ role: "user", content: prompt }
|
|
2754
2303
|
],
|
|
2755
2304
|
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
|
-
}
|
|
2305
|
+
reasoningEffort: openaiConfig.reasoningEffort
|
|
2764
2306
|
});
|
|
2765
|
-
devLog.info("OpenAI
|
|
2307
|
+
devLog.info("OpenAI translation response received", {
|
|
2766
2308
|
...logContext,
|
|
2767
|
-
tokensUsed: response.tokensUsed
|
|
2768
|
-
cost: response.cost
|
|
2309
|
+
tokensUsed: response.tokensUsed,
|
|
2310
|
+
cost: response.cost,
|
|
2311
|
+
responseLength: response.content.length,
|
|
2312
|
+
responsePreview: response.content.substring(0, 500)
|
|
2769
2313
|
});
|
|
2770
|
-
const result = deserializeDocument(
|
|
2314
|
+
const result = deserializeDocument(sourceDocument, response.content);
|
|
2315
|
+
result[languageFieldName] = targetLanguage;
|
|
2316
|
+
const validation = validateStructureIntegrity(sourceDocument, result);
|
|
2771
2317
|
if (!validation.valid) {
|
|
2772
2318
|
devLog.error("Structure validation failed", {
|
|
2773
2319
|
...logContext,
|
|
@@ -2781,17 +2327,14 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
|
|
|
2781
2327
|
`Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
|
|
2782
2328
|
);
|
|
2783
2329
|
}
|
|
2784
|
-
|
|
2785
|
-
const status = isReview ? "reviewing_with_openai" : "translating_with_openai";
|
|
2786
|
-
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, status, {
|
|
2330
|
+
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
|
|
2787
2331
|
tokensUsed: response.tokensUsed.total,
|
|
2788
|
-
estimatedCost: response.cost
|
|
2789
|
-
reasoningSummary: response.reasoningSummary
|
|
2332
|
+
estimatedCost: response.cost
|
|
2790
2333
|
}), result;
|
|
2791
2334
|
} catch (error) {
|
|
2792
2335
|
throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
|
|
2793
2336
|
}
|
|
2794
|
-
}, buildTranslationPrompt = ({
|
|
2337
|
+
}, buildTranslationPrompt$1 = ({
|
|
2795
2338
|
sourceText,
|
|
2796
2339
|
sourceLanguage,
|
|
2797
2340
|
targetLanguage,
|
|
@@ -2838,73 +2381,193 @@ Brief summary of key translation decisions and reasoning
|
|
|
2838
2381
|
</reasoning>
|
|
2839
2382
|
|
|
2840
2383
|
Then return the translated document:`;
|
|
2841
|
-
},
|
|
2842
|
-
|
|
2843
|
-
|
|
2384
|
+
}, assembleLanguageSpecs = ({
|
|
2385
|
+
sourceDocument,
|
|
2386
|
+
languageFieldName,
|
|
2387
|
+
targetLanguage
|
|
2388
|
+
}) => {
|
|
2389
|
+
const sourceLanugageId = sourceDocument[languageFieldName];
|
|
2390
|
+
if (!sourceLanugageId)
|
|
2391
|
+
throw new Error("[assembleLanguages] Source language ID is missing");
|
|
2392
|
+
return {
|
|
2393
|
+
fromLanguage: {
|
|
2394
|
+
id: sourceLanugageId,
|
|
2395
|
+
title: sourceLanugageId
|
|
2396
|
+
},
|
|
2397
|
+
toLanguage: {
|
|
2398
|
+
id: targetLanguage,
|
|
2399
|
+
title: targetLanguage
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2402
|
+
}, buildTranslationPrompt = ({
|
|
2844
2403
|
sourceLanguage,
|
|
2845
2404
|
targetLanguage,
|
|
2846
|
-
|
|
2405
|
+
components,
|
|
2406
|
+
isUpdate = !1
|
|
2847
2407
|
}) => {
|
|
2848
|
-
const
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
## Review Criteria
|
|
2408
|
+
const sections = [];
|
|
2409
|
+
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));
|
|
2410
|
+
const styleSection = buildStyleGuideSection(components.globalPrompt, components.localePrompt);
|
|
2411
|
+
return styleSection && sections.push(styleSection), sections.join(`
|
|
2854
2412
|
|
|
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?
|
|
2413
|
+
---
|
|
2861
2414
|
|
|
2862
|
-
|
|
2415
|
+
`);
|
|
2416
|
+
}, buildAgentDirectivesSection = (sourceLanguage, targetLanguage) => `# AGENT WORKFLOW DIRECTIVES
|
|
2863
2417
|
|
|
2864
|
-
${
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
${
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
${glossaryTerms}
|
|
2872
|
-
` : ""}
|
|
2873
|
-
${protectedPhrases?.length ? `### Protected Phrases (must preserve)
|
|
2874
|
-
${protectedPhrases.join(", ")}
|
|
2875
|
-
` : ""}
|
|
2418
|
+
- Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
|
|
2419
|
+
- Scan each field once, then move directly into translation work.
|
|
2420
|
+
- Plan updates before editing so document structure remains intact.
|
|
2421
|
+
- Default to medium reasoning effort and escalate only for ambiguous passages.
|
|
2422
|
+
- Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`, buildCriticalRulesSection = (sourceLanguage, targetLanguage) => {
|
|
2423
|
+
const fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
|
|
2424
|
+
return `# CRITICAL TRANSLATION RULES
|
|
2876
2425
|
|
|
2877
|
-
|
|
2426
|
+
Follow these without exception:
|
|
2878
2427
|
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2428
|
+
1. Translate from ${fromLanguage} into ${toLanguage}; every output token must be ${toLanguage} unless flagged as protected.
|
|
2429
|
+
2. Preserve meaning exactly\u2014no additions, deletions, or tone shifts.
|
|
2430
|
+
3. Maintain all layout, markdown, rich-text marks, and data structure.
|
|
2431
|
+
4. Never invent metadata or field values that are absent in the source.
|
|
2432
|
+
5. Leave technical tokens (code, IDs, filenames) untouched.
|
|
2433
|
+
6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
|
|
2434
|
+
}, buildUpdateInstructionsSection = () => `# UPDATING EXISTING TRANSLATION
|
|
2882
2435
|
|
|
2883
|
-
|
|
2436
|
+
This document already has a translation; only adjust fields that changed.
|
|
2884
2437
|
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2438
|
+
- Compare the latest source to the existing target text.
|
|
2439
|
+
- Keep prior terminology and style unless the source demands a change.
|
|
2440
|
+
- Edit minimally while guaranteeing accuracy and completeness.`, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
|
|
2888
2441
|
|
|
2889
|
-
|
|
2442
|
+
Apply these mappings immediately after protected phrases and before style preferences.
|
|
2890
2443
|
|
|
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
|
|
2444
|
+
${glossaryTerms}`, buildProtectedPhrasesSection = (protectedPhrases) => `# PROTECTED PHRASES
|
|
2897
2445
|
|
|
2898
|
-
|
|
2899
|
-
<reasoning>
|
|
2900
|
-
Brief summary of review findings and corrections made
|
|
2901
|
-
</reasoning>
|
|
2446
|
+
These phrases must NEVER be translated. Keep them exactly as they appear:
|
|
2902
2447
|
|
|
2903
|
-
|
|
2448
|
+
${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
|
|
2449
|
+
const components = [];
|
|
2450
|
+
return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
|
|
2451
|
+
"Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
|
|
2452
|
+
), 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(`
|
|
2453
|
+
`) : null;
|
|
2454
|
+
}, getSanityAgentTranslation = async ({
|
|
2455
|
+
sourceDocument,
|
|
2456
|
+
targetLanguage,
|
|
2457
|
+
languageFieldName,
|
|
2458
|
+
schemaId,
|
|
2459
|
+
client,
|
|
2460
|
+
translationPrompts
|
|
2461
|
+
}) => {
|
|
2462
|
+
const styleGuide = buildTranslationPrompt({
|
|
2463
|
+
sourceLanguage: sourceDocument[languageFieldName],
|
|
2464
|
+
targetLanguage,
|
|
2465
|
+
isUpdate: !1,
|
|
2466
|
+
// Always fresh translation in a version
|
|
2467
|
+
components: {
|
|
2468
|
+
globalPrompt: translationPrompts.globalPrompt,
|
|
2469
|
+
localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
|
|
2470
|
+
glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
|
|
2471
|
+
protectedPhrases: translationPrompts.protectedPhrases
|
|
2472
|
+
}
|
|
2473
|
+
}), { fromLanguage, toLanguage } = assembleLanguageSpecs({
|
|
2474
|
+
sourceDocument,
|
|
2475
|
+
languageFieldName,
|
|
2476
|
+
targetLanguage
|
|
2477
|
+
});
|
|
2478
|
+
return await client.agent.action.translate({
|
|
2479
|
+
schemaId,
|
|
2480
|
+
toLanguage,
|
|
2481
|
+
fromLanguage,
|
|
2482
|
+
documentId: sourceDocument._id,
|
|
2483
|
+
noWrite: !0,
|
|
2484
|
+
// Do not write to the document, just return the translated content
|
|
2485
|
+
languageFieldPath: [languageFieldName],
|
|
2486
|
+
styleGuide,
|
|
2487
|
+
protectedPhrases: translationPrompts.protectedPhrases,
|
|
2488
|
+
conditionalPaths: {
|
|
2489
|
+
defaultHidden: !1,
|
|
2490
|
+
defaultReadOnly: !1
|
|
2491
|
+
}
|
|
2492
|
+
});
|
|
2493
|
+
}, handleTranslation = async ({
|
|
2494
|
+
dispatch,
|
|
2495
|
+
client,
|
|
2496
|
+
schemaId,
|
|
2497
|
+
sourceDocument,
|
|
2498
|
+
targetLanguage,
|
|
2499
|
+
existingTranslationId,
|
|
2500
|
+
languageFieldName,
|
|
2501
|
+
translationPrompts,
|
|
2502
|
+
translationMode,
|
|
2503
|
+
openaiConfig,
|
|
2504
|
+
releaseId
|
|
2505
|
+
}) => {
|
|
2506
|
+
devLog.debug("Applying field transforms before translation", {
|
|
2507
|
+
documentId: sourceDocument._id,
|
|
2508
|
+
documentType: sourceDocument._type
|
|
2509
|
+
});
|
|
2510
|
+
const { transformedDocument, originalStructure } = applyTransforms({
|
|
2511
|
+
document: sourceDocument,
|
|
2512
|
+
registry: globalTransformRegistry,
|
|
2513
|
+
schemaType: sourceDocument._type
|
|
2514
|
+
});
|
|
2515
|
+
let translatedDoc;
|
|
2516
|
+
switch (translationMode) {
|
|
2517
|
+
case "sanity-only": {
|
|
2518
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
|
|
2519
|
+
sourceDocument: transformedDocument,
|
|
2520
|
+
targetLanguage,
|
|
2521
|
+
languageFieldName,
|
|
2522
|
+
schemaId,
|
|
2523
|
+
client,
|
|
2524
|
+
translationPrompts
|
|
2525
|
+
});
|
|
2526
|
+
break;
|
|
2527
|
+
}
|
|
2528
|
+
case "openai-only": {
|
|
2529
|
+
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2530
|
+
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2531
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), translatedDoc = await getOpenAITranslation({
|
|
2532
|
+
dispatch,
|
|
2533
|
+
sourceDocument: transformedDocument,
|
|
2534
|
+
targetLanguage,
|
|
2535
|
+
languageFieldName,
|
|
2536
|
+
translationPrompts,
|
|
2537
|
+
openaiConfig
|
|
2538
|
+
});
|
|
2539
|
+
break;
|
|
2540
|
+
}
|
|
2541
|
+
case "sanity-with-review":
|
|
2542
|
+
throw new Error("Sanity with review translation method is not implemented");
|
|
2543
|
+
default:
|
|
2544
|
+
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2545
|
+
}
|
|
2546
|
+
if (!translatedDoc)
|
|
2547
|
+
return null;
|
|
2548
|
+
devLog.debug("Restoring field transforms after translation", {
|
|
2549
|
+
documentId: sourceDocument._id,
|
|
2550
|
+
documentType: sourceDocument._type
|
|
2551
|
+
});
|
|
2552
|
+
const finalDoc = restoreTransforms({
|
|
2553
|
+
translatedDocument: translatedDoc,
|
|
2554
|
+
originalStructure,
|
|
2555
|
+
registry: globalTransformRegistry,
|
|
2556
|
+
schemaType: sourceDocument._type
|
|
2557
|
+
}), targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
|
|
2558
|
+
...finalDoc,
|
|
2559
|
+
_id: targetDocumentVersionId
|
|
2560
|
+
};
|
|
2561
|
+
return await client.action({
|
|
2562
|
+
actionType: "sanity.action.document.version.create",
|
|
2563
|
+
publishedId: targetDocumentPublishedId,
|
|
2564
|
+
document: newDoc
|
|
2565
|
+
}), newDoc;
|
|
2904
2566
|
}, translateDocument = async ({
|
|
2905
2567
|
dispatch,
|
|
2906
2568
|
client,
|
|
2907
2569
|
sourceDocument,
|
|
2570
|
+
sourceLanguage,
|
|
2908
2571
|
targetLanguage,
|
|
2909
2572
|
languageFieldName,
|
|
2910
2573
|
shouldOverwriteFreshTranslations,
|
|
@@ -2923,62 +2586,37 @@ Then return the corrected translation document:`;
|
|
|
2923
2586
|
};
|
|
2924
2587
|
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
|
|
2925
2588
|
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
2589
|
const translationMetadata = await getTranslationMetadata({
|
|
2937
2590
|
client,
|
|
2938
2591
|
sourceDocument,
|
|
2939
2592
|
sourceLanguage
|
|
2940
2593
|
}), isAlreadyFreshTranslation = translationMetadata.translationHistory?.some(
|
|
2941
2594
|
(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", {
|
|
2595
|
+
);
|
|
2596
|
+
if (!shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
|
|
2597
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
|
|
2949
2598
|
targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
|
|
2950
|
-
})
|
|
2951
|
-
return;
|
|
2952
|
-
}
|
|
2953
|
-
const
|
|
2954
|
-
translationMetadata,
|
|
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({
|
|
2599
|
+
});
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
|
|
2963
2603
|
dispatch,
|
|
2964
2604
|
client,
|
|
2965
2605
|
schemaId,
|
|
2966
2606
|
sourceDocument,
|
|
2967
2607
|
targetLanguage,
|
|
2608
|
+
existingTranslationId,
|
|
2968
2609
|
languageFieldName,
|
|
2969
|
-
translationTarget,
|
|
2970
2610
|
translationPrompts,
|
|
2971
2611
|
translationMode,
|
|
2972
2612
|
openaiConfig,
|
|
2973
|
-
releaseId
|
|
2974
|
-
isNewTranslation
|
|
2613
|
+
releaseId
|
|
2975
2614
|
});
|
|
2976
|
-
if (!newDocument)
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
}), devLog.debug("Updating translation metadata", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
|
|
2615
|
+
if (!newDocument) {
|
|
2616
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
|
|
2982
2620
|
targetId: newDocument._id
|
|
2983
2621
|
}), await updateTranslationMetadata({
|
|
2984
2622
|
client,
|
|
@@ -2987,8 +2625,10 @@ Then return the corrected translation document:`;
|
|
|
2987
2625
|
newDocument,
|
|
2988
2626
|
targetLanguage,
|
|
2989
2627
|
translationMetadata,
|
|
2990
|
-
isNewTranslation
|
|
2991
|
-
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed"
|
|
2628
|
+
isNewTranslation: !existingTranslationId
|
|
2629
|
+
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed", {
|
|
2630
|
+
targetId: newDocument._id
|
|
2631
|
+
}), devLog.info("Translation completed successfully", {
|
|
2992
2632
|
...logContext,
|
|
2993
2633
|
newDocumentId: newDocument._id
|
|
2994
2634
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
@@ -3014,77 +2654,61 @@ Then return the corrected translation document:`;
|
|
|
3014
2654
|
}) => {
|
|
3015
2655
|
const logContext = {
|
|
3016
2656
|
documentId: sourceDocument._id,
|
|
2657
|
+
documentRev: sourceDocument._rev,
|
|
3017
2658
|
targetLanguages,
|
|
3018
2659
|
referencesCount: references.length,
|
|
3019
2660
|
maxConcurrentJobs: maxConcurrentTranslationJobs,
|
|
3020
2661
|
translationMode,
|
|
3021
2662
|
shouldOverwriteFreshTranslations
|
|
3022
2663
|
};
|
|
3023
|
-
devLog.group("translateDocumentSet", logContext), devLog.time("translateDocumentSet");
|
|
3024
2664
|
let releaseId;
|
|
3025
2665
|
try {
|
|
3026
2666
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
3027
2667
|
if (!sourceLanguage)
|
|
3028
2668
|
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({
|
|
2669
|
+
maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
3033
2670
|
type: "SET_OVERALL_STATUS",
|
|
3034
2671
|
payload: "translating"
|
|
3035
2672
|
});
|
|
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({
|
|
2673
|
+
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
2674
|
metadata: {
|
|
3043
2675
|
title: releaseTitle,
|
|
3044
2676
|
description: releaseDescription,
|
|
3045
|
-
releaseType: "
|
|
2677
|
+
releaseType: "undecided"
|
|
3046
2678
|
}
|
|
3047
2679
|
});
|
|
3048
|
-
|
|
2680
|
+
devLog.info("Content Release created", {
|
|
3049
2681
|
...logContext,
|
|
3050
|
-
releaseId
|
|
2682
|
+
releaseId: releaseId2
|
|
3051
2683
|
});
|
|
3052
2684
|
const sourceDocuments = [
|
|
3053
2685
|
{
|
|
3054
2686
|
_id: sourceDocument._id,
|
|
3055
2687
|
_type: sourceDocument._type,
|
|
3056
|
-
// TODO:
|
|
2688
|
+
// TODO: Extract document title more robustly across schema types
|
|
3057
2689
|
title: sourceDocument.name || sourceDocument.title
|
|
3058
2690
|
},
|
|
3059
2691
|
...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({
|
|
2692
|
+
], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), versionedTargetDocumentIds = [], translationPrompts = await fetchTranslationPrompts({
|
|
3067
2693
|
client,
|
|
3068
2694
|
sourceLanguage,
|
|
3069
2695
|
targetLanguages,
|
|
3070
2696
|
sourceDocument
|
|
3071
2697
|
});
|
|
3072
|
-
|
|
2698
|
+
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
|
|
3073
2699
|
const translationTasks = sourceDocumentIds.flatMap(
|
|
3074
2700
|
(documentId) => targetLanguages.map(async (targetLanguage) => {
|
|
3075
2701
|
try {
|
|
3076
2702
|
await translationSemaphore.withPermit(async () => {
|
|
3077
2703
|
const document = await client.withConfig({
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
});
|
|
3083
|
-
if (document && releaseId) {
|
|
3084
|
-
const targetDocumentId = await translateDocument({
|
|
2704
|
+
perspective: "drafts"
|
|
2705
|
+
}).getDocument(documentId);
|
|
2706
|
+
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId2) {
|
|
2707
|
+
const versionedTargetDocumentId = await translateDocument({
|
|
3085
2708
|
dispatch,
|
|
3086
2709
|
client,
|
|
3087
2710
|
sourceDocument: document,
|
|
2711
|
+
sourceLanguage,
|
|
3088
2712
|
targetLanguage,
|
|
3089
2713
|
languageFieldName,
|
|
3090
2714
|
shouldOverwriteFreshTranslations,
|
|
@@ -3092,9 +2716,9 @@ Then return the corrected translation document:`;
|
|
|
3092
2716
|
translationPrompts,
|
|
3093
2717
|
translationMode,
|
|
3094
2718
|
openaiConfig,
|
|
3095
|
-
releaseId
|
|
2719
|
+
releaseId: releaseId2
|
|
3096
2720
|
});
|
|
3097
|
-
|
|
2721
|
+
versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
|
|
3098
2722
|
}
|
|
3099
2723
|
});
|
|
3100
2724
|
} catch (error) {
|
|
@@ -3110,25 +2734,21 @@ Then return the corrected translation document:`;
|
|
|
3110
2734
|
}
|
|
3111
2735
|
})
|
|
3112
2736
|
), 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
|
|
2737
|
+
devLog.info("Translation tasks completed", {
|
|
3114
2738
|
...logContext,
|
|
3115
2739
|
total: results.length,
|
|
3116
2740
|
succeeded,
|
|
3117
|
-
failed
|
|
3118
|
-
|
|
3119
|
-
...logContext,
|
|
3120
|
-
targetDocumentIds: targetDocumentIds.length
|
|
2741
|
+
failed,
|
|
2742
|
+
targetDocuments: versionedTargetDocumentIds.length
|
|
3121
2743
|
}), dispatch({
|
|
3122
2744
|
type: "SET_OVERALL_STATUS",
|
|
3123
2745
|
payload: "linking_references"
|
|
3124
|
-
}), devLog.debug("Starting reference localization", {
|
|
3125
|
-
...logContext,
|
|
3126
|
-
documentsToLocalize: targetDocumentIds.length
|
|
3127
2746
|
}), await Promise.all(
|
|
3128
|
-
|
|
2747
|
+
versionedTargetDocumentIds.map(async (documentId) => {
|
|
3129
2748
|
await localizeReferences({
|
|
3130
2749
|
client,
|
|
3131
2750
|
documentId,
|
|
2751
|
+
sourceLanguage,
|
|
3132
2752
|
languageFieldName
|
|
3133
2753
|
});
|
|
3134
2754
|
})
|
|
@@ -3138,26 +2758,13 @@ Then return the corrected translation document:`;
|
|
|
3138
2758
|
...logContext,
|
|
3139
2759
|
succeeded,
|
|
3140
2760
|
failed
|
|
3141
|
-
}), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) :
|
|
2761
|
+
}), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" }), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
|
|
3142
2762
|
} catch (error) {
|
|
3143
|
-
|
|
2763
|
+
throw devLog.error(
|
|
3144
2764
|
"translateDocumentSet failed with unexpected error",
|
|
3145
2765
|
error instanceof Error ? error : new Error(String(error)),
|
|
3146
2766
|
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;
|
|
2767
|
+
), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
|
|
3161
2768
|
}
|
|
3162
2769
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
3163
2770
|
const {
|
|
@@ -3578,24 +3185,12 @@ Then return the corrected translation document:`;
|
|
|
3578
3185
|
" \u2014 ",
|
|
3579
3186
|
job._id.slice(-6)
|
|
3580
3187
|
] }),
|
|
3581
|
-
!["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */
|
|
3582
|
-
|
|
3583
|
-
job.
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
] }),
|
|
3588
|
-
job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3589
|
-
/* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3590
|
-
job.tokensUsed.toLocaleString(),
|
|
3591
|
-
" tokens (~$",
|
|
3592
|
-
job.estimatedCost?.toFixed(4),
|
|
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
|
-
] }) })
|
|
3188
|
+
!["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
|
|
3189
|
+
job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3190
|
+
job.tokensUsed.toLocaleString(),
|
|
3191
|
+
" tokens (~$",
|
|
3192
|
+
job.estimatedCost?.toFixed(4),
|
|
3193
|
+
")"
|
|
3599
3194
|
] }) : null,
|
|
3600
3195
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3601
3196
|
"Error: ",
|
|
@@ -3709,26 +3304,9 @@ Then return the corrected translation document:`;
|
|
|
3709
3304
|
"completed"
|
|
3710
3305
|
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
|
|
3711
3306
|
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", {
|
|
3307
|
+
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
3308
|
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"
|
|
3309
|
+
overallStatus
|
|
3732
3310
|
}), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
|
|
3733
3311
|
}, getTranslateAction = (config) => ({ draft, published }) => {
|
|
3734
3312
|
const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
|
|
@@ -3755,7 +3333,427 @@ Then return the corrected translation document:`;
|
|
|
3755
3333
|
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
|
|
3756
3334
|
}
|
|
3757
3335
|
} : (devLog.error("Missing config in TranslateAction"), null);
|
|
3758
|
-
},
|
|
3336
|
+
}, translationGlossaryTerm = defineType({
|
|
3337
|
+
name: "translationGlossaryTerm",
|
|
3338
|
+
title: "Translation Glossary Term",
|
|
3339
|
+
icon: TranslateIcon,
|
|
3340
|
+
type: "document",
|
|
3341
|
+
fields: [
|
|
3342
|
+
defineField({
|
|
3343
|
+
name: "translations",
|
|
3344
|
+
title: "Translations",
|
|
3345
|
+
type: "internationalizedArrayString"
|
|
3346
|
+
})
|
|
3347
|
+
],
|
|
3348
|
+
preview: {
|
|
3349
|
+
select: {
|
|
3350
|
+
translations: "translations"
|
|
3351
|
+
},
|
|
3352
|
+
prepare({ translations }) {
|
|
3353
|
+
const enTranslation = translations?.find((t) => t.locale === "en-us"), firstTranslation = translations?.[0];
|
|
3354
|
+
return {
|
|
3355
|
+
title: enTranslation?.value || firstTranslation?.value || "No translation available"
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
}), translationHistory = defineField({
|
|
3360
|
+
name: "translationHistory",
|
|
3361
|
+
type: "array",
|
|
3362
|
+
of: [
|
|
3363
|
+
{
|
|
3364
|
+
name: "targetLanguageHistory",
|
|
3365
|
+
type: "object",
|
|
3366
|
+
fields: [
|
|
3367
|
+
defineField({
|
|
3368
|
+
name: "language",
|
|
3369
|
+
title: "Language",
|
|
3370
|
+
description: "The language of the target document",
|
|
3371
|
+
type: "string"
|
|
3372
|
+
}),
|
|
3373
|
+
defineField({
|
|
3374
|
+
name: "sourceId",
|
|
3375
|
+
title: "Source ID",
|
|
3376
|
+
description: "The ID of the source document",
|
|
3377
|
+
type: "string"
|
|
3378
|
+
}),
|
|
3379
|
+
defineField({
|
|
3380
|
+
name: "sourceRev",
|
|
3381
|
+
title: "Source Revision",
|
|
3382
|
+
description: "The revision ID of the source document at the time of translation",
|
|
3383
|
+
type: "string"
|
|
3384
|
+
}),
|
|
3385
|
+
defineField({
|
|
3386
|
+
name: "sourceLanguage",
|
|
3387
|
+
title: "Source Language",
|
|
3388
|
+
description: "The language of the source document",
|
|
3389
|
+
type: "string"
|
|
3390
|
+
}),
|
|
3391
|
+
defineField({
|
|
3392
|
+
name: "sourceSnapshot",
|
|
3393
|
+
title: "Source Snapshot",
|
|
3394
|
+
description: "The snapshot of the source document at the time of translation",
|
|
3395
|
+
type: "text"
|
|
3396
|
+
}),
|
|
3397
|
+
defineField({
|
|
3398
|
+
name: "targetId",
|
|
3399
|
+
title: "Target ID",
|
|
3400
|
+
description: "The ID of the target document",
|
|
3401
|
+
type: "string"
|
|
3402
|
+
})
|
|
3403
|
+
]
|
|
3404
|
+
}
|
|
3405
|
+
]
|
|
3406
|
+
}), translationSettings = defineType({
|
|
3407
|
+
name: "translationSettings",
|
|
3408
|
+
title: "Translation Settings",
|
|
3409
|
+
icon: TranslateIcon,
|
|
3410
|
+
type: "document",
|
|
3411
|
+
groups: [
|
|
3412
|
+
{
|
|
3413
|
+
name: "general",
|
|
3414
|
+
title: "General",
|
|
3415
|
+
default: !0
|
|
3416
|
+
},
|
|
3417
|
+
{
|
|
3418
|
+
name: "history",
|
|
3419
|
+
title: "History"
|
|
3420
|
+
}
|
|
3421
|
+
],
|
|
3422
|
+
fields: [
|
|
3423
|
+
defineField({
|
|
3424
|
+
name: "globalPrompt",
|
|
3425
|
+
title: "Global Translation Prompt",
|
|
3426
|
+
description: "A prompt that will given to the Sanity AI Agent for all translation tasks.",
|
|
3427
|
+
type: "text",
|
|
3428
|
+
rows: 5,
|
|
3429
|
+
group: "general"
|
|
3430
|
+
}),
|
|
3431
|
+
defineField({
|
|
3432
|
+
name: "localePrompts",
|
|
3433
|
+
title: "Locale Prompts",
|
|
3434
|
+
description: "Prompts that will be given to the Sanity AI Agent for each locale.",
|
|
3435
|
+
type: "internationalizedArrayText",
|
|
3436
|
+
group: "general"
|
|
3437
|
+
}),
|
|
3438
|
+
defineField({
|
|
3439
|
+
name: "protectedPhrases",
|
|
3440
|
+
title: "Protected Phrases",
|
|
3441
|
+
description: "Phrases that will never be translated.",
|
|
3442
|
+
type: "array",
|
|
3443
|
+
of: [{ type: "string" }],
|
|
3444
|
+
group: "general"
|
|
3445
|
+
}),
|
|
3446
|
+
defineField({
|
|
3447
|
+
name: "changeHistory",
|
|
3448
|
+
title: "Change History",
|
|
3449
|
+
description: "History of changes made via user feedback.",
|
|
3450
|
+
type: "array",
|
|
3451
|
+
readOnly: !0,
|
|
3452
|
+
group: "history",
|
|
3453
|
+
of: [
|
|
3454
|
+
{
|
|
3455
|
+
type: "object",
|
|
3456
|
+
fields: [
|
|
3457
|
+
defineField({
|
|
3458
|
+
name: "timestamp",
|
|
3459
|
+
title: "Timestamp",
|
|
3460
|
+
type: "datetime"
|
|
3461
|
+
}),
|
|
3462
|
+
defineField({
|
|
3463
|
+
name: "changeType",
|
|
3464
|
+
title: "Change Type",
|
|
3465
|
+
type: "string",
|
|
3466
|
+
options: {
|
|
3467
|
+
list: [
|
|
3468
|
+
{ title: "Global Prompt", value: "globalPrompt" },
|
|
3469
|
+
{ title: "Locale Prompt", value: "localePrompt" },
|
|
3470
|
+
{ title: "Protected Phrase", value: "protectedPhrase" },
|
|
3471
|
+
{ title: "Glossary Term", value: "glossaryTerm" }
|
|
3472
|
+
]
|
|
3473
|
+
}
|
|
3474
|
+
}),
|
|
3475
|
+
defineField({
|
|
3476
|
+
name: "description",
|
|
3477
|
+
title: "Description",
|
|
3478
|
+
type: "text",
|
|
3479
|
+
rows: 3
|
|
3480
|
+
}),
|
|
3481
|
+
defineField({
|
|
3482
|
+
name: "locale",
|
|
3483
|
+
title: "Locale",
|
|
3484
|
+
type: "string",
|
|
3485
|
+
description: "Target locale for locale-specific changes"
|
|
3486
|
+
})
|
|
3487
|
+
],
|
|
3488
|
+
preview: {
|
|
3489
|
+
select: {
|
|
3490
|
+
timestamp: "timestamp",
|
|
3491
|
+
changeType: "changeType",
|
|
3492
|
+
description: "description"
|
|
3493
|
+
},
|
|
3494
|
+
prepare({ timestamp, changeType, description }) {
|
|
3495
|
+
return {
|
|
3496
|
+
title: `${changeType} - ${new Date(timestamp).toLocaleDateString()}`,
|
|
3497
|
+
subtitle: description
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
]
|
|
3503
|
+
})
|
|
3504
|
+
],
|
|
3505
|
+
preview: {
|
|
3506
|
+
prepare: () => ({
|
|
3507
|
+
title: "Translation Settings"
|
|
3508
|
+
})
|
|
3509
|
+
}
|
|
3510
|
+
});
|
|
3511
|
+
function isPortableText(value) {
|
|
3512
|
+
return Array.isArray(value) && value.some((item) => item?._type === "block");
|
|
3513
|
+
}
|
|
3514
|
+
const richTextTransform = {
|
|
3515
|
+
// Matcher combines field name pattern with value-based detection
|
|
3516
|
+
// fieldPath pattern acts as a pre-filter for performance
|
|
3517
|
+
// valueMatcher definitively checks if the value is actually Portable Text
|
|
3518
|
+
matcher: {
|
|
3519
|
+
// Pre-filter: Match all fields to enable detection based solely on value
|
|
3520
|
+
fieldPath: /.*/,
|
|
3521
|
+
// Definitive check: Verify the value is actually Portable Text
|
|
3522
|
+
valueMatcher: (value) => Array.isArray(value) && isPortableText(value)
|
|
3523
|
+
},
|
|
3524
|
+
toTranslatable: (value, context) => {
|
|
3525
|
+
if (!Array.isArray(value) || !isPortableText(value))
|
|
3526
|
+
return {
|
|
3527
|
+
_type: "translatable.richtext",
|
|
3528
|
+
translationText: JSON.stringify(value),
|
|
3529
|
+
_originalValue: value
|
|
3530
|
+
};
|
|
3531
|
+
devLog.info("Applying rich text transform with XML serialization", {
|
|
3532
|
+
fieldPath: context.fieldPath,
|
|
3533
|
+
blockCount: value.length
|
|
3534
|
+
});
|
|
3535
|
+
try {
|
|
3536
|
+
const tempDoc = {
|
|
3537
|
+
_id: "temp",
|
|
3538
|
+
_type: "richTextWrapper",
|
|
3539
|
+
_createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3540
|
+
_updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3541
|
+
_rev: "temp",
|
|
3542
|
+
content: value
|
|
3543
|
+
}, serialized = serializeDocument(tempDoc);
|
|
3544
|
+
devLog.debug("Serialized rich text document", {
|
|
3545
|
+
fieldPath: context.fieldPath,
|
|
3546
|
+
serializedLength: serialized.length,
|
|
3547
|
+
preview: serialized.substring(0, 200)
|
|
3548
|
+
});
|
|
3549
|
+
const contentRegex = /<field name="content"[^>]*>([\s\S]*)<\/field>/s, match = serialized.match(contentRegex);
|
|
3550
|
+
if (match?.[1]) {
|
|
3551
|
+
const result = {
|
|
3552
|
+
_type: "translatable.richtext",
|
|
3553
|
+
translationText: match[1].trim(),
|
|
3554
|
+
_originalValue: value
|
|
3555
|
+
};
|
|
3556
|
+
return devLog.debug("Successfully extracted and wrapped rich text", {
|
|
3557
|
+
fieldPath: context.fieldPath,
|
|
3558
|
+
translationTextLength: result.translationText.length
|
|
3559
|
+
}), result;
|
|
3560
|
+
}
|
|
3561
|
+
return devLog.warn("Failed to serialize rich text content - regex did not match", {
|
|
3562
|
+
fieldPath: context.fieldPath,
|
|
3563
|
+
serializedPreview: serialized.substring(0, 500)
|
|
3564
|
+
}), {
|
|
3565
|
+
_type: "translatable.richtext",
|
|
3566
|
+
translationText: JSON.stringify(value),
|
|
3567
|
+
_originalValue: value
|
|
3568
|
+
};
|
|
3569
|
+
} catch (error) {
|
|
3570
|
+
return devLog.error("Error serializing rich text", error, {
|
|
3571
|
+
fieldPath: context.fieldPath
|
|
3572
|
+
}), {
|
|
3573
|
+
_type: "translatable.richtext",
|
|
3574
|
+
translationText: JSON.stringify(value),
|
|
3575
|
+
_originalValue: value
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
},
|
|
3579
|
+
fromTranslatable: (translatedValue, originalValue, context) => {
|
|
3580
|
+
if (!translatedValue || translatedValue._type !== "translatable.richtext" || !translatedValue.translationText)
|
|
3581
|
+
return devLog.debug("Value is not a translatable rich text object, returning original", {
|
|
3582
|
+
fieldPath: context.fieldPath,
|
|
3583
|
+
hasCorrectType: translatedValue._type === "translatable.richtext"
|
|
3584
|
+
}), originalValue;
|
|
3585
|
+
devLog.info("Deserializing rich text content", {
|
|
3586
|
+
fieldPath: context.fieldPath,
|
|
3587
|
+
translationTextLength: translatedValue.translationText.length
|
|
3588
|
+
});
|
|
3589
|
+
try {
|
|
3590
|
+
const { translationText, _originalValue } = translatedValue, wrappedSerialized = `<field name="content">
|
|
3591
|
+
${translationText}
|
|
3592
|
+
</field>`;
|
|
3593
|
+
devLog.debug("Prepared wrapped serialized content", {
|
|
3594
|
+
fieldPath: context.fieldPath,
|
|
3595
|
+
wrappedLength: wrappedSerialized.length,
|
|
3596
|
+
preview: wrappedSerialized.substring(0, 300)
|
|
3597
|
+
});
|
|
3598
|
+
const tempOriginalDoc = {
|
|
3599
|
+
_id: "temp",
|
|
3600
|
+
_type: "richTextWrapper",
|
|
3601
|
+
_createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3602
|
+
_updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3603
|
+
_rev: "temp",
|
|
3604
|
+
content: originalValue
|
|
3605
|
+
}, result = deserializeDocument(tempOriginalDoc, wrappedSerialized);
|
|
3606
|
+
return devLog.debug("Deserialization result", {
|
|
3607
|
+
fieldPath: context.fieldPath,
|
|
3608
|
+
hasContent: !!result.content,
|
|
3609
|
+
contentType: Array.isArray(result.content) ? "array" : typeof result.content,
|
|
3610
|
+
contentLength: Array.isArray(result.content) ? result.content.length : void 0
|
|
3611
|
+
}), result.content ? result.content : (devLog.warn("Deserialization returned no content, returning original", {
|
|
3612
|
+
fieldPath: context.fieldPath,
|
|
3613
|
+
resultKeys: Object.keys(result)
|
|
3614
|
+
}), originalValue);
|
|
3615
|
+
} catch (error) {
|
|
3616
|
+
return devLog.error("Failed to deserialize rich text", error, {
|
|
3617
|
+
fieldPath: context.fieldPath
|
|
3618
|
+
}), originalValue;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
}, vttCaptionTransform = {
|
|
3622
|
+
matcher: {
|
|
3623
|
+
fieldName: "captions",
|
|
3624
|
+
schemaType: "video"
|
|
3625
|
+
},
|
|
3626
|
+
toTranslatable: (captionsArray, context) => {
|
|
3627
|
+
if (devLog.info("VTT captions toTranslatable - input:", {
|
|
3628
|
+
fieldPath: context.fieldPath,
|
|
3629
|
+
isArray: Array.isArray(captionsArray),
|
|
3630
|
+
count: Array.isArray(captionsArray) ? captionsArray.length : 0,
|
|
3631
|
+
firstCaption: Array.isArray(captionsArray) && captionsArray.length > 0 ? captionsArray[0] : null
|
|
3632
|
+
}), !Array.isArray(captionsArray))
|
|
3633
|
+
return devLog.debug("VTT caption field is not an array, returning empty transform", {
|
|
3634
|
+
fieldPath: context.fieldPath,
|
|
3635
|
+
value: captionsArray
|
|
3636
|
+
}), {
|
|
3637
|
+
_type: "translatable.vtt.captions",
|
|
3638
|
+
translationText: "",
|
|
3639
|
+
_originalMetadata: []
|
|
3640
|
+
};
|
|
3641
|
+
devLog.info("Applying VTT caption hybrid transform", {
|
|
3642
|
+
fieldPath: context.fieldPath,
|
|
3643
|
+
count: captionsArray.length
|
|
3644
|
+
});
|
|
3645
|
+
const translationText = serializeVTTCaptionText(captionsArray), originalMetadata = captionsArray.map((caption, index) => {
|
|
3646
|
+
const { text: _text, ...metadata } = caption;
|
|
3647
|
+
return {
|
|
3648
|
+
...metadata,
|
|
3649
|
+
_originalIndex: index
|
|
3650
|
+
};
|
|
3651
|
+
});
|
|
3652
|
+
return devLog.info("VTT captions serialized:", {
|
|
3653
|
+
fieldPath: context.fieldPath,
|
|
3654
|
+
translationTextLength: translationText.length,
|
|
3655
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
|
|
3656
|
+
metadataCount: originalMetadata.length,
|
|
3657
|
+
metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
|
|
3658
|
+
}), {
|
|
3659
|
+
_type: "translatable.vtt.captions",
|
|
3660
|
+
translationText,
|
|
3661
|
+
_originalMetadata: originalMetadata
|
|
3662
|
+
};
|
|
3663
|
+
},
|
|
3664
|
+
fromTranslatable: (translatedValue, _originalValue, context) => {
|
|
3665
|
+
if (devLog.info("VTT captions fromTranslatable - input:", {
|
|
3666
|
+
fieldPath: context.fieldPath,
|
|
3667
|
+
valueType: typeof translatedValue,
|
|
3668
|
+
isCorrectType: translatedValue._type === "translatable.vtt.captions",
|
|
3669
|
+
hasText: !!translatedValue.translationText,
|
|
3670
|
+
hasMetadata: Array.isArray(translatedValue._originalMetadata)
|
|
3671
|
+
}), !translatedValue || translatedValue._type !== "translatable.vtt.captions" || !translatedValue.translationText || !Array.isArray(translatedValue._originalMetadata))
|
|
3672
|
+
return devLog.debug("Value is not a translatable VTT caption object, skipping transform", {
|
|
3673
|
+
fieldPath: context.fieldPath,
|
|
3674
|
+
valueType: typeof translatedValue,
|
|
3675
|
+
translatedValue
|
|
3676
|
+
}), [];
|
|
3677
|
+
devLog.info("Processing translated VTT captions", {
|
|
3678
|
+
fieldPath: context.fieldPath,
|
|
3679
|
+
metadataCount: translatedValue._originalMetadata.length
|
|
3680
|
+
});
|
|
3681
|
+
const { translationText, _originalMetadata } = translatedValue;
|
|
3682
|
+
devLog.info("VTT captions translationText:", {
|
|
3683
|
+
fieldPath: context.fieldPath,
|
|
3684
|
+
translationTextLength: translationText.length,
|
|
3685
|
+
translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
|
|
3686
|
+
});
|
|
3687
|
+
const result = deserializeVTTCaptionText(translationText, _originalMetadata);
|
|
3688
|
+
return devLog.info("VTT captions final result:", {
|
|
3689
|
+
fieldPath: context.fieldPath,
|
|
3690
|
+
resultCount: result.length,
|
|
3691
|
+
resultExample: result.length > 0 ? result[0] : null
|
|
3692
|
+
}), result;
|
|
3693
|
+
}
|
|
3694
|
+
};
|
|
3695
|
+
function serializeVTTCaptionText(captions) {
|
|
3696
|
+
if (devLog.debug("serializeVTTCaptionText - input:", {
|
|
3697
|
+
captionsCount: captions.length,
|
|
3698
|
+
firstCaption: captions.length > 0 ? captions[0] : null
|
|
3699
|
+
}), !Array.isArray(captions) || captions.length === 0)
|
|
3700
|
+
return "";
|
|
3701
|
+
const result = captions.map((caption, index) => (devLog.debug(`Processing caption ${index}:`, {
|
|
3702
|
+
text: caption.text,
|
|
3703
|
+
hasText: typeof caption.text == "string"
|
|
3704
|
+
}), `[${index}] ${caption.text || ""}`)).join(`
|
|
3705
|
+
`);
|
|
3706
|
+
return devLog.debug("serializeVTTCaptionText - output:", {
|
|
3707
|
+
resultLength: result.length,
|
|
3708
|
+
resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
|
|
3709
|
+
}), result;
|
|
3710
|
+
}
|
|
3711
|
+
function deserializeVTTCaptionText(translationText, originalMetadata) {
|
|
3712
|
+
if (devLog.debug("deserializeVTTCaptionText - input:", {
|
|
3713
|
+
translationTextLength: translationText.length,
|
|
3714
|
+
translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
|
|
3715
|
+
metadataCount: originalMetadata.length,
|
|
3716
|
+
metadataExample: originalMetadata.length > 0 ? originalMetadata[0] : null
|
|
3717
|
+
}), !translationText || translationText.trim() === "")
|
|
3718
|
+
return [];
|
|
3719
|
+
const result = [], lines = translationText.split(`
|
|
3720
|
+
`), lineRegex = /^\[(\d+)\]\s*(.*)$/;
|
|
3721
|
+
for (const line of lines) {
|
|
3722
|
+
const match = line.match(lineRegex);
|
|
3723
|
+
if (!match) {
|
|
3724
|
+
devLog.warn("Failed to parse caption line", { line });
|
|
3725
|
+
continue;
|
|
3726
|
+
}
|
|
3727
|
+
const index = Number.parseInt(match[1], 10), text = match[2];
|
|
3728
|
+
devLog.debug("Found caption:", {
|
|
3729
|
+
index,
|
|
3730
|
+
text
|
|
3731
|
+
});
|
|
3732
|
+
const metadata = originalMetadata.find((m) => m._originalIndex === index);
|
|
3733
|
+
if (metadata) {
|
|
3734
|
+
devLog.debug(`Found matching metadata for index ${index}`);
|
|
3735
|
+
const { _originalIndex, ...metadataWithoutIndex } = metadata;
|
|
3736
|
+
result.push({
|
|
3737
|
+
...metadataWithoutIndex,
|
|
3738
|
+
text
|
|
3739
|
+
});
|
|
3740
|
+
} else
|
|
3741
|
+
devLog.warn("Missing metadata for caption", { index }), result.push({
|
|
3742
|
+
_type: "vtt.cue",
|
|
3743
|
+
_key: `caption-${index}`,
|
|
3744
|
+
text,
|
|
3745
|
+
startTime: 0,
|
|
3746
|
+
endTime: 0
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
return result.length < originalMetadata.length && devLog.warn(`Missing captions in translation: found ${result.length} out of ${originalMetadata.length}`), devLog.debug("deserializeVTTCaptionText - output:", {
|
|
3750
|
+
resultCount: result.length,
|
|
3751
|
+
resultExample: result.length > 0 ? result[0] : null
|
|
3752
|
+
}), result;
|
|
3753
|
+
}
|
|
3754
|
+
const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
|
|
3755
|
+
globalTransformRegistry.register(transform);
|
|
3756
|
+
})), {
|
|
3759
3757
|
name: "sanity-plugin-ai-translation",
|
|
3760
3758
|
document: {
|
|
3761
3759
|
actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
|
|
@@ -3766,6 +3764,8 @@ Then return the corrected translation document:`;
|
|
|
3766
3764
|
})), translationHistorySchema = translationHistory;
|
|
3767
3765
|
export {
|
|
3768
3766
|
aiTranslationPlugin,
|
|
3769
|
-
|
|
3767
|
+
richTextTransform,
|
|
3768
|
+
translationHistorySchema,
|
|
3769
|
+
vttCaptionTransform
|
|
3770
3770
|
};
|
|
3771
3771
|
//# sourceMappingURL=index.js.map
|