@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.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\./, ""), draftId = (id) => `drafts.${undraftId(id)}`, TRANSLATION_METADATA_QUERY = "*[_type == $translationMetadataType && references($documentId)][0]", useTranslationMetadata = ({ documentId }) => {
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, defaults(action.payload, newTranslationJobs.get(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
- }, getTranslationMetadata = async ({
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: undraftId(newDocument._id),
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: undraftId(sourceDocument._id),
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
- }, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
1926
- (translation) => translation._key === targetLanguage
1927
- )?.value._ref || null, determineTranslationTarget = ({
1928
- translationMetadata,
1929
- targetLanguage
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
- }, assembleLanguageSpecs = ({
1964
- sourceDocument,
1965
- languageFieldName,
1966
- targetLanguage
1967
- }) => {
1968
- const sourceLanugageId = sourceDocument[languageFieldName];
1969
- if (!sourceLanugageId)
1970
- throw new Error("[assembleLanguages] Source language ID is missing");
1971
- return {
1972
- fromLanguage: {
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
- toLanguage: {
1977
- id: targetLanguage,
1978
- title: targetLanguage
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
- }, callGPT5 = async (apiKey, request) => {
1982
- const model = request.model || "gpt-5", isGPT5 = model === "gpt-5", useStreaming = !!request.onReasoningChunk, requestBody = {
1967
+ }, callChatCompletionsAPI = async (apiKey, request) => {
1968
+ const model = request.model || "gpt-4o", requestBody = {
1983
1969
  model,
1984
1970
  messages: request.messages,
1985
- stream: useStreaming
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(`GPT-5 API error: ${error.error?.message || "Unknown 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 = calculateGPT5Cost(tokensUsed, request.model || "gpt-5"), messageContent = result.choices[0].message.content;
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: contentBuffer || fullContent,
1991
+ content: result.choices[0].message.content || "",
2061
1992
  tokensUsed,
2062
- cost,
2063
- reasoningSummary: reasoningBuffer || void 0
1993
+ cost
2064
1994
  };
2065
- }, calculateGPT5Cost = (tokens, model) => {
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
- }, translateEntireDocument = async ({
2461
- dispatch,
2462
- client,
2463
- schemaId,
2505
+ }, assembleLanguageSpecs = ({
2464
2506
  sourceDocument,
2465
- targetLanguage,
2466
2507
  languageFieldName,
2467
- translationTarget,
2468
- translationPrompts,
2469
- translationMode,
2470
- openaiConfig,
2471
- releaseId,
2472
- isNewTranslation
2508
+ targetLanguage
2473
2509
  }) => {
2474
- const logContext = {
2475
- documentId: sourceDocument._id,
2476
- targetLanguage,
2477
- schemaId,
2478
- operation: translationTarget.operation,
2479
- targetId: translationTarget._id,
2480
- translationMode,
2481
- releaseId,
2482
- isNewTranslation
2483
- };
2484
- devLog.group("translateEntireDocument", logContext), devLog.time("translateEntireDocument");
2485
- try {
2486
- let versionedDocumentId, baseDocumentId;
2487
- if (isNewTranslation) {
2488
- const { _id, _createdAt, _updatedAt, _rev, ...docWithoutMeta } = sourceDocument, newTranslationDoc = {
2489
- ...docWithoutMeta,
2490
- _type: sourceDocument._type,
2491
- [languageFieldName]: targetLanguage
2492
- };
2493
- devLog.info("Creating new translation document in release", logContext), baseDocumentId = translationTarget._id || `new-translation-${Date.now()}`, await client.action({
2494
- actionType: "sanity.action.document.version.create",
2495
- publishedId: baseDocumentId,
2496
- document: {
2497
- ...newTranslationDoc,
2498
- _type: sourceDocument._type,
2499
- _id: `versions.${releaseId}.${baseDocumentId}`
2500
- }
2501
- }), versionedDocumentId = `versions.${releaseId}.${baseDocumentId}`, devLog.info("New translation version created in release", {
2502
- ...logContext,
2503
- versionedDocumentId,
2504
- baseDocumentId
2505
- });
2506
- } else {
2507
- const existingTranslationId = translationTarget._id;
2508
- if (!existingTranslationId)
2509
- throw new Error("Translation target ID not found for update operation");
2510
- devLog.info("Creating version of existing translation in release", {
2511
- ...logContext,
2512
- existingTranslationId
2513
- });
2514
- 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
- return devLog.timeEnd("translateEntireDocument"), devLog.groupEnd(), translatedDocument ? {
2622
- ...translatedDocument,
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
- versionedDocumentId,
2527
+ schemaId,
2528
+ client,
2644
2529
  translationPrompts
2645
2530
  }) => {
2646
- const logContext = {
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
- targetLanguage,
2649
- versionedDocumentId
2650
- };
2651
- try {
2652
- const { toLanguage, fromLanguage } = assembleLanguageSpecs({
2653
- sourceDocument,
2654
- languageFieldName,
2655
- targetLanguage
2656
- });
2657
- devLog.debug("Language specs assembled", {
2658
- ...logContext,
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
- isReview
2574
+ releaseId
2731
2575
  }) => {
2732
- const sourceLanguage = sourceDocument[languageFieldName], logContext = {
2733
- sourceId: sourceDocument._id,
2734
- targetId: targetDocument._id,
2735
- targetLanguage,
2736
- sourceLanguage,
2737
- isReview
2738
- };
2739
- try {
2740
- const sourceText = serializeDocument(sourceDocument), targetText = serializeDocument(targetDocument);
2741
- devLog.debug("Documents serialized for OpenAI", {
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
- const unexpectedErrors = validation.errors.filter(
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
- devLog.info("Structure validation passed", logContext);
2800
- const status = isReview ? "reviewing_with_openai" : "translating_with_openai";
2801
- return setJobStatus(dispatch, sourceDocument._id, targetLanguage, status, {
2802
- tokensUsed: response.tokensUsed.total,
2803
- estimatedCost: response.cost,
2804
- reasoningSummary: response.reasoningSummary
2805
- }), result;
2806
- } catch (error) {
2807
- throw devLog.error("OpenAI translation failed", error instanceof Error ? error : new Error(String(error)), logContext), error;
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
- }, buildTranslationPrompt = ({
2810
- sourceText,
2811
- sourceLanguage,
2812
- targetLanguage,
2813
- translationPrompts
2814
- }) => {
2815
- const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
2816
- return `# TRANSLATION TASK
2817
-
2818
- Translate this ${sourceLanguage} document into ${targetLanguage}.
2819
-
2820
- ## Guidelines
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
- ) ?? !1;
2958
- if (devLog.debug("Checking if translation is already fresh", {
2959
- ...logContext,
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
- }), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2653
+ });
2966
2654
  return;
2967
2655
  }
2968
- const translationTarget = determineTranslationTarget({
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
- throw devLog.error("Failed to create translation document", void 0, logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error"), devLog.timeEnd("translateDocument"), devLog.groupEnd(), new Error("Failed to create translation document");
2993
- return devLog.debug("Translation document created", {
2994
- ...logContext,
2995
- newDocumentId: newDocument._id
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"), devLog.info("Translation completed successfully", {
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)), devLog.info("Starting document set translation", logContext), dispatch({
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: newReleaseId } = await client.releases.create({
2735
+ const { releaseId: releaseId2 } = await client.releases.create({
3057
2736
  metadata: {
3058
2737
  title: releaseTitle,
3059
2738
  description: releaseDescription,
3060
- releaseType: "asap"
2739
+ releaseType: "undecided"
3061
2740
  }
3062
2741
  });
3063
- releaseId = newReleaseId, devLog.info("Content Release created", {
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
- // This enables us to detect documents that might be stale due to unpublished changes
3094
- perspective: "raw"
3095
- }).fetch("*[_id == $id][0]", {
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
- if (devLog.error(
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
- ), releaseId)
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__ */ jsxs(Stack, { space: 2, children: [
3597
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
3598
- job.reasoningProgress && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 3, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3599
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "\u{1F914} GPT-5 Reasoning" }),
3600
- /* @__PURE__ */ jsx(Text, { size: 1, style: { fontStyle: "italic" }, children: job.reasoningProgress })
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: ",