@braze/sanity-plugin-ai-translation 0.2.22 → 0.2.23
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 +1 -2
- package/dist/index.js +307 -654
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useClient, defineType, defineField, pathToString, definePlugin } from "sanity";
|
|
1
|
+
import { useClient, defineType, defineField, pathToString, getPublishedId, 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
4
|
import { createContext, useContext, useEffect, useState, useReducer, useCallback, useMemo } from "react";
|
|
@@ -6,6 +6,7 @@ import { Card, Flex, Stack, Text, Spinner, Button, Box, Tooltip, Checkbox, Selec
|
|
|
6
6
|
import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
|
|
7
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";
|
|
@@ -31,7 +32,7 @@ const TranslationContext = createContext({
|
|
|
31
32
|
"useTranslationContext must be used within a TranslationContextProvider with document and config props"
|
|
32
33
|
);
|
|
33
34
|
return context;
|
|
34
|
-
}, TRANSLATION_METADATA_TYPE = "translation.metadata", undraftId = (id) => id.replace(/^drafts\./, ""),
|
|
35
|
+
}, TRANSLATION_METADATA_TYPE = "translation.metadata", undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
|
|
35
36
|
const { dispatch } = useTranslationContext(), { data, loading, error } = useListeningQuery(TRANSLATION_METADATA_QUERY, {
|
|
36
37
|
params: {
|
|
37
38
|
documentId: undraftId(documentId),
|
|
@@ -1227,8 +1228,8 @@ const ICON_SIZE = 64, progressIconMap = {
|
|
|
1227
1228
|
references: action.payload
|
|
1228
1229
|
};
|
|
1229
1230
|
case "SET_TRANSLATION_JOB_STATUS": {
|
|
1230
|
-
const jobKey = `${action.payload._id}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs);
|
|
1231
|
-
return newTranslationJobs.set(jobKey,
|
|
1231
|
+
const jobKey = `${action.payload._id}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs), existingJob = newTranslationJobs.get(jobKey), mergedJob = defaults(action.payload, existingJob);
|
|
1232
|
+
return newTranslationJobs.set(jobKey, mergedJob), {
|
|
1232
1233
|
...context,
|
|
1233
1234
|
translationJobs: newTranslationJobs
|
|
1234
1235
|
};
|
|
@@ -1833,7 +1834,9 @@ const fetchTranslationPrompts = async ({
|
|
|
1833
1834
|
payload
|
|
1834
1835
|
});
|
|
1835
1836
|
}
|
|
1836
|
-
},
|
|
1837
|
+
}, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
|
|
1838
|
+
(translation) => translation._key === targetLanguage
|
|
1839
|
+
)?.value._ref || null, getTranslationMetadata = async ({
|
|
1837
1840
|
client,
|
|
1838
1841
|
sourceDocument,
|
|
1839
1842
|
sourceLanguage
|
|
@@ -1882,7 +1885,7 @@ const fetchTranslationPrompts = async ({
|
|
|
1882
1885
|
_type: "internationalizedArrayReferenceValue",
|
|
1883
1886
|
value: {
|
|
1884
1887
|
_type: "reference",
|
|
1885
|
-
_ref:
|
|
1888
|
+
_ref: getPublishedId(newDocument._id),
|
|
1886
1889
|
_weak: !0,
|
|
1887
1890
|
_strengthenOnPublish: {
|
|
1888
1891
|
_type: "reference",
|
|
@@ -1892,7 +1895,7 @@ const fetchTranslationPrompts = async ({
|
|
|
1892
1895
|
}, newTranslationHistory = {
|
|
1893
1896
|
_key: targetLanguage,
|
|
1894
1897
|
language: targetLanguage,
|
|
1895
|
-
sourceId:
|
|
1898
|
+
sourceId: getPublishedId(sourceDocument._id),
|
|
1896
1899
|
sourceRev: sourceDocument._rev,
|
|
1897
1900
|
sourceLanguage,
|
|
1898
1901
|
sourceSnapshot: JSON.stringify(sourceDocument),
|
|
@@ -1922,70 +1925,51 @@ const fetchTranslationPrompts = async ({
|
|
|
1922
1925
|
status
|
|
1923
1926
|
})
|
|
1924
1927
|
});
|
|
1925
|
-
},
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
}) => {
|
|
1931
|
-
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage);
|
|
1932
|
-
return existingTranslationId ? {
|
|
1933
|
-
// Update existing translation
|
|
1934
|
-
operation: "createOrReplace",
|
|
1935
|
-
_id: existingTranslationId
|
|
1936
|
-
} : {
|
|
1937
|
-
// Create new translation
|
|
1938
|
-
operation: "create"
|
|
1939
|
-
};
|
|
1940
|
-
}, analyzeDocumentContent = (document) => {
|
|
1941
|
-
let hasCodeBlocks = !1, hasInlineCode = !1, codeBlockCount = 0, inlineCodeCount = 0;
|
|
1942
|
-
const hasMarkdown = !1;
|
|
1943
|
-
let hasPortableText = !1, estimatedFieldCount = 0;
|
|
1944
|
-
const traverse = (value, depth = 0) => {
|
|
1945
|
-
value != null && (depth > 0 && typeof value == "object" && estimatedFieldCount++, Array.isArray(value) ? value.forEach((item) => {
|
|
1946
|
-
item && typeof item == "object" && "_type" in item && item._type === "code" && (hasCodeBlocks = !0, codeBlockCount++, hasPortableText = !0), item && typeof item == "object" && "_type" in item && item._type === "block" && "children" in item && Array.isArray(item.children) && (hasPortableText = !0, item.children.forEach((child) => {
|
|
1947
|
-
child && typeof child == "object" && "marks" in child && Array.isArray(child.marks) && child.marks.includes("code") && (hasInlineCode = !0, inlineCodeCount++);
|
|
1948
|
-
})), traverse(item, depth + 1);
|
|
1949
|
-
}) : typeof value == "object" && Object.values(value).forEach((val) => traverse(val, depth + 1)));
|
|
1950
|
-
};
|
|
1951
|
-
traverse(document);
|
|
1952
|
-
let complexity = "simple";
|
|
1953
|
-
return estimatedFieldCount > 50 ? complexity = "complex" : estimatedFieldCount > 20 && (complexity = "moderate"), {
|
|
1954
|
-
hasCodeBlocks,
|
|
1955
|
-
hasInlineCode,
|
|
1956
|
-
codeBlockCount,
|
|
1957
|
-
inlineCodeCount,
|
|
1958
|
-
hasMarkdown,
|
|
1959
|
-
hasPortableText,
|
|
1960
|
-
complexity,
|
|
1961
|
-
estimatedFieldCount
|
|
1928
|
+
}, callGPT5 = async (apiKey, request) => (request.model || "gpt-5") === "gpt-5" ? await callGPT5ResponsesAPI(apiKey, request) : await callChatCompletionsAPI(apiKey, request), callGPT5ResponsesAPI = async (apiKey, request) => {
|
|
1929
|
+
const model = request.model || "gpt-5", requestBody = {
|
|
1930
|
+
model,
|
|
1931
|
+
input: request.messages
|
|
1932
|
+
// Uses 'input' instead of 'messages'
|
|
1962
1933
|
};
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
})
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
id: sourceLanugageId,
|
|
1974
|
-
title: sourceLanugageId
|
|
1934
|
+
request.reasoningEffort && (requestBody.reasoning = {
|
|
1935
|
+
effort: request.reasoningEffort,
|
|
1936
|
+
summary: "auto"
|
|
1937
|
+
// Request reasoning summaries
|
|
1938
|
+
});
|
|
1939
|
+
const response = await fetch("https://api.openai.com/v1/responses", {
|
|
1940
|
+
method: "POST",
|
|
1941
|
+
headers: {
|
|
1942
|
+
"Content-Type": "application/json",
|
|
1943
|
+
Authorization: `Bearer ${apiKey}`
|
|
1975
1944
|
},
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1945
|
+
body: JSON.stringify(requestBody)
|
|
1946
|
+
});
|
|
1947
|
+
if (!response.ok) {
|
|
1948
|
+
const error = await response.json();
|
|
1949
|
+
throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
|
|
1950
|
+
}
|
|
1951
|
+
const result = await response.json(), tokensUsed = {
|
|
1952
|
+
prompt: result.usage?.input_tokens || 0,
|
|
1953
|
+
completion: result.usage?.output_tokens || 0,
|
|
1954
|
+
reasoning: result.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
1955
|
+
total: result.usage?.total_tokens || 0
|
|
1956
|
+
}, cost = calculateCost(tokensUsed, model);
|
|
1957
|
+
let content = "";
|
|
1958
|
+
if (result.output && Array.isArray(result.output)) {
|
|
1959
|
+
const messageItem = result.output.find((item) => item.type === "message");
|
|
1960
|
+
messageItem?.content && Array.isArray(messageItem.content) && (content = messageItem.content.find((item) => item.type === "output_text")?.text || "");
|
|
1961
|
+
}
|
|
1962
|
+
return content || (content = result.output_text || result.text || ""), {
|
|
1963
|
+
content,
|
|
1964
|
+
tokensUsed,
|
|
1965
|
+
cost
|
|
1980
1966
|
};
|
|
1981
|
-
},
|
|
1982
|
-
const model = request.model || "gpt-
|
|
1967
|
+
}, callChatCompletionsAPI = async (apiKey, request) => {
|
|
1968
|
+
const model = request.model || "gpt-4o", requestBody = {
|
|
1983
1969
|
model,
|
|
1984
1970
|
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", {
|
|
1971
|
+
temperature: request.temperature ?? 0.3
|
|
1972
|
+
}, response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1989
1973
|
method: "POST",
|
|
1990
1974
|
headers: {
|
|
1991
1975
|
"Content-Type": "application/json",
|
|
@@ -1995,88 +1979,30 @@ const fetchTranslationPrompts = async ({
|
|
|
1995
1979
|
});
|
|
1996
1980
|
if (!response.ok) {
|
|
1997
1981
|
const error = await response.json();
|
|
1998
|
-
throw new Error(`
|
|
1982
|
+
throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
|
|
1999
1983
|
}
|
|
2000
|
-
if (useStreaming && response.body && request.onReasoningChunk)
|
|
2001
|
-
return await handleStreamingResponse(response, request.onReasoningChunk, model);
|
|
2002
1984
|
const result = await response.json(), usage = result.usage || {}, tokensUsed = {
|
|
2003
1985
|
prompt: usage.prompt_tokens || 0,
|
|
2004
1986
|
completion: usage.completion_tokens || 0,
|
|
2005
1987
|
reasoning: usage.reasoning_tokens || 0,
|
|
2006
|
-
// New in GPT-5
|
|
2007
1988
|
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);
|
|
1989
|
+
}, cost = calculateCost(tokensUsed, model);
|
|
2059
1990
|
return {
|
|
2060
|
-
content:
|
|
1991
|
+
content: result.choices[0].message.content || "",
|
|
2061
1992
|
tokensUsed,
|
|
2062
|
-
cost
|
|
2063
|
-
reasoningSummary: reasoningBuffer || void 0
|
|
1993
|
+
cost
|
|
2064
1994
|
};
|
|
2065
|
-
},
|
|
1995
|
+
}, calculateCost = (tokens, model) => {
|
|
2066
1996
|
const pricing = {
|
|
2067
1997
|
"gpt-5": {
|
|
2068
1998
|
input: 2e-6,
|
|
2069
|
-
// $0.002 per 1k input tokens
|
|
2070
1999
|
output: 1e-5,
|
|
2071
|
-
// $0.010 per 1k output tokens
|
|
2072
2000
|
reasoning: 5e-6
|
|
2073
|
-
// $0.005 per 1k reasoning tokens
|
|
2074
2001
|
},
|
|
2075
2002
|
"gpt-4o": {
|
|
2076
2003
|
input: 5e-6,
|
|
2077
2004
|
output: 14999999999999999e-21,
|
|
2078
2005
|
reasoning: 0
|
|
2079
|
-
// No reasoning tokens in GPT-4
|
|
2080
2006
|
},
|
|
2081
2007
|
"gpt-4-turbo": {
|
|
2082
2008
|
input: 1e-5,
|
|
@@ -2355,6 +2281,125 @@ const fetchTranslationPrompts = async ({
|
|
|
2355
2281
|
type: "metadata_changed"
|
|
2356
2282
|
}) : key !== "text" && validateStructureRecursive(origObj[key], modObj[key], [...path, key], errors));
|
|
2357
2283
|
}
|
|
2284
|
+
}, getOpenAITranslation = async ({
|
|
2285
|
+
dispatch,
|
|
2286
|
+
sourceDocument,
|
|
2287
|
+
languageFieldName,
|
|
2288
|
+
translationPrompts,
|
|
2289
|
+
targetLanguage,
|
|
2290
|
+
openaiConfig
|
|
2291
|
+
}) => {
|
|
2292
|
+
const sourceLanguage = sourceDocument[languageFieldName], logContext = {
|
|
2293
|
+
sourceId: sourceDocument._id,
|
|
2294
|
+
sourceLanguage
|
|
2295
|
+
};
|
|
2296
|
+
try {
|
|
2297
|
+
const sourceText = serializeDocument(sourceDocument), prompt = buildTranslationPrompt({
|
|
2298
|
+
sourceText,
|
|
2299
|
+
sourceLanguage,
|
|
2300
|
+
targetLanguage,
|
|
2301
|
+
translationPrompts
|
|
2302
|
+
}), response = await callGPT5(openaiConfig.apiKey, {
|
|
2303
|
+
messages: [
|
|
2304
|
+
{
|
|
2305
|
+
role: "developer",
|
|
2306
|
+
content: "You are a translation quality reviewer. Review and refine translations while preserving structure."
|
|
2307
|
+
},
|
|
2308
|
+
{ role: "user", content: prompt }
|
|
2309
|
+
],
|
|
2310
|
+
model: openaiConfig.model,
|
|
2311
|
+
reasoningEffort: openaiConfig.reasoningEffort
|
|
2312
|
+
}), result = deserializeDocument(sourceDocument, response.content), validation = validateStructureIntegrity(sourceDocument, result);
|
|
2313
|
+
if (!validation.valid) {
|
|
2314
|
+
devLog.error("Structure validation failed", {
|
|
2315
|
+
...logContext,
|
|
2316
|
+
errors: validation.errors
|
|
2317
|
+
});
|
|
2318
|
+
const unexpectedErrors = validation.errors.filter(
|
|
2319
|
+
(err) => !(err.path.length === 1 && err.path[0] === languageFieldName)
|
|
2320
|
+
);
|
|
2321
|
+
if (unexpectedErrors.length > 0)
|
|
2322
|
+
throw new Error(
|
|
2323
|
+
`Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
|
|
2327
|
+
tokensUsed: response.tokensUsed.total,
|
|
2328
|
+
estimatedCost: response.cost
|
|
2329
|
+
}), result;
|
|
2330
|
+
} catch (error) {
|
|
2331
|
+
throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
|
|
2332
|
+
}
|
|
2333
|
+
}, buildTranslationPrompt = ({
|
|
2334
|
+
sourceText,
|
|
2335
|
+
sourceLanguage,
|
|
2336
|
+
targetLanguage,
|
|
2337
|
+
translationPrompts
|
|
2338
|
+
}) => {
|
|
2339
|
+
const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
|
|
2340
|
+
return `# TRANSLATION TASK
|
|
2341
|
+
|
|
2342
|
+
Translate this ${sourceLanguage} document into ${targetLanguage}.
|
|
2343
|
+
|
|
2344
|
+
## Guidelines
|
|
2345
|
+
|
|
2346
|
+
${globalPrompt ? `### General Guidelines
|
|
2347
|
+
${globalPrompt}
|
|
2348
|
+
` : ""}
|
|
2349
|
+
${localePrompt ? `### ${targetLanguage} Guidelines
|
|
2350
|
+
${localePrompt}
|
|
2351
|
+
` : ""}
|
|
2352
|
+
${glossaryTerms ? `### Required Terminology
|
|
2353
|
+
${glossaryTerms}
|
|
2354
|
+
` : ""}
|
|
2355
|
+
${protectedPhrases?.length ? `### Protected Phrases (do not translate)
|
|
2356
|
+
${protectedPhrases.join(", ")}
|
|
2357
|
+
` : ""}
|
|
2358
|
+
|
|
2359
|
+
## Source Document (${sourceLanguage})
|
|
2360
|
+
|
|
2361
|
+
\`\`\`
|
|
2362
|
+
${sourceText}
|
|
2363
|
+
\`\`\`
|
|
2364
|
+
|
|
2365
|
+
## Instructions
|
|
2366
|
+
|
|
2367
|
+
1. Translate all text content into ${targetLanguage}
|
|
2368
|
+
2. Follow the guidelines and terminology requirements above
|
|
2369
|
+
3. Preserve protected phrases exactly as they appear
|
|
2370
|
+
4. Return the document in THE EXACT SAME FORMAT
|
|
2371
|
+
5. **CRITICAL**: Only modify text content between tags
|
|
2372
|
+
6. Never add, remove, or change any tags, attributes, or structure
|
|
2373
|
+
|
|
2374
|
+
If you used significant reasoning to make translation decisions, you may include a brief summary:
|
|
2375
|
+
<reasoning>
|
|
2376
|
+
Brief summary of key translation decisions and reasoning
|
|
2377
|
+
</reasoning>
|
|
2378
|
+
|
|
2379
|
+
Then return the translated document:`;
|
|
2380
|
+
}, analyzeDocumentContent = (document) => {
|
|
2381
|
+
let hasCodeBlocks = !1, hasInlineCode = !1, codeBlockCount = 0, inlineCodeCount = 0;
|
|
2382
|
+
const hasMarkdown = !1;
|
|
2383
|
+
let hasPortableText = !1, estimatedFieldCount = 0;
|
|
2384
|
+
const traverse = (value, depth = 0) => {
|
|
2385
|
+
value != null && (depth > 0 && typeof value == "object" && estimatedFieldCount++, Array.isArray(value) ? value.forEach((item) => {
|
|
2386
|
+
item && typeof item == "object" && "_type" in item && item._type === "code" && (hasCodeBlocks = !0, codeBlockCount++, hasPortableText = !0), item && typeof item == "object" && "_type" in item && item._type === "block" && "children" in item && Array.isArray(item.children) && (hasPortableText = !0, item.children.forEach((child) => {
|
|
2387
|
+
child && typeof child == "object" && "marks" in child && Array.isArray(child.marks) && child.marks.includes("code") && (hasInlineCode = !0, inlineCodeCount++);
|
|
2388
|
+
})), traverse(item, depth + 1);
|
|
2389
|
+
}) : typeof value == "object" && Object.values(value).forEach((val) => traverse(val, depth + 1)));
|
|
2390
|
+
};
|
|
2391
|
+
traverse(document);
|
|
2392
|
+
let complexity = "simple";
|
|
2393
|
+
return estimatedFieldCount > 50 ? complexity = "complex" : estimatedFieldCount > 20 && (complexity = "moderate"), {
|
|
2394
|
+
hasCodeBlocks,
|
|
2395
|
+
hasInlineCode,
|
|
2396
|
+
codeBlockCount,
|
|
2397
|
+
inlineCodeCount,
|
|
2398
|
+
hasMarkdown,
|
|
2399
|
+
hasPortableText,
|
|
2400
|
+
complexity,
|
|
2401
|
+
estimatedFieldCount
|
|
2402
|
+
};
|
|
2358
2403
|
}, assembleTranslationPrompt = (context, components) => {
|
|
2359
2404
|
const sections = [
|
|
2360
2405
|
{
|
|
@@ -2457,469 +2502,126 @@ ${glossaryTerms}`, buildStyleGuideSection = (globalPrompt, localePrompt) => {
|
|
|
2457
2502
|
"Apply these directions only after honoring critical rules, protected phrases, and glossary terminology."
|
|
2458
2503
|
), components.push("")), globalPrompt && (components.push("## General Guidelines"), components.push(globalPrompt), components.push("")), localePrompt && (components.push("## Language-Specific Guidelines"), components.push(localePrompt)), components.length > 0 ? components.join(`
|
|
2459
2504
|
`) : null;
|
|
2460
|
-
},
|
|
2461
|
-
dispatch,
|
|
2462
|
-
client,
|
|
2463
|
-
schemaId,
|
|
2505
|
+
}, assembleLanguageSpecs = ({
|
|
2464
2506
|
sourceDocument,
|
|
2465
|
-
targetLanguage,
|
|
2466
2507
|
languageFieldName,
|
|
2467
|
-
|
|
2468
|
-
translationPrompts,
|
|
2469
|
-
translationMode,
|
|
2470
|
-
openaiConfig,
|
|
2471
|
-
releaseId,
|
|
2472
|
-
isNewTranslation
|
|
2508
|
+
targetLanguage
|
|
2473
2509
|
}) => {
|
|
2474
|
-
const
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
try {
|
|
2486
|
-
let versionedDocumentId, baseDocumentId;
|
|
2487
|
-
if (isNewTranslation) {
|
|
2488
|
-
const { _id, _createdAt, _updatedAt, _rev, ...docWithoutMeta } = sourceDocument, newTranslationDoc = {
|
|
2489
|
-
...docWithoutMeta,
|
|
2490
|
-
_type: sourceDocument._type,
|
|
2491
|
-
[languageFieldName]: targetLanguage
|
|
2492
|
-
};
|
|
2493
|
-
devLog.info("Creating new translation document in release", logContext), baseDocumentId = translationTarget._id || `new-translation-${Date.now()}`, await client.action({
|
|
2494
|
-
actionType: "sanity.action.document.version.create",
|
|
2495
|
-
publishedId: baseDocumentId,
|
|
2496
|
-
document: {
|
|
2497
|
-
...newTranslationDoc,
|
|
2498
|
-
_type: sourceDocument._type,
|
|
2499
|
-
_id: `versions.${releaseId}.${baseDocumentId}`
|
|
2500
|
-
}
|
|
2501
|
-
}), versionedDocumentId = `versions.${releaseId}.${baseDocumentId}`, devLog.info("New translation version created in release", {
|
|
2502
|
-
...logContext,
|
|
2503
|
-
versionedDocumentId,
|
|
2504
|
-
baseDocumentId
|
|
2505
|
-
});
|
|
2506
|
-
} else {
|
|
2507
|
-
const existingTranslationId = translationTarget._id;
|
|
2508
|
-
if (!existingTranslationId)
|
|
2509
|
-
throw new Error("Translation target ID not found for update operation");
|
|
2510
|
-
devLog.info("Creating version of existing translation in release", {
|
|
2511
|
-
...logContext,
|
|
2512
|
-
existingTranslationId
|
|
2513
|
-
});
|
|
2514
|
-
let existingDoc = await client.getDocument(existingTranslationId), actualDocumentId = existingTranslationId;
|
|
2515
|
-
if (!existingDoc) {
|
|
2516
|
-
const alternateId = existingTranslationId.startsWith("drafts.") ? undraftId(existingTranslationId) : draftId(existingTranslationId);
|
|
2517
|
-
devLog.info("Document not found with stored ID, trying alternate ID", {
|
|
2518
|
-
...logContext,
|
|
2519
|
-
storedId: existingTranslationId,
|
|
2520
|
-
alternateId
|
|
2521
|
-
}), existingDoc = await client.getDocument(alternateId), existingDoc && (actualDocumentId = alternateId, devLog.info("Found document with alternate ID", {
|
|
2522
|
-
...logContext,
|
|
2523
|
-
actualDocumentId
|
|
2524
|
-
}));
|
|
2525
|
-
}
|
|
2526
|
-
if (!existingDoc)
|
|
2527
|
-
throw new Error(
|
|
2528
|
-
`Existing translation document not found: ${existingTranslationId} (also tried ${existingTranslationId.startsWith("drafts.") ? undraftId(existingTranslationId) : draftId(existingTranslationId)})`
|
|
2529
|
-
);
|
|
2530
|
-
const publishedId = undraftId(actualDocumentId);
|
|
2531
|
-
await client.action({
|
|
2532
|
-
actionType: "sanity.action.document.version.create",
|
|
2533
|
-
publishedId,
|
|
2534
|
-
document: {
|
|
2535
|
-
...existingDoc,
|
|
2536
|
-
_id: `versions.${releaseId}.${publishedId}`
|
|
2537
|
-
}
|
|
2538
|
-
}), versionedDocumentId = `versions.${releaseId}.${publishedId}`, baseDocumentId = publishedId, devLog.info("Existing translation version created", {
|
|
2539
|
-
...logContext,
|
|
2540
|
-
versionedDocumentId,
|
|
2541
|
-
baseDocumentId
|
|
2542
|
-
});
|
|
2543
|
-
const { _id, _createdAt, _updatedAt, _rev, ...sourceContent } = sourceDocument;
|
|
2544
|
-
await client.createOrReplace({
|
|
2545
|
-
...sourceContent,
|
|
2546
|
-
_id: versionedDocumentId,
|
|
2547
|
-
[languageFieldName]: targetLanguage
|
|
2548
|
-
}), devLog.info("Source content cloned onto version", {
|
|
2549
|
-
...logContext,
|
|
2550
|
-
versionedDocumentId
|
|
2551
|
-
});
|
|
2552
|
-
}
|
|
2553
|
-
let translatedDocument;
|
|
2554
|
-
switch (translationMode) {
|
|
2555
|
-
case "openai-only": {
|
|
2556
|
-
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2557
|
-
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2558
|
-
devLog.info("Using OpenAI-only translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai");
|
|
2559
|
-
const versionedDoc = await client.getDocument(versionedDocumentId);
|
|
2560
|
-
if (!versionedDoc)
|
|
2561
|
-
throw new Error(`Versioned document not found: ${versionedDocumentId}`);
|
|
2562
|
-
const translatedDoc = await translateInPlaceWithOpenAI({
|
|
2563
|
-
dispatch,
|
|
2564
|
-
sourceDocument,
|
|
2565
|
-
targetDocument: versionedDoc,
|
|
2566
|
-
targetLanguage,
|
|
2567
|
-
languageFieldName,
|
|
2568
|
-
translationPrompts,
|
|
2569
|
-
openaiConfig,
|
|
2570
|
-
isReview: !1
|
|
2571
|
-
});
|
|
2572
|
-
translatedDoc && (translatedDocument = await client.createOrReplace({
|
|
2573
|
-
...translatedDoc,
|
|
2574
|
-
_id: versionedDocumentId
|
|
2575
|
-
}));
|
|
2576
|
-
break;
|
|
2577
|
-
}
|
|
2578
|
-
case "sanity-with-review":
|
|
2579
|
-
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2580
|
-
throw new Error("OpenAI is not configured but sanity-with-review mode was selected");
|
|
2581
|
-
if (devLog.info("Using Sanity + OpenAI review", logContext), translatedDocument = await translateWithSanityAgent({
|
|
2582
|
-
dispatch,
|
|
2583
|
-
client,
|
|
2584
|
-
schemaId,
|
|
2585
|
-
sourceDocument,
|
|
2586
|
-
targetLanguage,
|
|
2587
|
-
languageFieldName,
|
|
2588
|
-
versionedDocumentId,
|
|
2589
|
-
translationPrompts
|
|
2590
|
-
}), translatedDocument) {
|
|
2591
|
-
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "reviewing_with_openai");
|
|
2592
|
-
const reviewedDoc = await translateInPlaceWithOpenAI({
|
|
2593
|
-
dispatch,
|
|
2594
|
-
sourceDocument,
|
|
2595
|
-
targetDocument: translatedDocument,
|
|
2596
|
-
targetLanguage,
|
|
2597
|
-
languageFieldName,
|
|
2598
|
-
translationPrompts,
|
|
2599
|
-
openaiConfig,
|
|
2600
|
-
isReview: !0
|
|
2601
|
-
});
|
|
2602
|
-
reviewedDoc && (translatedDocument = await client.createOrReplace({
|
|
2603
|
-
...reviewedDoc,
|
|
2604
|
-
_id: versionedDocumentId
|
|
2605
|
-
}));
|
|
2606
|
-
}
|
|
2607
|
-
break;
|
|
2608
|
-
default:
|
|
2609
|
-
devLog.info("Using Sanity-only translation", logContext), translatedDocument = await translateWithSanityAgent({
|
|
2610
|
-
dispatch,
|
|
2611
|
-
client,
|
|
2612
|
-
schemaId,
|
|
2613
|
-
sourceDocument,
|
|
2614
|
-
targetLanguage,
|
|
2615
|
-
languageFieldName,
|
|
2616
|
-
versionedDocumentId,
|
|
2617
|
-
translationPrompts
|
|
2618
|
-
});
|
|
2619
|
-
break;
|
|
2510
|
+
const sourceLanugageId = sourceDocument[languageFieldName];
|
|
2511
|
+
if (!sourceLanugageId)
|
|
2512
|
+
throw new Error("[assembleLanguages] Source language ID is missing");
|
|
2513
|
+
return {
|
|
2514
|
+
fromLanguage: {
|
|
2515
|
+
id: sourceLanugageId,
|
|
2516
|
+
title: sourceLanugageId
|
|
2517
|
+
},
|
|
2518
|
+
toLanguage: {
|
|
2519
|
+
id: targetLanguage,
|
|
2520
|
+
title: targetLanguage
|
|
2620
2521
|
}
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
_id: baseDocumentId
|
|
2624
|
-
} : void 0;
|
|
2625
|
-
} catch (error) {
|
|
2626
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2627
|
-
devLog.error(
|
|
2628
|
-
"translateEntireDocument failed",
|
|
2629
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
2630
|
-
logContext
|
|
2631
|
-
), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
2632
|
-
errorMessage
|
|
2633
|
-
}), devLog.timeEnd("translateEntireDocument"), devLog.groupEnd();
|
|
2634
|
-
return;
|
|
2635
|
-
}
|
|
2636
|
-
}, translateWithSanityAgent = async ({
|
|
2637
|
-
dispatch,
|
|
2638
|
-
client,
|
|
2639
|
-
schemaId,
|
|
2522
|
+
};
|
|
2523
|
+
}, getSanityAgentTranslation = async ({
|
|
2640
2524
|
sourceDocument,
|
|
2641
2525
|
targetLanguage,
|
|
2642
2526
|
languageFieldName,
|
|
2643
|
-
|
|
2527
|
+
schemaId,
|
|
2528
|
+
client,
|
|
2644
2529
|
translationPrompts
|
|
2645
2530
|
}) => {
|
|
2646
|
-
|
|
2531
|
+
analyzeDocumentContent(sourceDocument);
|
|
2532
|
+
const { styleGuide } = assembleTranslationPrompt(
|
|
2533
|
+
{
|
|
2534
|
+
// Always fresh translation in a version
|
|
2535
|
+
targetLanguage,
|
|
2536
|
+
sourceLanguage: sourceDocument[languageFieldName]
|
|
2537
|
+
},
|
|
2538
|
+
{
|
|
2539
|
+
globalPrompt: translationPrompts.globalPrompt,
|
|
2540
|
+
localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
|
|
2541
|
+
glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms
|
|
2542
|
+
}
|
|
2543
|
+
), { fromLanguage, toLanguage } = assembleLanguageSpecs({
|
|
2544
|
+
sourceDocument,
|
|
2545
|
+
languageFieldName,
|
|
2546
|
+
targetLanguage
|
|
2547
|
+
});
|
|
2548
|
+
return await client.agent.action.translate({
|
|
2549
|
+
schemaId,
|
|
2550
|
+
toLanguage,
|
|
2551
|
+
fromLanguage,
|
|
2647
2552
|
documentId: sourceDocument._id,
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
toLanguage,
|
|
2660
|
-
fromLanguage
|
|
2661
|
-
});
|
|
2662
|
-
const documentAnalysis = analyzeDocumentContent(sourceDocument);
|
|
2663
|
-
devLog.debug("Document analysis completed", {
|
|
2664
|
-
...logContext,
|
|
2665
|
-
analysis: documentAnalysis
|
|
2666
|
-
}), devLog.info("Starting Sanity agent translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating");
|
|
2667
|
-
const { styleGuide, omittedSections, truncatedSections } = assembleTranslationPrompt(
|
|
2668
|
-
{
|
|
2669
|
-
documentAnalysis,
|
|
2670
|
-
isUpdate: !1,
|
|
2671
|
-
// Always fresh translation in a version
|
|
2672
|
-
targetLanguage,
|
|
2673
|
-
sourceLanguage: sourceDocument[languageFieldName]
|
|
2674
|
-
},
|
|
2675
|
-
{
|
|
2676
|
-
globalPrompt: translationPrompts.globalPrompt,
|
|
2677
|
-
localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
|
|
2678
|
-
glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
|
|
2679
|
-
protectedPhrases: translationPrompts.protectedPhrases
|
|
2680
|
-
}
|
|
2681
|
-
);
|
|
2682
|
-
devLog.info("Structured style guide generated", {
|
|
2683
|
-
...logContext,
|
|
2684
|
-
styleGuideLength: styleGuide.length,
|
|
2685
|
-
omittedSections,
|
|
2686
|
-
truncatedSections
|
|
2687
|
-
}), (omittedSections.length > 0 || truncatedSections.length > 0) && devLog.warn("Style guide adjusted to fit character limit", {
|
|
2688
|
-
...logContext,
|
|
2689
|
-
omittedSections,
|
|
2690
|
-
truncatedSections
|
|
2691
|
-
});
|
|
2692
|
-
const newDocument = await client.agent.action.translate({
|
|
2693
|
-
schemaId,
|
|
2694
|
-
toLanguage,
|
|
2695
|
-
fromLanguage,
|
|
2696
|
-
documentId: versionedDocumentId,
|
|
2697
|
-
// Use versioned document ID
|
|
2698
|
-
languageFieldPath: [languageFieldName],
|
|
2699
|
-
styleGuide,
|
|
2700
|
-
protectedPhrases: translationPrompts.protectedPhrases || [],
|
|
2701
|
-
conditionalPaths: {
|
|
2702
|
-
defaultHidden: !1,
|
|
2703
|
-
defaultReadOnly: !1
|
|
2704
|
-
}
|
|
2705
|
-
});
|
|
2706
|
-
return devLog.info("Sanity agent translation completed", {
|
|
2707
|
-
...logContext,
|
|
2708
|
-
newDocumentId: newDocument._id,
|
|
2709
|
-
newDocumentRev: newDocument._rev
|
|
2710
|
-
}), newDocument;
|
|
2711
|
-
} catch (error) {
|
|
2712
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2713
|
-
devLog.error(
|
|
2714
|
-
"Sanity agent translation failed",
|
|
2715
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
2716
|
-
logContext
|
|
2717
|
-
), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
2718
|
-
errorMessage
|
|
2719
|
-
});
|
|
2720
|
-
return;
|
|
2721
|
-
}
|
|
2722
|
-
}, translateInPlaceWithOpenAI = async ({
|
|
2553
|
+
noWrite: !0,
|
|
2554
|
+
// Do not write to the document, just return the translated content
|
|
2555
|
+
languageFieldPath: [languageFieldName],
|
|
2556
|
+
styleGuide,
|
|
2557
|
+
protectedPhrases: translationPrompts.protectedPhrases,
|
|
2558
|
+
conditionalPaths: {
|
|
2559
|
+
defaultHidden: !1,
|
|
2560
|
+
defaultReadOnly: !1
|
|
2561
|
+
}
|
|
2562
|
+
});
|
|
2563
|
+
}, handleTranslation = async ({
|
|
2723
2564
|
dispatch,
|
|
2565
|
+
client,
|
|
2566
|
+
schemaId,
|
|
2724
2567
|
sourceDocument,
|
|
2725
|
-
targetDocument,
|
|
2726
2568
|
targetLanguage,
|
|
2569
|
+
existingTranslationId,
|
|
2727
2570
|
languageFieldName,
|
|
2728
2571
|
translationPrompts,
|
|
2572
|
+
translationMode,
|
|
2729
2573
|
openaiConfig,
|
|
2730
|
-
|
|
2574
|
+
releaseId
|
|
2731
2575
|
}) => {
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
...logContext,
|
|
2743
|
-
sourceLength: sourceText.length,
|
|
2744
|
-
targetLength: targetText.length
|
|
2745
|
-
});
|
|
2746
|
-
const prompt = isReview ? buildReviewPrompt({
|
|
2747
|
-
sourceText,
|
|
2748
|
-
translatedText: targetText,
|
|
2749
|
-
sourceLanguage,
|
|
2750
|
-
targetLanguage,
|
|
2751
|
-
translationPrompts
|
|
2752
|
-
}) : buildTranslationPrompt({
|
|
2753
|
-
sourceText,
|
|
2754
|
-
sourceLanguage,
|
|
2755
|
-
targetLanguage,
|
|
2756
|
-
translationPrompts
|
|
2757
|
-
});
|
|
2758
|
-
devLog.debug("Calling GPT-5", {
|
|
2759
|
-
...logContext,
|
|
2760
|
-
promptLength: prompt.length
|
|
2761
|
-
});
|
|
2762
|
-
const response = await callGPT5(openaiConfig.apiKey, {
|
|
2763
|
-
messages: [
|
|
2764
|
-
{
|
|
2765
|
-
role: "system",
|
|
2766
|
-
content: isReview ? "You are a translation quality reviewer. Review and refine translations while preserving structure." : "You are a professional translator. Translate documents while preserving their exact structure."
|
|
2767
|
-
},
|
|
2768
|
-
{ role: "user", content: prompt }
|
|
2769
|
-
],
|
|
2770
|
-
model: openaiConfig.model,
|
|
2771
|
-
reasoningEffort: openaiConfig.reasoningEffort,
|
|
2772
|
-
verbosity: openaiConfig.verbosity,
|
|
2773
|
-
onReasoningChunk: (reasoning) => {
|
|
2774
|
-
const status2 = isReview ? "reviewing_with_openai" : "translating_with_openai";
|
|
2775
|
-
setJobStatus(dispatch, sourceDocument._id, targetLanguage, status2, {
|
|
2776
|
-
reasoningProgress: reasoning
|
|
2777
|
-
});
|
|
2778
|
-
}
|
|
2779
|
-
});
|
|
2780
|
-
devLog.info("OpenAI operation completed", {
|
|
2781
|
-
...logContext,
|
|
2782
|
-
tokensUsed: response.tokensUsed.total,
|
|
2783
|
-
cost: response.cost
|
|
2784
|
-
});
|
|
2785
|
-
const result = deserializeDocument(targetDocument, response.content), validation = validateStructureIntegrity(targetDocument, result);
|
|
2786
|
-
if (!validation.valid) {
|
|
2787
|
-
devLog.error("Structure validation failed", {
|
|
2788
|
-
...logContext,
|
|
2789
|
-
errors: validation.errors
|
|
2576
|
+
let translatedDoc;
|
|
2577
|
+
switch (translationMode) {
|
|
2578
|
+
case "sanity-only": {
|
|
2579
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating"), translatedDoc = await getSanityAgentTranslation({
|
|
2580
|
+
sourceDocument,
|
|
2581
|
+
targetLanguage,
|
|
2582
|
+
languageFieldName,
|
|
2583
|
+
schemaId,
|
|
2584
|
+
client,
|
|
2585
|
+
translationPrompts
|
|
2790
2586
|
});
|
|
2791
|
-
|
|
2792
|
-
(err) => !(err.path.length === 1 && err.path[0] === languageFieldName)
|
|
2793
|
-
);
|
|
2794
|
-
if (unexpectedErrors.length > 0)
|
|
2795
|
-
throw new Error(
|
|
2796
|
-
`Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
|
|
2797
|
-
);
|
|
2587
|
+
break;
|
|
2798
2588
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2589
|
+
case "openai-only": {
|
|
2590
|
+
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2591
|
+
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2592
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), translatedDoc = await getOpenAITranslation({
|
|
2593
|
+
dispatch,
|
|
2594
|
+
sourceDocument,
|
|
2595
|
+
targetLanguage,
|
|
2596
|
+
languageFieldName,
|
|
2597
|
+
translationPrompts,
|
|
2598
|
+
openaiConfig
|
|
2599
|
+
});
|
|
2600
|
+
break;
|
|
2601
|
+
}
|
|
2602
|
+
case "sanity-with-review":
|
|
2603
|
+
throw new Error("Sanity with review translation method is not implemented");
|
|
2604
|
+
default:
|
|
2605
|
+
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2808
2606
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
}
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
${globalPrompt ? `### General Guidelines
|
|
2823
|
-
${globalPrompt}
|
|
2824
|
-
` : ""}
|
|
2825
|
-
${localePrompt ? `### ${targetLanguage} Guidelines
|
|
2826
|
-
${localePrompt}
|
|
2827
|
-
` : ""}
|
|
2828
|
-
${glossaryTerms ? `### Required Terminology
|
|
2829
|
-
${glossaryTerms}
|
|
2830
|
-
` : ""}
|
|
2831
|
-
${protectedPhrases?.length ? `### Protected Phrases (do not translate)
|
|
2832
|
-
${protectedPhrases.join(", ")}
|
|
2833
|
-
` : ""}
|
|
2834
|
-
|
|
2835
|
-
## Source Document (${sourceLanguage})
|
|
2836
|
-
|
|
2837
|
-
\`\`\`
|
|
2838
|
-
${sourceText}
|
|
2839
|
-
\`\`\`
|
|
2840
|
-
|
|
2841
|
-
## Instructions
|
|
2842
|
-
|
|
2843
|
-
1. Translate all text content into ${targetLanguage}
|
|
2844
|
-
2. Follow the guidelines and terminology requirements above
|
|
2845
|
-
3. Preserve protected phrases exactly as they appear
|
|
2846
|
-
4. Return the document in THE EXACT SAME FORMAT
|
|
2847
|
-
5. **CRITICAL**: Only modify text content between tags
|
|
2848
|
-
6. Never add, remove, or change any tags, attributes, or structure
|
|
2849
|
-
|
|
2850
|
-
If you used significant reasoning to make translation decisions, you may include a brief summary:
|
|
2851
|
-
<reasoning>
|
|
2852
|
-
Brief summary of key translation decisions and reasoning
|
|
2853
|
-
</reasoning>
|
|
2854
|
-
|
|
2855
|
-
Then return the translated document:`;
|
|
2856
|
-
}, buildReviewPrompt = ({
|
|
2857
|
-
sourceText,
|
|
2858
|
-
translatedText,
|
|
2859
|
-
sourceLanguage,
|
|
2860
|
-
targetLanguage,
|
|
2861
|
-
translationPrompts
|
|
2862
|
-
}) => {
|
|
2863
|
-
const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
|
|
2864
|
-
return `# TRANSLATION QUALITY REVIEW
|
|
2865
|
-
|
|
2866
|
-
Review this ${targetLanguage} translation and return a corrected version.
|
|
2867
|
-
|
|
2868
|
-
## Review Criteria
|
|
2869
|
-
|
|
2870
|
-
1. **Accuracy**: Does it accurately convey the ${sourceLanguage} source meaning?
|
|
2871
|
-
2. **Terminology**: Are glossary terms used correctly?
|
|
2872
|
-
3. **Protected Phrases**: Are protected phrases preserved?
|
|
2873
|
-
4. **Fluency**: Is it natural and fluent in ${targetLanguage}?
|
|
2874
|
-
5. **Style**: Does it follow the style guide?
|
|
2875
|
-
6. **Completeness**: Is all content translated?
|
|
2876
|
-
|
|
2877
|
-
## Guidelines
|
|
2878
|
-
|
|
2879
|
-
${globalPrompt ? `### General Guidelines
|
|
2880
|
-
${globalPrompt}
|
|
2881
|
-
` : ""}
|
|
2882
|
-
${localePrompt ? `### ${targetLanguage} Guidelines
|
|
2883
|
-
${localePrompt}
|
|
2884
|
-
` : ""}
|
|
2885
|
-
${glossaryTerms ? `### Required Terminology
|
|
2886
|
-
${glossaryTerms}
|
|
2887
|
-
` : ""}
|
|
2888
|
-
${protectedPhrases?.length ? `### Protected Phrases (must preserve)
|
|
2889
|
-
${protectedPhrases.join(", ")}
|
|
2890
|
-
` : ""}
|
|
2891
|
-
|
|
2892
|
-
## Source Document (${sourceLanguage})
|
|
2893
|
-
|
|
2894
|
-
\`\`\`
|
|
2895
|
-
${sourceText}
|
|
2896
|
-
\`\`\`
|
|
2897
|
-
|
|
2898
|
-
## Current Translation (${targetLanguage})
|
|
2899
|
-
|
|
2900
|
-
\`\`\`
|
|
2901
|
-
${translatedText}
|
|
2902
|
-
\`\`\`
|
|
2903
|
-
|
|
2904
|
-
## Instructions
|
|
2905
|
-
|
|
2906
|
-
1. Review the translation for quality issues
|
|
2907
|
-
2. Make corrections where needed to improve accuracy, fluency, or adherence to guidelines
|
|
2908
|
-
3. Keep good translations as-is - only fix what needs fixing
|
|
2909
|
-
4. Return the corrected translation in THE EXACT SAME FORMAT
|
|
2910
|
-
5. **CRITICAL**: Only modify text content between tags
|
|
2911
|
-
6. Never add, remove, or change any tags, attributes, or structure
|
|
2912
|
-
|
|
2913
|
-
If you made corrections or have insights about the translation quality, include a brief summary:
|
|
2914
|
-
<reasoning>
|
|
2915
|
-
Brief summary of review findings and corrections made
|
|
2916
|
-
</reasoning>
|
|
2917
|
-
|
|
2918
|
-
Then return the corrected translation document:`;
|
|
2607
|
+
if (!translatedDoc)
|
|
2608
|
+
return null;
|
|
2609
|
+
const targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
|
|
2610
|
+
...translatedDoc,
|
|
2611
|
+
_id: targetDocumentVersionId
|
|
2612
|
+
};
|
|
2613
|
+
console.log("NEW DOC", newDoc);
|
|
2614
|
+
const createdDoc = await client.action({
|
|
2615
|
+
actionType: "sanity.action.document.version.create",
|
|
2616
|
+
publishedId: targetDocumentPublishedId,
|
|
2617
|
+
document: newDoc
|
|
2618
|
+
});
|
|
2619
|
+
return console.log("CREATED DOC", createdDoc), newDoc;
|
|
2919
2620
|
}, translateDocument = async ({
|
|
2920
2621
|
dispatch,
|
|
2921
2622
|
client,
|
|
2922
2623
|
sourceDocument,
|
|
2624
|
+
sourceLanguage,
|
|
2923
2625
|
targetLanguage,
|
|
2924
2626
|
languageFieldName,
|
|
2925
2627
|
shouldOverwriteFreshTranslations,
|
|
@@ -2938,62 +2640,37 @@ Then return the corrected translation document:`;
|
|
|
2938
2640
|
};
|
|
2939
2641
|
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
|
|
2940
2642
|
try {
|
|
2941
|
-
const sourceLanguage = sourceDocument[languageFieldName];
|
|
2942
|
-
if (devLog.debug("Checking source document language", {
|
|
2943
|
-
...logContext,
|
|
2944
|
-
sourceLanguage,
|
|
2945
|
-
languageFieldName
|
|
2946
|
-
}), !sourceLanguage) {
|
|
2947
|
-
const error = `Source document ${sourceDocument._id} does not have a language`;
|
|
2948
|
-
throw devLog.error(error, void 0, logContext), new Error(error);
|
|
2949
|
-
}
|
|
2950
|
-
devLog.debug("Getting translation metadata", logContext);
|
|
2951
2643
|
const translationMetadata = await getTranslationMetadata({
|
|
2952
2644
|
client,
|
|
2953
2645
|
sourceDocument,
|
|
2954
2646
|
sourceLanguage
|
|
2955
2647
|
}), isAlreadyFreshTranslation = translationMetadata.translationHistory?.some(
|
|
2956
2648
|
(history) => history.language === targetLanguage && history.sourceRev === sourceDocument._rev
|
|
2957
|
-
)
|
|
2958
|
-
if (
|
|
2959
|
-
|
|
2960
|
-
isAlreadyFreshTranslation,
|
|
2961
|
-
currentRev: sourceDocument._rev
|
|
2962
|
-
}), !shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
|
|
2963
|
-
devLog.info("Skipping fresh translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
|
|
2649
|
+
);
|
|
2650
|
+
if (!shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
|
|
2651
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
|
|
2964
2652
|
targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
|
|
2965
|
-
})
|
|
2653
|
+
});
|
|
2966
2654
|
return;
|
|
2967
2655
|
}
|
|
2968
|
-
const
|
|
2969
|
-
translationMetadata,
|
|
2970
|
-
targetLanguage
|
|
2971
|
-
}), isNewTranslation = translationTarget.operation === "create";
|
|
2972
|
-
devLog.debug("Translation target determined", {
|
|
2973
|
-
...logContext,
|
|
2974
|
-
operation: translationTarget.operation,
|
|
2975
|
-
isNewTranslation
|
|
2976
|
-
});
|
|
2977
|
-
const newDocument = await translateEntireDocument({
|
|
2656
|
+
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
|
|
2978
2657
|
dispatch,
|
|
2979
2658
|
client,
|
|
2980
2659
|
schemaId,
|
|
2981
2660
|
sourceDocument,
|
|
2982
2661
|
targetLanguage,
|
|
2662
|
+
existingTranslationId,
|
|
2983
2663
|
languageFieldName,
|
|
2984
|
-
translationTarget,
|
|
2985
2664
|
translationPrompts,
|
|
2986
2665
|
translationMode,
|
|
2987
2666
|
openaiConfig,
|
|
2988
|
-
releaseId
|
|
2989
|
-
isNewTranslation
|
|
2667
|
+
releaseId
|
|
2990
2668
|
});
|
|
2991
|
-
if (!newDocument)
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
}), devLog.debug("Updating translation metadata", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
|
|
2669
|
+
if (!newDocument) {
|
|
2670
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), devLog.timeEnd("translateDocument"), devLog.groupEnd();
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
return setJobStatus(dispatch, sourceDocument._id, targetLanguage, "updating_metadata", {
|
|
2997
2674
|
targetId: newDocument._id
|
|
2998
2675
|
}), await updateTranslationMetadata({
|
|
2999
2676
|
client,
|
|
@@ -3002,8 +2679,10 @@ Then return the corrected translation document:`;
|
|
|
3002
2679
|
newDocument,
|
|
3003
2680
|
targetLanguage,
|
|
3004
2681
|
translationMetadata,
|
|
3005
|
-
isNewTranslation
|
|
3006
|
-
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed"
|
|
2682
|
+
isNewTranslation: !existingTranslationId
|
|
2683
|
+
}), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "completed", {
|
|
2684
|
+
targetId: newDocument._id
|
|
2685
|
+
}), devLog.info("Translation completed successfully", {
|
|
3007
2686
|
...logContext,
|
|
3008
2687
|
newDocumentId: newDocument._id
|
|
3009
2688
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
@@ -3029,13 +2708,13 @@ Then return the corrected translation document:`;
|
|
|
3029
2708
|
}) => {
|
|
3030
2709
|
const logContext = {
|
|
3031
2710
|
documentId: sourceDocument._id,
|
|
2711
|
+
documentRev: sourceDocument._rev,
|
|
3032
2712
|
targetLanguages,
|
|
3033
2713
|
referencesCount: references.length,
|
|
3034
2714
|
maxConcurrentJobs: maxConcurrentTranslationJobs,
|
|
3035
2715
|
translationMode,
|
|
3036
2716
|
shouldOverwriteFreshTranslations
|
|
3037
2717
|
};
|
|
3038
|
-
devLog.group("translateDocumentSet", logContext), devLog.time("translateDocumentSet");
|
|
3039
2718
|
let releaseId;
|
|
3040
2719
|
try {
|
|
3041
2720
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
@@ -3044,25 +2723,26 @@ Then return the corrected translation document:`;
|
|
|
3044
2723
|
maxConcurrentTranslationJobs && (devLog.debug("Updating translation semaphore permits", {
|
|
3045
2724
|
...logContext,
|
|
3046
2725
|
newPermits: maxConcurrentTranslationJobs
|
|
3047
|
-
}), translationSemaphore.updatePermits(maxConcurrentTranslationJobs)),
|
|
2726
|
+
}), translationSemaphore.updatePermits(maxConcurrentTranslationJobs)), dispatch({
|
|
3048
2727
|
type: "SET_OVERALL_STATUS",
|
|
3049
2728
|
payload: "translating"
|
|
3050
2729
|
});
|
|
3051
2730
|
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()}`;
|
|
3052
|
-
devLog.info("Creating Content Release for translation batch", {
|
|
2731
|
+
devLog.info("Creating new Content Release for translation batch", {
|
|
3053
2732
|
...logContext,
|
|
3054
2733
|
releaseTitle
|
|
3055
2734
|
});
|
|
3056
|
-
const { releaseId:
|
|
2735
|
+
const { releaseId: releaseId2 } = await client.releases.create({
|
|
3057
2736
|
metadata: {
|
|
3058
2737
|
title: releaseTitle,
|
|
3059
2738
|
description: releaseDescription,
|
|
3060
|
-
releaseType: "
|
|
2739
|
+
releaseType: "undecided"
|
|
3061
2740
|
}
|
|
3062
2741
|
});
|
|
3063
|
-
|
|
2742
|
+
devLog.info("\u2705 NEW CONTENT RELEASE CREATED", {
|
|
3064
2743
|
...logContext,
|
|
3065
|
-
releaseId
|
|
2744
|
+
releaseId: releaseId2,
|
|
2745
|
+
releaseTitle
|
|
3066
2746
|
});
|
|
3067
2747
|
const sourceDocuments = [
|
|
3068
2748
|
{
|
|
@@ -3090,16 +2770,14 @@ Then return the corrected translation document:`;
|
|
|
3090
2770
|
try {
|
|
3091
2771
|
await translationSemaphore.withPermit(async () => {
|
|
3092
2772
|
const document = await client.withConfig({
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
id: documentId
|
|
3097
|
-
});
|
|
3098
|
-
if (document && releaseId) {
|
|
2773
|
+
perspective: "drafts"
|
|
2774
|
+
}).getDocument(documentId);
|
|
2775
|
+
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId2) {
|
|
3099
2776
|
const targetDocumentId = await translateDocument({
|
|
3100
2777
|
dispatch,
|
|
3101
2778
|
client,
|
|
3102
2779
|
sourceDocument: document,
|
|
2780
|
+
sourceLanguage,
|
|
3103
2781
|
targetLanguage,
|
|
3104
2782
|
languageFieldName,
|
|
3105
2783
|
shouldOverwriteFreshTranslations,
|
|
@@ -3107,7 +2785,7 @@ Then return the corrected translation document:`;
|
|
|
3107
2785
|
translationPrompts,
|
|
3108
2786
|
translationMode,
|
|
3109
2787
|
openaiConfig,
|
|
3110
|
-
releaseId
|
|
2788
|
+
releaseId: releaseId2
|
|
3111
2789
|
});
|
|
3112
2790
|
targetDocumentId && targetDocumentIds.push(targetDocumentId);
|
|
3113
2791
|
}
|
|
@@ -3155,24 +2833,11 @@ Then return the corrected translation document:`;
|
|
|
3155
2833
|
failed
|
|
3156
2834
|
}), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })) : (devLog.info("Document set translation completed successfully", logContext), dispatch({ type: "SET_OVERALL_STATUS", payload: "completed" })), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd();
|
|
3157
2835
|
} catch (error) {
|
|
3158
|
-
|
|
2836
|
+
throw devLog.error(
|
|
3159
2837
|
"translateDocumentSet failed with unexpected error",
|
|
3160
2838
|
error instanceof Error ? error : new Error(String(error)),
|
|
3161
2839
|
logContext
|
|
3162
|
-
),
|
|
3163
|
-
try {
|
|
3164
|
-
await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
|
|
3165
|
-
...logContext,
|
|
3166
|
-
releaseId
|
|
3167
|
-
});
|
|
3168
|
-
} catch (cleanupError) {
|
|
3169
|
-
devLog.error(
|
|
3170
|
-
"Failed to cleanup release",
|
|
3171
|
-
cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
|
|
3172
|
-
logContext
|
|
3173
|
-
);
|
|
3174
|
-
}
|
|
3175
|
-
throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
|
|
2840
|
+
), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
|
|
3176
2841
|
}
|
|
3177
2842
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
3178
2843
|
const {
|
|
@@ -3593,24 +3258,12 @@ Then return the corrected translation document:`;
|
|
|
3593
3258
|
" \u2014 ",
|
|
3594
3259
|
job._id.slice(-6)
|
|
3595
3260
|
] }),
|
|
3596
|
-
!["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */
|
|
3597
|
-
|
|
3598
|
-
job.
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
] }),
|
|
3603
|
-
job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3604
|
-
/* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3605
|
-
job.tokensUsed.toLocaleString(),
|
|
3606
|
-
" tokens (~$",
|
|
3607
|
-
job.estimatedCost?.toFixed(4),
|
|
3608
|
-
")"
|
|
3609
|
-
] }),
|
|
3610
|
-
job.reasoningSummary && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3611
|
-
"\u{1F4A1} ",
|
|
3612
|
-
job.reasoningSummary
|
|
3613
|
-
] }) })
|
|
3261
|
+
!["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
|
|
3262
|
+
job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3263
|
+
job.tokensUsed.toLocaleString(),
|
|
3264
|
+
" tokens (~$",
|
|
3265
|
+
job.estimatedCost?.toFixed(4),
|
|
3266
|
+
")"
|
|
3614
3267
|
] }) : null,
|
|
3615
3268
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3616
3269
|
"Error: ",
|