@braze/sanity-plugin-ai-translation 0.2.1 → 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 +1310 -1266
- 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),
|
|
@@ -1922,70 +1674,214 @@ const fetchTranslationPrompts = async ({
|
|
|
1922
1674
|
status
|
|
1923
1675
|
})
|
|
1924
1676
|
});
|
|
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
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
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
|
+
}
|
|
1979
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'
|
|
1845
|
+
};
|
|
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}`
|
|
1856
|
+
},
|
|
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);
|
|
@@ -2178,10 +2083,27 @@ const fetchTranslationPrompts = async ({
|
|
|
2178
2083
|
return;
|
|
2179
2084
|
}
|
|
2180
2085
|
const children = block.children;
|
|
2181
|
-
Array.isArray(children) && blockNode.children.forEach((spanNode, idx) => {
|
|
2086
|
+
Array.isArray(children) && blockNode.children.some((c) => c.type === "span") ? blockNode.children.forEach((spanNode, idx) => {
|
|
2182
2087
|
const childItem = children[idx];
|
|
2183
2088
|
spanNode.type === "span" && childItem && applySpanChanges(childItem, spanNode);
|
|
2089
|
+
}) : blockNode.children.some((c) => c.type === "field") && blockNode.children.forEach((fieldNode) => {
|
|
2090
|
+
fieldNode.type === "field" && fieldNode.name && applyBlockFieldChanges(block, fieldNode.name, fieldNode);
|
|
2184
2091
|
});
|
|
2092
|
+
}, applyBlockFieldChanges = (block, fieldName, fieldNode) => {
|
|
2093
|
+
const currentValue = block[fieldName];
|
|
2094
|
+
if (fieldNode.attributes.type === "array") {
|
|
2095
|
+
Array.isArray(currentValue) && fieldNode.children.forEach((childNode, idx) => {
|
|
2096
|
+
childNode.type === "block" && currentValue[idx] ? applyBlockChanges(currentValue[idx], childNode) : childNode.type === "field" && applyBlockFieldChanges(currentValue[idx], childNode.name || String(idx), childNode);
|
|
2097
|
+
});
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
if (fieldNode.attributes.type === "object") {
|
|
2101
|
+
currentValue && typeof currentValue == "object" && fieldNode.children.forEach((childNode) => {
|
|
2102
|
+
childNode.name && applyBlockFieldChanges(currentValue, childNode.name, childNode);
|
|
2103
|
+
});
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
fieldNode.text !== void 0 && !fieldNode.attributes.readonly && (block[fieldName] = fieldNode.text);
|
|
2185
2107
|
}, applySpanChanges = (span, spanNode) => {
|
|
2186
2108
|
if (spanNode.attributes._key && span._key !== spanNode.attributes._key) {
|
|
2187
2109
|
devLog.warn("Span key mismatch, skipping", {
|
|
@@ -2198,7 +2120,7 @@ const fetchTranslationPrompts = async ({
|
|
|
2198
2120
|
if (path.length === 0) {
|
|
2199
2121
|
if (isPlainObject(value))
|
|
2200
2122
|
for (const [key, val] of Object.entries(value))
|
|
2201
|
-
!key.startsWith("_") && key !== "language" && serialize(val, [key],
|
|
2123
|
+
!key.startsWith("_") && key !== "language" && serialize(val, [key], 0);
|
|
2202
2124
|
return;
|
|
2203
2125
|
}
|
|
2204
2126
|
const fieldName = path[path.length - 1];
|
|
@@ -2211,6 +2133,14 @@ const fetchTranslationPrompts = async ({
|
|
|
2211
2133
|
return;
|
|
2212
2134
|
}
|
|
2213
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
|
+
}
|
|
2214
2144
|
lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
|
|
2215
2145
|
for (const [key, val] of Object.entries(value))
|
|
2216
2146
|
key.startsWith("_") || serialize(val, [...path, key], indent + 1);
|
|
@@ -2218,512 +2148,172 @@ const fetchTranslationPrompts = async ({
|
|
|
2218
2148
|
return;
|
|
2219
2149
|
}
|
|
2220
2150
|
if (typeof value == "string") {
|
|
2221
|
-
value.length > 0 && lines.push(`${
|
|
2222
|
-
return;
|
|
2223
|
-
}
|
|
2224
|
-
(typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
|
|
2225
|
-
};
|
|
2226
|
-
return serialize(doc), lines.join(`
|
|
2227
|
-
`);
|
|
2228
|
-
}, serializeBlock = (block, path, indent, lines) => {
|
|
2229
|
-
const indentStr = " ".repeat(indent), metadata = {};
|
|
2230
|
-
block._type && (metadata._type = block._type), block._key && (metadata._key = block._key), block.style && (metadata.style = block.style), block.listItem && (metadata.listItem = block.listItem), block.level && (metadata.level = block.level), block.markDefs && (metadata.markDefs = block.markDefs);
|
|
2231
|
-
const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(typeof v == "string" ? v : JSON.stringify(v))}"`).join(" ");
|
|
2232
|
-
lines.push(`${indentStr}<block ${metaStr}>`), Array.isArray(block.children) && block.children.forEach((child, idx) => {
|
|
2233
|
-
serializeSpan(child, [...path, "children", idx], indent + 1, lines);
|
|
2234
|
-
}), lines.push(`${indentStr}</block>`);
|
|
2235
|
-
}, serializeSpan = (span, _path, indent, lines) => {
|
|
2236
|
-
const indentStr = " ".repeat(indent);
|
|
2237
|
-
if (!span || typeof span != "object") return;
|
|
2238
|
-
const metadata = {};
|
|
2239
|
-
span._type && (metadata._type = span._type), span._key && (metadata._key = span._key), span.marks && Array.isArray(span.marks) && (metadata.marks = span.marks.join(","));
|
|
2240
|
-
const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(String(v))}"`).join(" "), text = typeof span.text == "string" ? span.text : "";
|
|
2241
|
-
metaStr ? lines.push(`${indentStr}<span ${metaStr}>${escapeXml(text)}</span>`) : lines.push(`${indentStr}<span>${escapeXml(text)}</span>`);
|
|
2242
|
-
}, 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) => {
|
|
2243
|
-
const errors = [], criticalFields = ["_id", "_type", "_rev", "_createdAt", "_updatedAt"];
|
|
2244
|
-
for (const field of criticalFields)
|
|
2245
|
-
original[field] !== modified[field] && errors.push({
|
|
2246
|
-
path: [field],
|
|
2247
|
-
expected: original[field],
|
|
2248
|
-
actual: modified[field],
|
|
2249
|
-
type: "metadata_changed"
|
|
2250
|
-
});
|
|
2251
|
-
return validateStructureRecursive(original, modified, [], errors), {
|
|
2252
|
-
valid: errors.length === 0,
|
|
2253
|
-
errors
|
|
2254
|
-
};
|
|
2255
|
-
}, validateStructureRecursive = (original, modified, path, errors) => {
|
|
2256
|
-
if (typeof original != typeof modified) {
|
|
2257
|
-
errors.push({
|
|
2258
|
-
path,
|
|
2259
|
-
expected: typeof original,
|
|
2260
|
-
actual: typeof modified,
|
|
2261
|
-
type: "type_mismatch"
|
|
2262
|
-
});
|
|
2263
|
-
return;
|
|
2264
|
-
}
|
|
2265
|
-
if (Array.isArray(original)) {
|
|
2266
|
-
if (!Array.isArray(modified)) {
|
|
2267
|
-
errors.push({
|
|
2268
|
-
path,
|
|
2269
|
-
expected: "array",
|
|
2270
|
-
actual: typeof modified,
|
|
2271
|
-
type: "type_mismatch"
|
|
2272
|
-
});
|
|
2273
|
-
return;
|
|
2274
|
-
}
|
|
2275
|
-
if (original.length !== modified.length) {
|
|
2276
|
-
errors.push({
|
|
2277
|
-
path,
|
|
2278
|
-
expected: `length ${original.length}`,
|
|
2279
|
-
actual: `length ${modified.length}`,
|
|
2280
|
-
type: "type_mismatch"
|
|
2281
|
-
});
|
|
2151
|
+
value.length > 0 && lines.push(`${fieldName}: ${escapeXml(value)}`);
|
|
2282
2152
|
return;
|
|
2283
2153
|
}
|
|
2284
|
-
|
|
2285
|
-
validateStructureRecursive(item, modified[idx], [...path, String(idx)], errors);
|
|
2286
|
-
});
|
|
2287
|
-
return;
|
|
2288
|
-
}
|
|
2289
|
-
if (typeof original == "object" && original !== null && typeof modified == "object" && modified !== null) {
|
|
2290
|
-
const origKeys = Object.keys(original).sort(), modKeys = Object.keys(modified).sort();
|
|
2291
|
-
for (const key of origKeys)
|
|
2292
|
-
key in modified || errors.push({
|
|
2293
|
-
path: [...path, key],
|
|
2294
|
-
expected: "present",
|
|
2295
|
-
actual: "missing",
|
|
2296
|
-
type: "missing"
|
|
2297
|
-
});
|
|
2298
|
-
for (const key of modKeys)
|
|
2299
|
-
key in original || errors.push({
|
|
2300
|
-
path: [...path, key],
|
|
2301
|
-
expected: "absent",
|
|
2302
|
-
actual: "present",
|
|
2303
|
-
type: "extra"
|
|
2304
|
-
});
|
|
2305
|
-
const origObj = original, modObj = modified;
|
|
2306
|
-
for (const key of origKeys)
|
|
2307
|
-
key in modObj && (key.startsWith("_") || key === "marks" || key === "style" || key === "markDefs" ? JSON.stringify(origObj[key]) !== JSON.stringify(modObj[key]) && errors.push({
|
|
2308
|
-
path: [...path, key],
|
|
2309
|
-
expected: origObj[key],
|
|
2310
|
-
actual: modObj[key],
|
|
2311
|
-
type: "metadata_changed"
|
|
2312
|
-
}) : key !== "text" && validateStructureRecursive(origObj[key], modObj[key], [...path, key], errors));
|
|
2313
|
-
}
|
|
2314
|
-
}, assembleTranslationPrompt = (context, components) => {
|
|
2315
|
-
const sections = [
|
|
2316
|
-
{
|
|
2317
|
-
id: "agentDirectives",
|
|
2318
|
-
content: buildAgentDirectivesSection(context),
|
|
2319
|
-
required: !0,
|
|
2320
|
-
priority: 0
|
|
2321
|
-
},
|
|
2322
|
-
{
|
|
2323
|
-
id: "criticalRules",
|
|
2324
|
-
content: buildCriticalRulesSection(context),
|
|
2325
|
-
required: !0,
|
|
2326
|
-
priority: 1
|
|
2327
|
-
}
|
|
2328
|
-
];
|
|
2329
|
-
components.glossaryTerms?.trim() && sections.push({
|
|
2330
|
-
id: "glossaryTerms",
|
|
2331
|
-
content: buildGlossarySection(components.glossaryTerms),
|
|
2332
|
-
required: !1,
|
|
2333
|
-
priority: 3
|
|
2334
|
-
});
|
|
2335
|
-
const styleSection = buildStyleGuideSection(components.globalPrompt, components.localePrompt);
|
|
2336
|
-
styleSection && sections.push({
|
|
2337
|
-
id: "styleGuide",
|
|
2338
|
-
content: styleSection,
|
|
2339
|
-
required: !1,
|
|
2340
|
-
priority: 4
|
|
2341
|
-
}), sections.sort((a, b) => a.priority - b.priority);
|
|
2342
|
-
let styleGuide = "";
|
|
2343
|
-
const omittedSections = [], truncatedSections = [];
|
|
2344
|
-
for (const section of sections) {
|
|
2345
|
-
const prefix = styleGuide.length > 0 ? `
|
|
2346
|
-
|
|
2347
|
-
---
|
|
2348
|
-
|
|
2349
|
-
` : "", availableCharacters = 2e3 - (styleGuide.length + prefix.length);
|
|
2350
|
-
if (availableCharacters <= 0) {
|
|
2351
|
-
if (section.required) {
|
|
2352
|
-
const truncatedContent = truncateSection(section.content, 0);
|
|
2353
|
-
truncatedContent && (styleGuide += prefix + truncatedContent, truncatedSections.push(section.id));
|
|
2354
|
-
} else
|
|
2355
|
-
omittedSections.push(section.id);
|
|
2356
|
-
continue;
|
|
2357
|
-
}
|
|
2358
|
-
if (section.content.length <= availableCharacters) {
|
|
2359
|
-
styleGuide += prefix + section.content;
|
|
2360
|
-
continue;
|
|
2361
|
-
}
|
|
2362
|
-
if (section.required) {
|
|
2363
|
-
const truncatedContent = truncateSection(section.content, availableCharacters);
|
|
2364
|
-
styleGuide += prefix + truncatedContent, truncatedSections.push(section.id);
|
|
2365
|
-
} else
|
|
2366
|
-
omittedSections.push(section.id);
|
|
2367
|
-
}
|
|
2368
|
-
return {
|
|
2369
|
-
styleGuide,
|
|
2370
|
-
omittedSections,
|
|
2371
|
-
truncatedSections
|
|
2372
|
-
};
|
|
2373
|
-
}, truncateSection = (content, limit) => {
|
|
2374
|
-
if (limit <= 0)
|
|
2375
|
-
return "";
|
|
2376
|
-
if (content.length <= limit)
|
|
2377
|
-
return content;
|
|
2378
|
-
if (limit <= 1)
|
|
2379
|
-
return content.slice(0, limit);
|
|
2380
|
-
const ellipsis = "\u2026";
|
|
2381
|
-
if (limit <= ellipsis.length)
|
|
2382
|
-
return content.slice(0, limit);
|
|
2383
|
-
const sliceEnd = limit - ellipsis.length, sliced = content.slice(0, sliceEnd), trimmed = sliced.replace(/\s+\S*$/, "").trimEnd();
|
|
2384
|
-
return `${trimmed.length >= sliceEnd / 2 ? trimmed : sliced}${ellipsis}`;
|
|
2385
|
-
}, buildAgentDirectivesSection = (context) => {
|
|
2386
|
-
const { targetLanguage, sourceLanguage } = context;
|
|
2387
|
-
return `# AGENT WORKFLOW DIRECTIVES
|
|
2388
|
-
|
|
2389
|
-
- Use only the provided ${sourceLanguage || "the source language"} source; never fetch outside context.
|
|
2390
|
-
- Scan each field once, then move directly into translation work.
|
|
2391
|
-
- Plan updates before editing so document structure remains intact.
|
|
2392
|
-
- Default to medium reasoning effort and escalate only for ambiguous passages.
|
|
2393
|
-
- Output just the ${targetLanguage || "the target language"} translation and mirror the source formatting exactly.`;
|
|
2394
|
-
}, buildCriticalRulesSection = (context) => {
|
|
2395
|
-
const { sourceLanguage, targetLanguage } = context, fromLanguage = sourceLanguage || "the source language", toLanguage = targetLanguage || "the target language";
|
|
2396
|
-
return `# CRITICAL TRANSLATION RULES
|
|
2397
|
-
|
|
2398
|
-
Follow these without exception:
|
|
2399
|
-
|
|
2400
|
-
1. Translate from ${fromLanguage} into ${toLanguage}; every output token must be ${toLanguage} unless flagged as protected.
|
|
2401
|
-
2. Preserve meaning exactly\u2014no additions, deletions, or tone shifts.
|
|
2402
|
-
3. Maintain all layout, markdown, rich-text marks, and data structure.
|
|
2403
|
-
4. Never invent metadata or field values that are absent in the source.
|
|
2404
|
-
5. Leave technical tokens (code, IDs, filenames) untouched.
|
|
2405
|
-
6. Keep punctuation, casing, and spacing unless ${toLanguage} grammar forces a change.`;
|
|
2406
|
-
}, buildGlossarySection = (glossaryTerms) => `# TERMINOLOGY RULES
|
|
2407
|
-
|
|
2408
|
-
Apply these mappings immediately after protected phrases and before style preferences.
|
|
2409
|
-
|
|
2410
|
-
${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
|
|
2411
|
-
const components = [];
|
|
2412
|
-
return (globalPrompt || localePrompt) && (components.push("# TRANSLATION STYLE GUIDE (LOWEST PRIORITY)"), components.push(
|
|
2413
|
-
"Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
|
|
2414
|
-
), 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(`
|
|
2415
|
-
`) : null;
|
|
2416
|
-
}, translateEntireDocument = async ({
|
|
2417
|
-
dispatch,
|
|
2418
|
-
client,
|
|
2419
|
-
schemaId,
|
|
2420
|
-
sourceDocument,
|
|
2421
|
-
targetLanguage,
|
|
2422
|
-
languageFieldName,
|
|
2423
|
-
translationTarget,
|
|
2424
|
-
translationPrompts,
|
|
2425
|
-
translationMode,
|
|
2426
|
-
openaiConfig,
|
|
2427
|
-
releaseId,
|
|
2428
|
-
isNewTranslation
|
|
2429
|
-
}) => {
|
|
2430
|
-
const logContext = {
|
|
2431
|
-
documentId: sourceDocument._id,
|
|
2432
|
-
targetLanguage,
|
|
2433
|
-
schemaId,
|
|
2434
|
-
operation: translationTarget.operation,
|
|
2435
|
-
targetId: translationTarget._id,
|
|
2436
|
-
translationMode,
|
|
2437
|
-
releaseId,
|
|
2438
|
-
isNewTranslation
|
|
2439
|
-
};
|
|
2440
|
-
devLog.group("translateEntireDocument", logContext), devLog.time("translateEntireDocument");
|
|
2441
|
-
try {
|
|
2442
|
-
let versionedDocumentId, baseDocumentId;
|
|
2443
|
-
if (isNewTranslation) {
|
|
2444
|
-
const { _id, _createdAt, _updatedAt, _rev, ...docWithoutMeta } = sourceDocument, newTranslationDoc = {
|
|
2445
|
-
...docWithoutMeta,
|
|
2446
|
-
_type: sourceDocument._type,
|
|
2447
|
-
[languageFieldName]: targetLanguage
|
|
2448
|
-
};
|
|
2449
|
-
devLog.info("Creating new translation document in release", logContext), baseDocumentId = translationTarget._id || `new-translation-${Date.now()}`, await client.action({
|
|
2450
|
-
actionType: "sanity.action.document.version.create",
|
|
2451
|
-
publishedId: baseDocumentId,
|
|
2452
|
-
document: {
|
|
2453
|
-
...newTranslationDoc,
|
|
2454
|
-
_type: sourceDocument._type,
|
|
2455
|
-
_id: `versions.${releaseId}.${baseDocumentId}`
|
|
2456
|
-
}
|
|
2457
|
-
}), versionedDocumentId = `versions.${releaseId}.${baseDocumentId}`, devLog.info("New translation version created in release", {
|
|
2458
|
-
...logContext,
|
|
2459
|
-
versionedDocumentId,
|
|
2460
|
-
baseDocumentId
|
|
2461
|
-
});
|
|
2462
|
-
} else {
|
|
2463
|
-
const existingTranslationId = translationTarget._id;
|
|
2464
|
-
if (!existingTranslationId)
|
|
2465
|
-
throw new Error("Translation target ID not found for update operation");
|
|
2466
|
-
devLog.info("Creating version of existing translation in release", {
|
|
2467
|
-
...logContext,
|
|
2468
|
-
existingTranslationId
|
|
2469
|
-
});
|
|
2470
|
-
const existingDoc = await client.getDocument(existingTranslationId);
|
|
2471
|
-
if (!existingDoc)
|
|
2472
|
-
throw new Error(`Existing translation document not found: ${existingTranslationId}`);
|
|
2473
|
-
await client.action({
|
|
2474
|
-
actionType: "sanity.action.document.version.create",
|
|
2475
|
-
publishedId: existingTranslationId,
|
|
2476
|
-
document: {
|
|
2477
|
-
...existingDoc,
|
|
2478
|
-
_id: `versions.${releaseId}.${existingTranslationId}`
|
|
2479
|
-
}
|
|
2480
|
-
}), versionedDocumentId = `versions.${releaseId}.${existingTranslationId}`, baseDocumentId = existingTranslationId, devLog.info("Existing translation version created", {
|
|
2481
|
-
...logContext,
|
|
2482
|
-
versionedDocumentId
|
|
2483
|
-
});
|
|
2484
|
-
const { _id, _createdAt, _updatedAt, _rev, ...sourceContent } = sourceDocument;
|
|
2485
|
-
await client.createOrReplace({
|
|
2486
|
-
...sourceContent,
|
|
2487
|
-
_id: versionedDocumentId,
|
|
2488
|
-
[languageFieldName]: targetLanguage
|
|
2489
|
-
}), devLog.info("Source content cloned onto version", {
|
|
2490
|
-
...logContext,
|
|
2491
|
-
versionedDocumentId
|
|
2492
|
-
});
|
|
2493
|
-
}
|
|
2494
|
-
let translatedDocument;
|
|
2495
|
-
switch (translationMode) {
|
|
2496
|
-
case "openai-only": {
|
|
2497
|
-
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2498
|
-
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2499
|
-
devLog.info("Using OpenAI-only translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai");
|
|
2500
|
-
const versionedDoc = await client.getDocument(versionedDocumentId);
|
|
2501
|
-
if (!versionedDoc)
|
|
2502
|
-
throw new Error(`Versioned document not found: ${versionedDocumentId}`);
|
|
2503
|
-
const translatedDoc = await translateInPlaceWithOpenAI({
|
|
2504
|
-
dispatch,
|
|
2505
|
-
sourceDocument,
|
|
2506
|
-
targetDocument: versionedDoc,
|
|
2507
|
-
targetLanguage,
|
|
2508
|
-
languageFieldName,
|
|
2509
|
-
translationPrompts,
|
|
2510
|
-
openaiConfig,
|
|
2511
|
-
isReview: !1
|
|
2512
|
-
});
|
|
2513
|
-
translatedDoc && (translatedDocument = await client.createOrReplace({
|
|
2514
|
-
...translatedDoc,
|
|
2515
|
-
_id: versionedDocumentId
|
|
2516
|
-
}));
|
|
2517
|
-
break;
|
|
2518
|
-
}
|
|
2519
|
-
case "sanity-with-review":
|
|
2520
|
-
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2521
|
-
throw new Error("OpenAI is not configured but sanity-with-review mode was selected");
|
|
2522
|
-
if (devLog.info("Using Sanity + OpenAI review", logContext), translatedDocument = await translateWithSanityAgent({
|
|
2523
|
-
dispatch,
|
|
2524
|
-
client,
|
|
2525
|
-
schemaId,
|
|
2526
|
-
sourceDocument,
|
|
2527
|
-
targetLanguage,
|
|
2528
|
-
languageFieldName,
|
|
2529
|
-
versionedDocumentId,
|
|
2530
|
-
translationPrompts
|
|
2531
|
-
}), translatedDocument) {
|
|
2532
|
-
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "reviewing_with_openai");
|
|
2533
|
-
const reviewedDoc = await translateInPlaceWithOpenAI({
|
|
2534
|
-
dispatch,
|
|
2535
|
-
sourceDocument,
|
|
2536
|
-
targetDocument: translatedDocument,
|
|
2537
|
-
targetLanguage,
|
|
2538
|
-
languageFieldName,
|
|
2539
|
-
translationPrompts,
|
|
2540
|
-
openaiConfig,
|
|
2541
|
-
isReview: !0
|
|
2542
|
-
});
|
|
2543
|
-
reviewedDoc && (translatedDocument = await client.createOrReplace({
|
|
2544
|
-
...reviewedDoc,
|
|
2545
|
-
_id: versionedDocumentId
|
|
2546
|
-
}));
|
|
2547
|
-
}
|
|
2548
|
-
break;
|
|
2549
|
-
default:
|
|
2550
|
-
devLog.info("Using Sanity-only translation", logContext), translatedDocument = await translateWithSanityAgent({
|
|
2551
|
-
dispatch,
|
|
2552
|
-
client,
|
|
2553
|
-
schemaId,
|
|
2554
|
-
sourceDocument,
|
|
2555
|
-
targetLanguage,
|
|
2556
|
-
languageFieldName,
|
|
2557
|
-
versionedDocumentId,
|
|
2558
|
-
translationPrompts
|
|
2559
|
-
});
|
|
2560
|
-
break;
|
|
2561
|
-
}
|
|
2562
|
-
return devLog.timeEnd("translateEntireDocument"), devLog.groupEnd(), translatedDocument ? {
|
|
2563
|
-
...translatedDocument,
|
|
2564
|
-
_id: baseDocumentId
|
|
2565
|
-
} : void 0;
|
|
2566
|
-
} catch (error) {
|
|
2567
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2568
|
-
devLog.error(
|
|
2569
|
-
"translateEntireDocument failed",
|
|
2570
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
2571
|
-
logContext
|
|
2572
|
-
), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
2573
|
-
errorMessage
|
|
2574
|
-
}), devLog.timeEnd("translateEntireDocument"), devLog.groupEnd();
|
|
2575
|
-
return;
|
|
2576
|
-
}
|
|
2577
|
-
}, translateWithSanityAgent = async ({
|
|
2578
|
-
dispatch,
|
|
2579
|
-
client,
|
|
2580
|
-
schemaId,
|
|
2581
|
-
sourceDocument,
|
|
2582
|
-
targetLanguage,
|
|
2583
|
-
languageFieldName,
|
|
2584
|
-
versionedDocumentId,
|
|
2585
|
-
translationPrompts
|
|
2586
|
-
}) => {
|
|
2587
|
-
const logContext = {
|
|
2588
|
-
documentId: sourceDocument._id,
|
|
2589
|
-
targetLanguage,
|
|
2590
|
-
versionedDocumentId
|
|
2154
|
+
(typeof value == "number" || typeof value == "boolean") && lines.push(`${indentStr}<field name="${fieldName}" type="${typeof value}" readonly="true">${value}</field>`);
|
|
2591
2155
|
};
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
fromLanguage
|
|
2156
|
+
return serialize(doc), lines.join(`
|
|
2157
|
+
`);
|
|
2158
|
+
}, serializeBlock = (block, path, indent, lines) => {
|
|
2159
|
+
const indentStr = " ".repeat(indent), metadata = {};
|
|
2160
|
+
block._type && (metadata._type = block._type), block._key && (metadata._key = block._key), block.style && (metadata.style = block.style), block.listItem && (metadata.listItem = block.listItem), block.level && (metadata.level = block.level), block.markDefs && (metadata.markDefs = block.markDefs);
|
|
2161
|
+
const metaStr = Object.entries(metadata).map(([k, v]) => `${k}="${escapeXml(typeof v == "string" ? v : JSON.stringify(v))}"`).join(" ");
|
|
2162
|
+
if (lines.push(`${indentStr}<block ${metaStr}>`), Array.isArray(block.children) && block._type === "block")
|
|
2163
|
+
block.children.forEach((child, idx) => {
|
|
2164
|
+
serializeSpan(child, [...path, "children", idx], indent + 1, lines);
|
|
2602
2165
|
});
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
...
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2166
|
+
else
|
|
2167
|
+
for (const [key, value] of Object.entries(block))
|
|
2168
|
+
key.startsWith("_") || key === "style" || key === "listItem" || key === "level" || key === "markDefs" || serializeBlockField(value, key, [...path, key], indent + 1, lines);
|
|
2169
|
+
lines.push(`${indentStr}</block>`);
|
|
2170
|
+
}, serializeBlockField = (value, fieldName, path, indent, lines) => {
|
|
2171
|
+
const indentStr = " ".repeat(indent);
|
|
2172
|
+
if (Array.isArray(value)) {
|
|
2173
|
+
value.some((item) => item?._type) ? (lines.push(`${indentStr}<field name="${fieldName}" type="array">`), value.forEach((item, idx) => {
|
|
2174
|
+
item?._type === "block" && item?.children ? serializeBlock(item, [...path, idx], indent + 1, lines) : item?._type ? serializeBlock(item, [...path, idx], indent + 1, lines) : serializeBlockField(item, String(idx), [...path, idx], indent + 1, lines);
|
|
2175
|
+
}), lines.push(`${indentStr}</field>`)) : (lines.push(`${indentStr}<field name="${fieldName}" type="array">`), value.forEach((item, idx) => {
|
|
2176
|
+
serializeBlockField(item, String(idx), [...path, idx], indent + 1, lines);
|
|
2177
|
+
}), lines.push(`${indentStr}</field>`));
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
if (isPlainObject(value)) {
|
|
2181
|
+
lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
|
|
2182
|
+
for (const [key, val] of Object.entries(value))
|
|
2183
|
+
key.startsWith("_") || serializeBlockField(val, key, [...path, key], indent + 1, lines);
|
|
2184
|
+
lines.push(`${indentStr}</field>`);
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
if (typeof value == "string" && value.length > 0) {
|
|
2188
|
+
lines.push(`${indentStr}<field name="${fieldName}">${escapeXml(value)}</field>`);
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
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"
|
|
2632
2207
|
});
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
defaultReadOnly: !1
|
|
2645
|
-
}
|
|
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"
|
|
2646
2219
|
});
|
|
2647
|
-
return
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
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);
|
|
2660
2243
|
});
|
|
2661
2244
|
return;
|
|
2662
2245
|
}
|
|
2663
|
-
|
|
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 ({
|
|
2664
2272
|
dispatch,
|
|
2665
2273
|
sourceDocument,
|
|
2666
|
-
targetDocument,
|
|
2667
|
-
targetLanguage,
|
|
2668
2274
|
languageFieldName,
|
|
2669
2275
|
translationPrompts,
|
|
2670
|
-
|
|
2671
|
-
|
|
2276
|
+
targetLanguage,
|
|
2277
|
+
openaiConfig
|
|
2672
2278
|
}) => {
|
|
2673
2279
|
const sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
2674
2280
|
sourceId: sourceDocument._id,
|
|
2675
|
-
|
|
2676
|
-
targetLanguage,
|
|
2677
|
-
sourceLanguage,
|
|
2678
|
-
isReview
|
|
2281
|
+
sourceLanguage
|
|
2679
2282
|
};
|
|
2680
2283
|
try {
|
|
2681
|
-
const sourceText = serializeDocument(sourceDocument)
|
|
2682
|
-
devLog.
|
|
2284
|
+
const sourceText = serializeDocument(sourceDocument);
|
|
2285
|
+
devLog.info("Serialized document for OpenAI", {
|
|
2683
2286
|
...logContext,
|
|
2684
|
-
|
|
2685
|
-
|
|
2287
|
+
characterCount: sourceText.length,
|
|
2288
|
+
estimatedTokens: Math.ceil(sourceText.length / 4),
|
|
2289
|
+
preview: sourceText.substring(0, 500)
|
|
2686
2290
|
});
|
|
2687
|
-
const prompt =
|
|
2688
|
-
sourceText,
|
|
2689
|
-
translatedText: targetText,
|
|
2690
|
-
sourceLanguage,
|
|
2691
|
-
targetLanguage,
|
|
2692
|
-
translationPrompts
|
|
2693
|
-
}) : buildTranslationPrompt({
|
|
2291
|
+
const prompt = buildTranslationPrompt$1({
|
|
2694
2292
|
sourceText,
|
|
2695
2293
|
sourceLanguage,
|
|
2696
2294
|
targetLanguage,
|
|
2697
2295
|
translationPrompts
|
|
2698
|
-
})
|
|
2699
|
-
devLog.debug("Calling GPT-5", {
|
|
2700
|
-
...logContext,
|
|
2701
|
-
promptLength: prompt.length
|
|
2702
|
-
});
|
|
2703
|
-
const response = await callGPT5(openaiConfig.apiKey, {
|
|
2296
|
+
}), response = await callGPT5(openaiConfig.apiKey, {
|
|
2704
2297
|
messages: [
|
|
2705
2298
|
{
|
|
2706
|
-
role: "
|
|
2707
|
-
content:
|
|
2299
|
+
role: "developer",
|
|
2300
|
+
content: "You are a translation quality reviewer. Review and refine translations while preserving structure."
|
|
2708
2301
|
},
|
|
2709
2302
|
{ role: "user", content: prompt }
|
|
2710
2303
|
],
|
|
2711
2304
|
model: openaiConfig.model,
|
|
2712
|
-
reasoningEffort: openaiConfig.reasoningEffort
|
|
2713
|
-
verbosity: openaiConfig.verbosity,
|
|
2714
|
-
onReasoningChunk: (reasoning) => {
|
|
2715
|
-
const status2 = isReview ? "reviewing_with_openai" : "translating_with_openai";
|
|
2716
|
-
setJobStatus(dispatch, sourceDocument._id, targetLanguage, status2, {
|
|
2717
|
-
reasoningProgress: reasoning
|
|
2718
|
-
});
|
|
2719
|
-
}
|
|
2305
|
+
reasoningEffort: openaiConfig.reasoningEffort
|
|
2720
2306
|
});
|
|
2721
|
-
devLog.info("OpenAI
|
|
2307
|
+
devLog.info("OpenAI translation response received", {
|
|
2722
2308
|
...logContext,
|
|
2723
|
-
tokensUsed: response.tokensUsed
|
|
2724
|
-
cost: response.cost
|
|
2309
|
+
tokensUsed: response.tokensUsed,
|
|
2310
|
+
cost: response.cost,
|
|
2311
|
+
responseLength: response.content.length,
|
|
2312
|
+
responsePreview: response.content.substring(0, 500)
|
|
2725
2313
|
});
|
|
2726
|
-
const result = deserializeDocument(
|
|
2314
|
+
const result = deserializeDocument(sourceDocument, response.content);
|
|
2315
|
+
result[languageFieldName] = targetLanguage;
|
|
2316
|
+
const validation = validateStructureIntegrity(sourceDocument, result);
|
|
2727
2317
|
if (!validation.valid) {
|
|
2728
2318
|
devLog.error("Structure validation failed", {
|
|
2729
2319
|
...logContext,
|
|
@@ -2737,17 +2327,14 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
|
|
|
2737
2327
|
`Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
|
|
2738
2328
|
);
|
|
2739
2329
|
}
|
|
2740
|
-
|
|
2741
|
-
const status = isReview ? "reviewing_with_openai" : "translating_with_openai";
|
|
2742
|
-
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, status, {
|
|
2330
|
+
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
|
|
2743
2331
|
tokensUsed: response.tokensUsed.total,
|
|
2744
|
-
estimatedCost: response.cost
|
|
2745
|
-
reasoningSummary: response.reasoningSummary
|
|
2332
|
+
estimatedCost: response.cost
|
|
2746
2333
|
}), result;
|
|
2747
2334
|
} catch (error) {
|
|
2748
2335
|
throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
|
|
2749
2336
|
}
|
|
2750
|
-
}, buildTranslationPrompt = ({
|
|
2337
|
+
}, buildTranslationPrompt$1 = ({
|
|
2751
2338
|
sourceText,
|
|
2752
2339
|
sourceLanguage,
|
|
2753
2340
|
targetLanguage,
|
|
@@ -2794,73 +2381,193 @@ Brief summary of key translation decisions and reasoning
|
|
|
2794
2381
|
</reasoning>
|
|
2795
2382
|
|
|
2796
2383
|
Then return the translated document:`;
|
|
2797
|
-
},
|
|
2798
|
-
|
|
2799
|
-
|
|
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 = ({
|
|
2800
2403
|
sourceLanguage,
|
|
2801
2404
|
targetLanguage,
|
|
2802
|
-
|
|
2405
|
+
components,
|
|
2406
|
+
isUpdate = !1
|
|
2803
2407
|
}) => {
|
|
2804
|
-
const
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
## 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(`
|
|
2810
2412
|
|
|
2811
|
-
|
|
2812
|
-
2. **Terminology**: Are glossary terms used correctly?
|
|
2813
|
-
3. **Protected Phrases**: Are protected phrases preserved?
|
|
2814
|
-
4. **Fluency**: Is it natural and fluent in ${targetLanguage}?
|
|
2815
|
-
5. **Style**: Does it follow the style guide?
|
|
2816
|
-
6. **Completeness**: Is all content translated?
|
|
2413
|
+
---
|
|
2817
2414
|
|
|
2818
|
-
|
|
2415
|
+
`);
|
|
2416
|
+
}, buildAgentDirectivesSection = (sourceLanguage, targetLanguage) => `# AGENT WORKFLOW DIRECTIVES
|
|
2819
2417
|
|
|
2820
|
-
${
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
${
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
${glossaryTerms}
|
|
2828
|
-
` : ""}
|
|
2829
|
-
${protectedPhrases?.length ? `### Protected Phrases (must preserve)
|
|
2830
|
-
${protectedPhrases.join(", ")}
|
|
2831
|
-
` : ""}
|
|
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
|
|
2832
2425
|
|
|
2833
|
-
|
|
2426
|
+
Follow these without exception:
|
|
2834
2427
|
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
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
|
|
2838
2435
|
|
|
2839
|
-
|
|
2436
|
+
This document already has a translation; only adjust fields that changed.
|
|
2840
2437
|
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
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
|
|
2844
2441
|
|
|
2845
|
-
|
|
2442
|
+
Apply these mappings immediately after protected phrases and before style preferences.
|
|
2846
2443
|
|
|
2847
|
-
|
|
2848
|
-
2. Make corrections where needed to improve accuracy, fluency, or adherence to guidelines
|
|
2849
|
-
3. Keep good translations as-is - only fix what needs fixing
|
|
2850
|
-
4. Return the corrected translation in THE EXACT SAME FORMAT
|
|
2851
|
-
5. **CRITICAL**: Only modify text content between tags
|
|
2852
|
-
6. Never add, remove, or change any tags, attributes, or structure
|
|
2444
|
+
${glossaryTerms}`, buildProtectedPhrasesSection = (protectedPhrases) => `# PROTECTED PHRASES
|
|
2853
2445
|
|
|
2854
|
-
|
|
2855
|
-
<reasoning>
|
|
2856
|
-
Brief summary of review findings and corrections made
|
|
2857
|
-
</reasoning>
|
|
2446
|
+
These phrases must NEVER be translated. Keep them exactly as they appear:
|
|
2858
2447
|
|
|
2859
|
-
|
|
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;
|
|
2860
2566
|
}, translateDocument = async ({
|
|
2861
2567
|
dispatch,
|
|
2862
2568
|
client,
|
|
2863
2569
|
sourceDocument,
|
|
2570
|
+
sourceLanguage,
|
|
2864
2571
|
targetLanguage,
|
|
2865
2572
|
languageFieldName,
|
|
2866
2573
|
shouldOverwriteFreshTranslations,
|
|
@@ -2879,62 +2586,37 @@ Then return the corrected translation document:`;
|
|
|
2879
2586
|
};
|
|
2880
2587
|
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
|
|
2881
2588
|
try {
|
|
2882
|
-
const sourceLanguage = sourceDocument[languageFieldName];
|
|
2883
|
-
if (devLog.debug("Checking source document language", {
|
|
2884
|
-
...logContext,
|
|
2885
|
-
sourceLanguage,
|
|
2886
|
-
languageFieldName
|
|
2887
|
-
}), !sourceLanguage) {
|
|
2888
|
-
const error = `Source document ${sourceDocument._id} does not have a language`;
|
|
2889
|
-
throw devLog.error(error, void 0, logContext), new Error(error);
|
|
2890
|
-
}
|
|
2891
|
-
devLog.debug("Getting translation metadata", logContext);
|
|
2892
2589
|
const translationMetadata = await getTranslationMetadata({
|
|
2893
2590
|
client,
|
|
2894
2591
|
sourceDocument,
|
|
2895
2592
|
sourceLanguage
|
|
2896
2593
|
}), isAlreadyFreshTranslation = translationMetadata.translationHistory?.some(
|
|
2897
2594
|
(history) => history.language === targetLanguage && history.sourceRev === sourceDocument._rev
|
|
2898
|
-
)
|
|
2899
|
-
if (
|
|
2900
|
-
|
|
2901
|
-
isAlreadyFreshTranslation,
|
|
2902
|
-
currentRev: sourceDocument._rev
|
|
2903
|
-
}), !shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
|
|
2904
|
-
devLog.info("Skipping fresh translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
|
|
2595
|
+
);
|
|
2596
|
+
if (!shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
|
|
2597
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
|
|
2905
2598
|
targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
|
|
2906
|
-
})
|
|
2907
|
-
return;
|
|
2908
|
-
}
|
|
2909
|
-
const
|
|
2910
|
-
translationMetadata,
|
|
2911
|
-
targetLanguage
|
|
2912
|
-
}), isNewTranslation = translationTarget.operation === "create";
|
|
2913
|
-
devLog.debug("Translation target determined", {
|
|
2914
|
-
...logContext,
|
|
2915
|
-
operation: translationTarget.operation,
|
|
2916
|
-
isNewTranslation
|
|
2917
|
-
});
|
|
2918
|
-
const newDocument = await translateEntireDocument({
|
|
2599
|
+
});
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
|
|
2919
2603
|
dispatch,
|
|
2920
2604
|
client,
|
|
2921
2605
|
schemaId,
|
|
2922
2606
|
sourceDocument,
|
|
2923
2607
|
targetLanguage,
|
|
2608
|
+
existingTranslationId,
|
|
2924
2609
|
languageFieldName,
|
|
2925
|
-
translationTarget,
|
|
2926
2610
|
translationPrompts,
|
|
2927
2611
|
translationMode,
|
|
2928
2612
|
openaiConfig,
|
|
2929
|
-
releaseId
|
|
2930
|
-
isNewTranslation
|
|
2613
|
+
releaseId
|
|
2931
2614
|
});
|
|
2932
|
-
if (!newDocument)
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
}), 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", {
|
|
2938
2620
|
targetId: newDocument._id
|
|
2939
2621
|
}), await updateTranslationMetadata({
|
|
2940
2622
|
client,
|
|
@@ -2943,8 +2625,10 @@ Then return the corrected translation document:`;
|
|
|
2943
2625
|
newDocument,
|
|
2944
2626
|
targetLanguage,
|
|
2945
2627
|
translationMetadata,
|
|
2946
|
-
isNewTranslation
|
|
2947
|
-
}), 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", {
|
|
2948
2632
|
...logContext,
|
|
2949
2633
|
newDocumentId: newDocument._id
|
|
2950
2634
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
@@ -2970,77 +2654,61 @@ Then return the corrected translation document:`;
|
|
|
2970
2654
|
}) => {
|
|
2971
2655
|
const logContext = {
|
|
2972
2656
|
documentId: sourceDocument._id,
|
|
2657
|
+
documentRev: sourceDocument._rev,
|
|
2973
2658
|
targetLanguages,
|
|
2974
2659
|
referencesCount: references.length,
|
|
2975
2660
|
maxConcurrentJobs: maxConcurrentTranslationJobs,
|
|
2976
2661
|
translationMode,
|
|
2977
2662
|
shouldOverwriteFreshTranslations
|
|
2978
2663
|
};
|
|
2979
|
-
devLog.group("translateDocumentSet", logContext), devLog.time("translateDocumentSet");
|
|
2980
2664
|
let releaseId;
|
|
2981
2665
|
try {
|
|
2982
2666
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
2983
2667
|
if (!sourceLanguage)
|
|
2984
2668
|
throw devLog.error("Source language not found"), new Error("Source language not found");
|
|
2985
|
-
maxConcurrentTranslationJobs &&
|
|
2986
|
-
...logContext,
|
|
2987
|
-
newPermits: maxConcurrentTranslationJobs
|
|
2988
|
-
}), translationSemaphore.updatePermits(maxConcurrentTranslationJobs)), devLog.info("Starting document set translation", logContext), dispatch({
|
|
2669
|
+
maxConcurrentTranslationJobs && translationSemaphore.updatePermits(maxConcurrentTranslationJobs), dispatch({
|
|
2989
2670
|
type: "SET_OVERALL_STATUS",
|
|
2990
2671
|
payload: "translating"
|
|
2991
2672
|
});
|
|
2992
|
-
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()}
|
|
2993
|
-
devLog.info("Creating Content Release for translation batch", {
|
|
2994
|
-
...logContext,
|
|
2995
|
-
releaseTitle
|
|
2996
|
-
});
|
|
2997
|
-
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({
|
|
2998
2674
|
metadata: {
|
|
2999
2675
|
title: releaseTitle,
|
|
3000
2676
|
description: releaseDescription,
|
|
3001
|
-
releaseType: "
|
|
2677
|
+
releaseType: "undecided"
|
|
3002
2678
|
}
|
|
3003
2679
|
});
|
|
3004
|
-
|
|
2680
|
+
devLog.info("Content Release created", {
|
|
3005
2681
|
...logContext,
|
|
3006
|
-
releaseId
|
|
2682
|
+
releaseId: releaseId2
|
|
3007
2683
|
});
|
|
3008
2684
|
const sourceDocuments = [
|
|
3009
2685
|
{
|
|
3010
2686
|
_id: sourceDocument._id,
|
|
3011
2687
|
_type: sourceDocument._type,
|
|
3012
|
-
// TODO:
|
|
2688
|
+
// TODO: Extract document title more robustly across schema types
|
|
3013
2689
|
title: sourceDocument.name || sourceDocument.title
|
|
3014
2690
|
},
|
|
3015
2691
|
...references
|
|
3016
|
-
], sourceDocumentIds = sourceDocuments.map((doc) => doc._id),
|
|
3017
|
-
devLog.debug("Prepared document set for translation", {
|
|
3018
|
-
...logContext,
|
|
3019
|
-
sourceDocumentIds: sourceDocumentIds.length,
|
|
3020
|
-
totalTranslationJobs: sourceDocumentIds.length * targetLanguages.length
|
|
3021
|
-
});
|
|
3022
|
-
const translationPrompts = await fetchTranslationPrompts({
|
|
2692
|
+
], sourceDocumentIds = sourceDocuments.map((doc) => doc._id), versionedTargetDocumentIds = [], translationPrompts = await fetchTranslationPrompts({
|
|
3023
2693
|
client,
|
|
3024
2694
|
sourceLanguage,
|
|
3025
2695
|
targetLanguages,
|
|
3026
2696
|
sourceDocument
|
|
3027
2697
|
});
|
|
3028
|
-
|
|
2698
|
+
setupTranslationJobStatuses(dispatch, sourceDocuments, targetLanguages);
|
|
3029
2699
|
const translationTasks = sourceDocumentIds.flatMap(
|
|
3030
2700
|
(documentId) => targetLanguages.map(async (targetLanguage) => {
|
|
3031
2701
|
try {
|
|
3032
2702
|
await translationSemaphore.withPermit(async () => {
|
|
3033
2703
|
const document = await client.withConfig({
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
});
|
|
3039
|
-
if (document && releaseId) {
|
|
3040
|
-
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({
|
|
3041
2708
|
dispatch,
|
|
3042
2709
|
client,
|
|
3043
2710
|
sourceDocument: document,
|
|
2711
|
+
sourceLanguage,
|
|
3044
2712
|
targetLanguage,
|
|
3045
2713
|
languageFieldName,
|
|
3046
2714
|
shouldOverwriteFreshTranslations,
|
|
@@ -3048,9 +2716,9 @@ Then return the corrected translation document:`;
|
|
|
3048
2716
|
translationPrompts,
|
|
3049
2717
|
translationMode,
|
|
3050
2718
|
openaiConfig,
|
|
3051
|
-
releaseId
|
|
2719
|
+
releaseId: releaseId2
|
|
3052
2720
|
});
|
|
3053
|
-
|
|
2721
|
+
versionedTargetDocumentId && versionedTargetDocumentIds.push(versionedTargetDocumentId);
|
|
3054
2722
|
}
|
|
3055
2723
|
});
|
|
3056
2724
|
} catch (error) {
|
|
@@ -3066,25 +2734,21 @@ Then return the corrected translation document:`;
|
|
|
3066
2734
|
}
|
|
3067
2735
|
})
|
|
3068
2736
|
), results = await Promise.allSettled(translationTasks), succeeded = results.filter((r) => r.status === "fulfilled").length, failed = results.filter((r) => r.status === "rejected").length;
|
|
3069
|
-
devLog.info("Translation tasks
|
|
2737
|
+
devLog.info("Translation tasks completed", {
|
|
3070
2738
|
...logContext,
|
|
3071
2739
|
total: results.length,
|
|
3072
2740
|
succeeded,
|
|
3073
|
-
failed
|
|
3074
|
-
|
|
3075
|
-
...logContext,
|
|
3076
|
-
targetDocumentIds: targetDocumentIds.length
|
|
2741
|
+
failed,
|
|
2742
|
+
targetDocuments: versionedTargetDocumentIds.length
|
|
3077
2743
|
}), dispatch({
|
|
3078
2744
|
type: "SET_OVERALL_STATUS",
|
|
3079
2745
|
payload: "linking_references"
|
|
3080
|
-
}), devLog.debug("Starting reference localization", {
|
|
3081
|
-
...logContext,
|
|
3082
|
-
documentsToLocalize: targetDocumentIds.length
|
|
3083
2746
|
}), await Promise.all(
|
|
3084
|
-
|
|
2747
|
+
versionedTargetDocumentIds.map(async (documentId) => {
|
|
3085
2748
|
await localizeReferences({
|
|
3086
2749
|
client,
|
|
3087
2750
|
documentId,
|
|
2751
|
+
sourceLanguage,
|
|
3088
2752
|
languageFieldName
|
|
3089
2753
|
});
|
|
3090
2754
|
})
|
|
@@ -3094,26 +2758,13 @@ Then return the corrected translation document:`;
|
|
|
3094
2758
|
...logContext,
|
|
3095
2759
|
succeeded,
|
|
3096
2760
|
failed
|
|
3097
|
-
}), 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();
|
|
3098
2762
|
} catch (error) {
|
|
3099
|
-
|
|
2763
|
+
throw devLog.error(
|
|
3100
2764
|
"translateDocumentSet failed with unexpected error",
|
|
3101
2765
|
error instanceof Error ? error : new Error(String(error)),
|
|
3102
2766
|
logContext
|
|
3103
|
-
),
|
|
3104
|
-
try {
|
|
3105
|
-
await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
|
|
3106
|
-
...logContext,
|
|
3107
|
-
releaseId
|
|
3108
|
-
});
|
|
3109
|
-
} catch (cleanupError) {
|
|
3110
|
-
devLog.error(
|
|
3111
|
-
"Failed to cleanup release",
|
|
3112
|
-
cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
|
|
3113
|
-
logContext
|
|
3114
|
-
);
|
|
3115
|
-
}
|
|
3116
|
-
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;
|
|
3117
2768
|
}
|
|
3118
2769
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
3119
2770
|
const {
|
|
@@ -3534,24 +3185,12 @@ Then return the corrected translation document:`;
|
|
|
3534
3185
|
" \u2014 ",
|
|
3535
3186
|
job._id.slice(-6)
|
|
3536
3187
|
] }),
|
|
3537
|
-
!["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */
|
|
3538
|
-
|
|
3539
|
-
job.
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
] }),
|
|
3544
|
-
job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3545
|
-
/* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3546
|
-
job.tokensUsed.toLocaleString(),
|
|
3547
|
-
" tokens (~$",
|
|
3548
|
-
job.estimatedCost?.toFixed(4),
|
|
3549
|
-
")"
|
|
3550
|
-
] }),
|
|
3551
|
-
job.reasoningSummary && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3552
|
-
"\u{1F4A1} ",
|
|
3553
|
-
job.reasoningSummary
|
|
3554
|
-
] }) })
|
|
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
|
+
")"
|
|
3555
3194
|
] }) : null,
|
|
3556
3195
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3557
3196
|
"Error: ",
|
|
@@ -3665,26 +3304,9 @@ Then return the corrected translation document:`;
|
|
|
3665
3304
|
"completed"
|
|
3666
3305
|
], STATES_IN_WHICH_TO_SHOW_ERROR = ["error"], TranslationDialog = ({ setIsDialogOpen }) => {
|
|
3667
3306
|
const { overallStatus, document } = useTranslationContext();
|
|
3668
|
-
return devLog.
|
|
3669
|
-
documentId: document._id,
|
|
3670
|
-
overallStatus,
|
|
3671
|
-
component: "TranslationDialog"
|
|
3672
|
-
}), STATES_IN_WHICH_TO_SHOW_CONFIG.includes(overallStatus) ? (devLog.debug("Showing translation config view", {
|
|
3673
|
-
documentId: document._id,
|
|
3674
|
-
overallStatus,
|
|
3675
|
-
component: "TranslationDialog"
|
|
3676
|
-
}), /* @__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", {
|
|
3677
3308
|
documentId: document._id,
|
|
3678
|
-
overallStatus
|
|
3679
|
-
component: "TranslationDialog"
|
|
3680
|
-
}), /* @__PURE__ */ jsx(TranslationStatusPanel, {})) : STATES_IN_WHICH_TO_SHOW_ERROR.includes(overallStatus) ? (devLog.warn("Translation error state reached", {
|
|
3681
|
-
documentId: document._id,
|
|
3682
|
-
overallStatus,
|
|
3683
|
-
component: "TranslationDialog"
|
|
3684
|
-
}), /* @__PURE__ */ jsx("div", { children: "Error occurred" })) : (devLog.warn("Unhandled translation dialog state", {
|
|
3685
|
-
documentId: document._id,
|
|
3686
|
-
overallStatus,
|
|
3687
|
-
component: "TranslationDialog"
|
|
3309
|
+
overallStatus
|
|
3688
3310
|
}), /* @__PURE__ */ jsx("div", { children: "Unhandled overall state" }));
|
|
3689
3311
|
}, getTranslateAction = (config) => ({ draft, published }) => {
|
|
3690
3312
|
const document = draft ?? published, [isDialogOpen, setIsDialogOpen] = useState(!1);
|
|
@@ -3711,7 +3333,427 @@ Then return the corrected translation document:`;
|
|
|
3711
3333
|
content: /* @__PURE__ */ jsx(TranslationContextProvider, { document, config, children: /* @__PURE__ */ jsx(TranslationDialog, { setIsDialogOpen }) })
|
|
3712
3334
|
}
|
|
3713
3335
|
} : (devLog.error("Missing config in TranslateAction"), null);
|
|
3714
|
-
},
|
|
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
|
+
})), {
|
|
3715
3757
|
name: "sanity-plugin-ai-translation",
|
|
3716
3758
|
document: {
|
|
3717
3759
|
actions: [getTranslateAction(config), getGiveTranslationFeedbackAction(config)]
|
|
@@ -3722,6 +3764,8 @@ Then return the corrected translation document:`;
|
|
|
3722
3764
|
})), translationHistorySchema = translationHistory;
|
|
3723
3765
|
export {
|
|
3724
3766
|
aiTranslationPlugin,
|
|
3725
|
-
|
|
3767
|
+
richTextTransform,
|
|
3768
|
+
translationHistorySchema,
|
|
3769
|
+
vttCaptionTransform
|
|
3726
3770
|
};
|
|
3727
3771
|
//# sourceMappingURL=index.js.map
|