@braze/sanity-plugin-ai-translation 0.2.21 → 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 +306 -638
- 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";
|
|
@@ -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,454 +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
|
-
const existingDoc = await client.getDocument(existingTranslationId);
|
|
2515
|
-
if (!existingDoc)
|
|
2516
|
-
throw new Error(`Existing translation document not found: ${existingTranslationId}`);
|
|
2517
|
-
await client.action({
|
|
2518
|
-
actionType: "sanity.action.document.version.create",
|
|
2519
|
-
publishedId: existingTranslationId,
|
|
2520
|
-
document: {
|
|
2521
|
-
...existingDoc,
|
|
2522
|
-
_id: `versions.${releaseId}.${existingTranslationId}`
|
|
2523
|
-
}
|
|
2524
|
-
}), versionedDocumentId = `versions.${releaseId}.${existingTranslationId}`, baseDocumentId = existingTranslationId, devLog.info("Existing translation version created", {
|
|
2525
|
-
...logContext,
|
|
2526
|
-
versionedDocumentId
|
|
2527
|
-
});
|
|
2528
|
-
const { _id, _createdAt, _updatedAt, _rev, ...sourceContent } = sourceDocument;
|
|
2529
|
-
await client.createOrReplace({
|
|
2530
|
-
...sourceContent,
|
|
2531
|
-
_id: versionedDocumentId,
|
|
2532
|
-
[languageFieldName]: targetLanguage
|
|
2533
|
-
}), devLog.info("Source content cloned onto version", {
|
|
2534
|
-
...logContext,
|
|
2535
|
-
versionedDocumentId
|
|
2536
|
-
});
|
|
2537
|
-
}
|
|
2538
|
-
let translatedDocument;
|
|
2539
|
-
switch (translationMode) {
|
|
2540
|
-
case "openai-only": {
|
|
2541
|
-
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2542
|
-
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2543
|
-
devLog.info("Using OpenAI-only translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai");
|
|
2544
|
-
const versionedDoc = await client.getDocument(versionedDocumentId);
|
|
2545
|
-
if (!versionedDoc)
|
|
2546
|
-
throw new Error(`Versioned document not found: ${versionedDocumentId}`);
|
|
2547
|
-
const translatedDoc = await translateInPlaceWithOpenAI({
|
|
2548
|
-
dispatch,
|
|
2549
|
-
sourceDocument,
|
|
2550
|
-
targetDocument: versionedDoc,
|
|
2551
|
-
targetLanguage,
|
|
2552
|
-
languageFieldName,
|
|
2553
|
-
translationPrompts,
|
|
2554
|
-
openaiConfig,
|
|
2555
|
-
isReview: !1
|
|
2556
|
-
});
|
|
2557
|
-
translatedDoc && (translatedDocument = await client.createOrReplace({
|
|
2558
|
-
...translatedDoc,
|
|
2559
|
-
_id: versionedDocumentId
|
|
2560
|
-
}));
|
|
2561
|
-
break;
|
|
2562
|
-
}
|
|
2563
|
-
case "sanity-with-review":
|
|
2564
|
-
if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
|
|
2565
|
-
throw new Error("OpenAI is not configured but sanity-with-review mode was selected");
|
|
2566
|
-
if (devLog.info("Using Sanity + OpenAI review", logContext), translatedDocument = await translateWithSanityAgent({
|
|
2567
|
-
dispatch,
|
|
2568
|
-
client,
|
|
2569
|
-
schemaId,
|
|
2570
|
-
sourceDocument,
|
|
2571
|
-
targetLanguage,
|
|
2572
|
-
languageFieldName,
|
|
2573
|
-
versionedDocumentId,
|
|
2574
|
-
translationPrompts
|
|
2575
|
-
}), translatedDocument) {
|
|
2576
|
-
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "reviewing_with_openai");
|
|
2577
|
-
const reviewedDoc = await translateInPlaceWithOpenAI({
|
|
2578
|
-
dispatch,
|
|
2579
|
-
sourceDocument,
|
|
2580
|
-
targetDocument: translatedDocument,
|
|
2581
|
-
targetLanguage,
|
|
2582
|
-
languageFieldName,
|
|
2583
|
-
translationPrompts,
|
|
2584
|
-
openaiConfig,
|
|
2585
|
-
isReview: !0
|
|
2586
|
-
});
|
|
2587
|
-
reviewedDoc && (translatedDocument = await client.createOrReplace({
|
|
2588
|
-
...reviewedDoc,
|
|
2589
|
-
_id: versionedDocumentId
|
|
2590
|
-
}));
|
|
2591
|
-
}
|
|
2592
|
-
break;
|
|
2593
|
-
default:
|
|
2594
|
-
devLog.info("Using Sanity-only translation", logContext), translatedDocument = await translateWithSanityAgent({
|
|
2595
|
-
dispatch,
|
|
2596
|
-
client,
|
|
2597
|
-
schemaId,
|
|
2598
|
-
sourceDocument,
|
|
2599
|
-
targetLanguage,
|
|
2600
|
-
languageFieldName,
|
|
2601
|
-
versionedDocumentId,
|
|
2602
|
-
translationPrompts
|
|
2603
|
-
});
|
|
2604
|
-
break;
|
|
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
|
|
2605
2521
|
}
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
_id: baseDocumentId
|
|
2609
|
-
} : void 0;
|
|
2610
|
-
} catch (error) {
|
|
2611
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2612
|
-
devLog.error(
|
|
2613
|
-
"translateEntireDocument failed",
|
|
2614
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
2615
|
-
logContext
|
|
2616
|
-
), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
2617
|
-
errorMessage
|
|
2618
|
-
}), devLog.timeEnd("translateEntireDocument"), devLog.groupEnd();
|
|
2619
|
-
return;
|
|
2620
|
-
}
|
|
2621
|
-
}, translateWithSanityAgent = async ({
|
|
2622
|
-
dispatch,
|
|
2623
|
-
client,
|
|
2624
|
-
schemaId,
|
|
2522
|
+
};
|
|
2523
|
+
}, getSanityAgentTranslation = async ({
|
|
2625
2524
|
sourceDocument,
|
|
2626
2525
|
targetLanguage,
|
|
2627
2526
|
languageFieldName,
|
|
2628
|
-
|
|
2527
|
+
schemaId,
|
|
2528
|
+
client,
|
|
2629
2529
|
translationPrompts
|
|
2630
2530
|
}) => {
|
|
2631
|
-
|
|
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,
|
|
2632
2552
|
documentId: sourceDocument._id,
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
toLanguage,
|
|
2645
|
-
fromLanguage
|
|
2646
|
-
});
|
|
2647
|
-
const documentAnalysis = analyzeDocumentContent(sourceDocument);
|
|
2648
|
-
devLog.debug("Document analysis completed", {
|
|
2649
|
-
...logContext,
|
|
2650
|
-
analysis: documentAnalysis
|
|
2651
|
-
}), devLog.info("Starting Sanity agent translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating");
|
|
2652
|
-
const { styleGuide, omittedSections, truncatedSections } = assembleTranslationPrompt(
|
|
2653
|
-
{
|
|
2654
|
-
documentAnalysis,
|
|
2655
|
-
isUpdate: !1,
|
|
2656
|
-
// Always fresh translation in a version
|
|
2657
|
-
targetLanguage,
|
|
2658
|
-
sourceLanguage: sourceDocument[languageFieldName]
|
|
2659
|
-
},
|
|
2660
|
-
{
|
|
2661
|
-
globalPrompt: translationPrompts.globalPrompt,
|
|
2662
|
-
localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
|
|
2663
|
-
glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
|
|
2664
|
-
protectedPhrases: translationPrompts.protectedPhrases
|
|
2665
|
-
}
|
|
2666
|
-
);
|
|
2667
|
-
devLog.info("Structured style guide generated", {
|
|
2668
|
-
...logContext,
|
|
2669
|
-
styleGuideLength: styleGuide.length,
|
|
2670
|
-
omittedSections,
|
|
2671
|
-
truncatedSections
|
|
2672
|
-
}), (omittedSections.length > 0 || truncatedSections.length > 0) && devLog.warn("Style guide adjusted to fit character limit", {
|
|
2673
|
-
...logContext,
|
|
2674
|
-
omittedSections,
|
|
2675
|
-
truncatedSections
|
|
2676
|
-
});
|
|
2677
|
-
const newDocument = await client.agent.action.translate({
|
|
2678
|
-
schemaId,
|
|
2679
|
-
toLanguage,
|
|
2680
|
-
fromLanguage,
|
|
2681
|
-
documentId: versionedDocumentId,
|
|
2682
|
-
// Use versioned document ID
|
|
2683
|
-
languageFieldPath: [languageFieldName],
|
|
2684
|
-
styleGuide,
|
|
2685
|
-
protectedPhrases: translationPrompts.protectedPhrases || [],
|
|
2686
|
-
conditionalPaths: {
|
|
2687
|
-
defaultHidden: !1,
|
|
2688
|
-
defaultReadOnly: !1
|
|
2689
|
-
}
|
|
2690
|
-
});
|
|
2691
|
-
return devLog.info("Sanity agent translation completed", {
|
|
2692
|
-
...logContext,
|
|
2693
|
-
newDocumentId: newDocument._id,
|
|
2694
|
-
newDocumentRev: newDocument._rev
|
|
2695
|
-
}), newDocument;
|
|
2696
|
-
} catch (error) {
|
|
2697
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2698
|
-
devLog.error(
|
|
2699
|
-
"Sanity agent translation failed",
|
|
2700
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
2701
|
-
logContext
|
|
2702
|
-
), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
|
|
2703
|
-
errorMessage
|
|
2704
|
-
});
|
|
2705
|
-
return;
|
|
2706
|
-
}
|
|
2707
|
-
}, 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 ({
|
|
2708
2564
|
dispatch,
|
|
2565
|
+
client,
|
|
2566
|
+
schemaId,
|
|
2709
2567
|
sourceDocument,
|
|
2710
|
-
targetDocument,
|
|
2711
2568
|
targetLanguage,
|
|
2569
|
+
existingTranslationId,
|
|
2712
2570
|
languageFieldName,
|
|
2713
2571
|
translationPrompts,
|
|
2572
|
+
translationMode,
|
|
2714
2573
|
openaiConfig,
|
|
2715
|
-
|
|
2574
|
+
releaseId
|
|
2716
2575
|
}) => {
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
...logContext,
|
|
2728
|
-
sourceLength: sourceText.length,
|
|
2729
|
-
targetLength: targetText.length
|
|
2730
|
-
});
|
|
2731
|
-
const prompt = isReview ? buildReviewPrompt({
|
|
2732
|
-
sourceText,
|
|
2733
|
-
translatedText: targetText,
|
|
2734
|
-
sourceLanguage,
|
|
2735
|
-
targetLanguage,
|
|
2736
|
-
translationPrompts
|
|
2737
|
-
}) : buildTranslationPrompt({
|
|
2738
|
-
sourceText,
|
|
2739
|
-
sourceLanguage,
|
|
2740
|
-
targetLanguage,
|
|
2741
|
-
translationPrompts
|
|
2742
|
-
});
|
|
2743
|
-
devLog.debug("Calling GPT-5", {
|
|
2744
|
-
...logContext,
|
|
2745
|
-
promptLength: prompt.length
|
|
2746
|
-
});
|
|
2747
|
-
const response = await callGPT5(openaiConfig.apiKey, {
|
|
2748
|
-
messages: [
|
|
2749
|
-
{
|
|
2750
|
-
role: "system",
|
|
2751
|
-
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."
|
|
2752
|
-
},
|
|
2753
|
-
{ role: "user", content: prompt }
|
|
2754
|
-
],
|
|
2755
|
-
model: openaiConfig.model,
|
|
2756
|
-
reasoningEffort: openaiConfig.reasoningEffort,
|
|
2757
|
-
verbosity: openaiConfig.verbosity,
|
|
2758
|
-
onReasoningChunk: (reasoning) => {
|
|
2759
|
-
const status2 = isReview ? "reviewing_with_openai" : "translating_with_openai";
|
|
2760
|
-
setJobStatus(dispatch, sourceDocument._id, targetLanguage, status2, {
|
|
2761
|
-
reasoningProgress: reasoning
|
|
2762
|
-
});
|
|
2763
|
-
}
|
|
2764
|
-
});
|
|
2765
|
-
devLog.info("OpenAI operation completed", {
|
|
2766
|
-
...logContext,
|
|
2767
|
-
tokensUsed: response.tokensUsed.total,
|
|
2768
|
-
cost: response.cost
|
|
2769
|
-
});
|
|
2770
|
-
const result = deserializeDocument(targetDocument, response.content), validation = validateStructureIntegrity(targetDocument, result);
|
|
2771
|
-
if (!validation.valid) {
|
|
2772
|
-
devLog.error("Structure validation failed", {
|
|
2773
|
-
...logContext,
|
|
2774
|
-
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
|
|
2775
2586
|
});
|
|
2776
|
-
|
|
2777
|
-
(err) => !(err.path.length === 1 && err.path[0] === languageFieldName)
|
|
2778
|
-
);
|
|
2779
|
-
if (unexpectedErrors.length > 0)
|
|
2780
|
-
throw new Error(
|
|
2781
|
-
`Structure validation failed: ${unexpectedErrors.length} errors. First: ${unexpectedErrors[0].type} at ${unexpectedErrors[0].path.join(".")}`
|
|
2782
|
-
);
|
|
2587
|
+
break;
|
|
2783
2588
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
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}`);
|
|
2793
2606
|
}
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
}
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
${globalPrompt ? `### General Guidelines
|
|
2808
|
-
${globalPrompt}
|
|
2809
|
-
` : ""}
|
|
2810
|
-
${localePrompt ? `### ${targetLanguage} Guidelines
|
|
2811
|
-
${localePrompt}
|
|
2812
|
-
` : ""}
|
|
2813
|
-
${glossaryTerms ? `### Required Terminology
|
|
2814
|
-
${glossaryTerms}
|
|
2815
|
-
` : ""}
|
|
2816
|
-
${protectedPhrases?.length ? `### Protected Phrases (do not translate)
|
|
2817
|
-
${protectedPhrases.join(", ")}
|
|
2818
|
-
` : ""}
|
|
2819
|
-
|
|
2820
|
-
## Source Document (${sourceLanguage})
|
|
2821
|
-
|
|
2822
|
-
\`\`\`
|
|
2823
|
-
${sourceText}
|
|
2824
|
-
\`\`\`
|
|
2825
|
-
|
|
2826
|
-
## Instructions
|
|
2827
|
-
|
|
2828
|
-
1. Translate all text content into ${targetLanguage}
|
|
2829
|
-
2. Follow the guidelines and terminology requirements above
|
|
2830
|
-
3. Preserve protected phrases exactly as they appear
|
|
2831
|
-
4. Return the document in THE EXACT SAME FORMAT
|
|
2832
|
-
5. **CRITICAL**: Only modify text content between tags
|
|
2833
|
-
6. Never add, remove, or change any tags, attributes, or structure
|
|
2834
|
-
|
|
2835
|
-
If you used significant reasoning to make translation decisions, you may include a brief summary:
|
|
2836
|
-
<reasoning>
|
|
2837
|
-
Brief summary of key translation decisions and reasoning
|
|
2838
|
-
</reasoning>
|
|
2839
|
-
|
|
2840
|
-
Then return the translated document:`;
|
|
2841
|
-
}, buildReviewPrompt = ({
|
|
2842
|
-
sourceText,
|
|
2843
|
-
translatedText,
|
|
2844
|
-
sourceLanguage,
|
|
2845
|
-
targetLanguage,
|
|
2846
|
-
translationPrompts
|
|
2847
|
-
}) => {
|
|
2848
|
-
const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
|
|
2849
|
-
return `# TRANSLATION QUALITY REVIEW
|
|
2850
|
-
|
|
2851
|
-
Review this ${targetLanguage} translation and return a corrected version.
|
|
2852
|
-
|
|
2853
|
-
## Review Criteria
|
|
2854
|
-
|
|
2855
|
-
1. **Accuracy**: Does it accurately convey the ${sourceLanguage} source meaning?
|
|
2856
|
-
2. **Terminology**: Are glossary terms used correctly?
|
|
2857
|
-
3. **Protected Phrases**: Are protected phrases preserved?
|
|
2858
|
-
4. **Fluency**: Is it natural and fluent in ${targetLanguage}?
|
|
2859
|
-
5. **Style**: Does it follow the style guide?
|
|
2860
|
-
6. **Completeness**: Is all content translated?
|
|
2861
|
-
|
|
2862
|
-
## Guidelines
|
|
2863
|
-
|
|
2864
|
-
${globalPrompt ? `### General Guidelines
|
|
2865
|
-
${globalPrompt}
|
|
2866
|
-
` : ""}
|
|
2867
|
-
${localePrompt ? `### ${targetLanguage} Guidelines
|
|
2868
|
-
${localePrompt}
|
|
2869
|
-
` : ""}
|
|
2870
|
-
${glossaryTerms ? `### Required Terminology
|
|
2871
|
-
${glossaryTerms}
|
|
2872
|
-
` : ""}
|
|
2873
|
-
${protectedPhrases?.length ? `### Protected Phrases (must preserve)
|
|
2874
|
-
${protectedPhrases.join(", ")}
|
|
2875
|
-
` : ""}
|
|
2876
|
-
|
|
2877
|
-
## Source Document (${sourceLanguage})
|
|
2878
|
-
|
|
2879
|
-
\`\`\`
|
|
2880
|
-
${sourceText}
|
|
2881
|
-
\`\`\`
|
|
2882
|
-
|
|
2883
|
-
## Current Translation (${targetLanguage})
|
|
2884
|
-
|
|
2885
|
-
\`\`\`
|
|
2886
|
-
${translatedText}
|
|
2887
|
-
\`\`\`
|
|
2888
|
-
|
|
2889
|
-
## Instructions
|
|
2890
|
-
|
|
2891
|
-
1. Review the translation for quality issues
|
|
2892
|
-
2. Make corrections where needed to improve accuracy, fluency, or adherence to guidelines
|
|
2893
|
-
3. Keep good translations as-is - only fix what needs fixing
|
|
2894
|
-
4. Return the corrected translation in THE EXACT SAME FORMAT
|
|
2895
|
-
5. **CRITICAL**: Only modify text content between tags
|
|
2896
|
-
6. Never add, remove, or change any tags, attributes, or structure
|
|
2897
|
-
|
|
2898
|
-
If you made corrections or have insights about the translation quality, include a brief summary:
|
|
2899
|
-
<reasoning>
|
|
2900
|
-
Brief summary of review findings and corrections made
|
|
2901
|
-
</reasoning>
|
|
2902
|
-
|
|
2903
|
-
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;
|
|
2904
2620
|
}, translateDocument = async ({
|
|
2905
2621
|
dispatch,
|
|
2906
2622
|
client,
|
|
2907
2623
|
sourceDocument,
|
|
2624
|
+
sourceLanguage,
|
|
2908
2625
|
targetLanguage,
|
|
2909
2626
|
languageFieldName,
|
|
2910
2627
|
shouldOverwriteFreshTranslations,
|
|
@@ -2923,62 +2640,37 @@ Then return the corrected translation document:`;
|
|
|
2923
2640
|
};
|
|
2924
2641
|
devLog.group("translateDocument", logContext), devLog.time("translateDocument"), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "preparing_translation");
|
|
2925
2642
|
try {
|
|
2926
|
-
const sourceLanguage = sourceDocument[languageFieldName];
|
|
2927
|
-
if (devLog.debug("Checking source document language", {
|
|
2928
|
-
...logContext,
|
|
2929
|
-
sourceLanguage,
|
|
2930
|
-
languageFieldName
|
|
2931
|
-
}), !sourceLanguage) {
|
|
2932
|
-
const error = `Source document ${sourceDocument._id} does not have a language`;
|
|
2933
|
-
throw devLog.error(error, void 0, logContext), new Error(error);
|
|
2934
|
-
}
|
|
2935
|
-
devLog.debug("Getting translation metadata", logContext);
|
|
2936
2643
|
const translationMetadata = await getTranslationMetadata({
|
|
2937
2644
|
client,
|
|
2938
2645
|
sourceDocument,
|
|
2939
2646
|
sourceLanguage
|
|
2940
2647
|
}), isAlreadyFreshTranslation = translationMetadata.translationHistory?.some(
|
|
2941
2648
|
(history) => history.language === targetLanguage && history.sourceRev === sourceDocument._rev
|
|
2942
|
-
)
|
|
2943
|
-
if (
|
|
2944
|
-
|
|
2945
|
-
isAlreadyFreshTranslation,
|
|
2946
|
-
currentRev: sourceDocument._rev
|
|
2947
|
-
}), !shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
|
|
2948
|
-
devLog.info("Skipping fresh translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
|
|
2649
|
+
);
|
|
2650
|
+
if (!shouldOverwriteFreshTranslations && isAlreadyFreshTranslation) {
|
|
2651
|
+
setJobStatus(dispatch, sourceDocument._id, targetLanguage, "skipped", {
|
|
2949
2652
|
targetId: translationMetadata.translations.find((t) => t._key === targetLanguage)?.value._ref
|
|
2950
|
-
})
|
|
2653
|
+
});
|
|
2951
2654
|
return;
|
|
2952
2655
|
}
|
|
2953
|
-
const
|
|
2954
|
-
translationMetadata,
|
|
2955
|
-
targetLanguage
|
|
2956
|
-
}), isNewTranslation = translationTarget.operation === "create";
|
|
2957
|
-
devLog.debug("Translation target determined", {
|
|
2958
|
-
...logContext,
|
|
2959
|
-
operation: translationTarget.operation,
|
|
2960
|
-
isNewTranslation
|
|
2961
|
-
});
|
|
2962
|
-
const newDocument = await translateEntireDocument({
|
|
2656
|
+
const existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = await handleTranslation({
|
|
2963
2657
|
dispatch,
|
|
2964
2658
|
client,
|
|
2965
2659
|
schemaId,
|
|
2966
2660
|
sourceDocument,
|
|
2967
2661
|
targetLanguage,
|
|
2662
|
+
existingTranslationId,
|
|
2968
2663
|
languageFieldName,
|
|
2969
|
-
translationTarget,
|
|
2970
2664
|
translationPrompts,
|
|
2971
2665
|
translationMode,
|
|
2972
2666
|
openaiConfig,
|
|
2973
|
-
releaseId
|
|
2974
|
-
isNewTranslation
|
|
2667
|
+
releaseId
|
|
2975
2668
|
});
|
|
2976
|
-
if (!newDocument)
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
}), 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", {
|
|
2982
2674
|
targetId: newDocument._id
|
|
2983
2675
|
}), await updateTranslationMetadata({
|
|
2984
2676
|
client,
|
|
@@ -2987,8 +2679,10 @@ Then return the corrected translation document:`;
|
|
|
2987
2679
|
newDocument,
|
|
2988
2680
|
targetLanguage,
|
|
2989
2681
|
translationMetadata,
|
|
2990
|
-
isNewTranslation
|
|
2991
|
-
}), 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", {
|
|
2992
2686
|
...logContext,
|
|
2993
2687
|
newDocumentId: newDocument._id
|
|
2994
2688
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
@@ -3014,13 +2708,13 @@ Then return the corrected translation document:`;
|
|
|
3014
2708
|
}) => {
|
|
3015
2709
|
const logContext = {
|
|
3016
2710
|
documentId: sourceDocument._id,
|
|
2711
|
+
documentRev: sourceDocument._rev,
|
|
3017
2712
|
targetLanguages,
|
|
3018
2713
|
referencesCount: references.length,
|
|
3019
2714
|
maxConcurrentJobs: maxConcurrentTranslationJobs,
|
|
3020
2715
|
translationMode,
|
|
3021
2716
|
shouldOverwriteFreshTranslations
|
|
3022
2717
|
};
|
|
3023
|
-
devLog.group("translateDocumentSet", logContext), devLog.time("translateDocumentSet");
|
|
3024
2718
|
let releaseId;
|
|
3025
2719
|
try {
|
|
3026
2720
|
const sourceLanguage = sourceDocument[languageFieldName];
|
|
@@ -3029,25 +2723,26 @@ Then return the corrected translation document:`;
|
|
|
3029
2723
|
maxConcurrentTranslationJobs && (devLog.debug("Updating translation semaphore permits", {
|
|
3030
2724
|
...logContext,
|
|
3031
2725
|
newPermits: maxConcurrentTranslationJobs
|
|
3032
|
-
}), translationSemaphore.updatePermits(maxConcurrentTranslationJobs)),
|
|
2726
|
+
}), translationSemaphore.updatePermits(maxConcurrentTranslationJobs)), dispatch({
|
|
3033
2727
|
type: "SET_OVERALL_STATUS",
|
|
3034
2728
|
payload: "translating"
|
|
3035
2729
|
});
|
|
3036
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()}`;
|
|
3037
|
-
devLog.info("Creating Content Release for translation batch", {
|
|
2731
|
+
devLog.info("Creating new Content Release for translation batch", {
|
|
3038
2732
|
...logContext,
|
|
3039
2733
|
releaseTitle
|
|
3040
2734
|
});
|
|
3041
|
-
const { releaseId:
|
|
2735
|
+
const { releaseId: releaseId2 } = await client.releases.create({
|
|
3042
2736
|
metadata: {
|
|
3043
2737
|
title: releaseTitle,
|
|
3044
2738
|
description: releaseDescription,
|
|
3045
|
-
releaseType: "
|
|
2739
|
+
releaseType: "undecided"
|
|
3046
2740
|
}
|
|
3047
2741
|
});
|
|
3048
|
-
|
|
2742
|
+
devLog.info("\u2705 NEW CONTENT RELEASE CREATED", {
|
|
3049
2743
|
...logContext,
|
|
3050
|
-
releaseId
|
|
2744
|
+
releaseId: releaseId2,
|
|
2745
|
+
releaseTitle
|
|
3051
2746
|
});
|
|
3052
2747
|
const sourceDocuments = [
|
|
3053
2748
|
{
|
|
@@ -3075,16 +2770,14 @@ Then return the corrected translation document:`;
|
|
|
3075
2770
|
try {
|
|
3076
2771
|
await translationSemaphore.withPermit(async () => {
|
|
3077
2772
|
const document = await client.withConfig({
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
id: documentId
|
|
3082
|
-
});
|
|
3083
|
-
if (document && releaseId) {
|
|
2773
|
+
perspective: "drafts"
|
|
2774
|
+
}).getDocument(documentId);
|
|
2775
|
+
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId2) {
|
|
3084
2776
|
const targetDocumentId = await translateDocument({
|
|
3085
2777
|
dispatch,
|
|
3086
2778
|
client,
|
|
3087
2779
|
sourceDocument: document,
|
|
2780
|
+
sourceLanguage,
|
|
3088
2781
|
targetLanguage,
|
|
3089
2782
|
languageFieldName,
|
|
3090
2783
|
shouldOverwriteFreshTranslations,
|
|
@@ -3092,7 +2785,7 @@ Then return the corrected translation document:`;
|
|
|
3092
2785
|
translationPrompts,
|
|
3093
2786
|
translationMode,
|
|
3094
2787
|
openaiConfig,
|
|
3095
|
-
releaseId
|
|
2788
|
+
releaseId: releaseId2
|
|
3096
2789
|
});
|
|
3097
2790
|
targetDocumentId && targetDocumentIds.push(targetDocumentId);
|
|
3098
2791
|
}
|
|
@@ -3140,24 +2833,11 @@ Then return the corrected translation document:`;
|
|
|
3140
2833
|
failed
|
|
3141
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();
|
|
3142
2835
|
} catch (error) {
|
|
3143
|
-
|
|
2836
|
+
throw devLog.error(
|
|
3144
2837
|
"translateDocumentSet failed with unexpected error",
|
|
3145
2838
|
error instanceof Error ? error : new Error(String(error)),
|
|
3146
2839
|
logContext
|
|
3147
|
-
),
|
|
3148
|
-
try {
|
|
3149
|
-
await client.releases.delete({ releaseId }), devLog.warn("Deleted release due to fatal error", {
|
|
3150
|
-
...logContext,
|
|
3151
|
-
releaseId
|
|
3152
|
-
});
|
|
3153
|
-
} catch (cleanupError) {
|
|
3154
|
-
devLog.error(
|
|
3155
|
-
"Failed to cleanup release",
|
|
3156
|
-
cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)),
|
|
3157
|
-
logContext
|
|
3158
|
-
);
|
|
3159
|
-
}
|
|
3160
|
-
throw devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
|
|
2840
|
+
), devLog.timeEnd("translateDocumentSet"), devLog.groupEnd(), dispatch({ type: "SET_OVERALL_STATUS", payload: "error" }), error;
|
|
3161
2841
|
}
|
|
3162
2842
|
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
3163
2843
|
const {
|
|
@@ -3578,24 +3258,12 @@ Then return the corrected translation document:`;
|
|
|
3578
3258
|
" \u2014 ",
|
|
3579
3259
|
job._id.slice(-6)
|
|
3580
3260
|
] }),
|
|
3581
|
-
!["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */
|
|
3582
|
-
|
|
3583
|
-
job.
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
] }),
|
|
3588
|
-
job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
3589
|
-
/* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
3590
|
-
job.tokensUsed.toLocaleString(),
|
|
3591
|
-
" tokens (~$",
|
|
3592
|
-
job.estimatedCost?.toFixed(4),
|
|
3593
|
-
")"
|
|
3594
|
-
] }),
|
|
3595
|
-
job.reasoningSummary && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3596
|
-
"\u{1F4A1} ",
|
|
3597
|
-
job.reasoningSummary
|
|
3598
|
-
] }) })
|
|
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
|
+
")"
|
|
3599
3267
|
] }) : null,
|
|
3600
3268
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
3601
3269
|
"Error: ",
|