@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.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, 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,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
- }, 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
- 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
- return devLog.timeEnd("translateEntireDocument"), devLog.groupEnd(), translatedDocument ? {
2607
- ...translatedDocument,
2608
- _id: baseDocumentId
2609
- } : void 0;
2610
- } catch (error) {
2611
- const errorMessage = error instanceof Error ? error.message : String(error);
2612
- devLog.error(
2613
- "translateEntireDocument failed",
2614
- error instanceof Error ? error : new Error(String(error)),
2615
- logContext
2616
- ), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "error", {
2617
- errorMessage
2618
- }), devLog.timeEnd("translateEntireDocument"), devLog.groupEnd();
2619
- return;
2620
- }
2621
- }, translateWithSanityAgent = async ({
2622
- dispatch,
2623
- client,
2624
- schemaId,
2522
+ };
2523
+ }, getSanityAgentTranslation = async ({
2625
2524
  sourceDocument,
2626
2525
  targetLanguage,
2627
2526
  languageFieldName,
2628
- versionedDocumentId,
2527
+ schemaId,
2528
+ client,
2629
2529
  translationPrompts
2630
2530
  }) => {
2631
- 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,
2632
2552
  documentId: sourceDocument._id,
2633
- targetLanguage,
2634
- versionedDocumentId
2635
- };
2636
- try {
2637
- const { toLanguage, fromLanguage } = assembleLanguageSpecs({
2638
- sourceDocument,
2639
- languageFieldName,
2640
- targetLanguage
2641
- });
2642
- devLog.debug("Language specs assembled", {
2643
- ...logContext,
2644
- toLanguage,
2645
- fromLanguage
2646
- });
2647
- const documentAnalysis = analyzeDocumentContent(sourceDocument);
2648
- devLog.debug("Document analysis completed", {
2649
- ...logContext,
2650
- analysis: documentAnalysis
2651
- }), devLog.info("Starting Sanity agent translation", logContext), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating");
2652
- const { styleGuide, omittedSections, truncatedSections } = assembleTranslationPrompt(
2653
- {
2654
- documentAnalysis,
2655
- isUpdate: !1,
2656
- // Always fresh translation in a version
2657
- targetLanguage,
2658
- sourceLanguage: sourceDocument[languageFieldName]
2659
- },
2660
- {
2661
- globalPrompt: translationPrompts.globalPrompt,
2662
- localePrompt: translationPrompts.localeInstructions[targetLanguage]?.prompt,
2663
- glossaryTerms: translationPrompts.localeInstructions[targetLanguage]?.glossaryTerms,
2664
- protectedPhrases: translationPrompts.protectedPhrases
2665
- }
2666
- );
2667
- devLog.info("Structured style guide generated", {
2668
- ...logContext,
2669
- styleGuideLength: styleGuide.length,
2670
- omittedSections,
2671
- truncatedSections
2672
- }), (omittedSections.length > 0 || truncatedSections.length > 0) && devLog.warn("Style guide adjusted to fit character limit", {
2673
- ...logContext,
2674
- omittedSections,
2675
- truncatedSections
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
- isReview
2574
+ releaseId
2716
2575
  }) => {
2717
- const sourceLanguage = sourceDocument[languageFieldName], logContext = {
2718
- sourceId: sourceDocument._id,
2719
- targetId: targetDocument._id,
2720
- targetLanguage,
2721
- sourceLanguage,
2722
- isReview
2723
- };
2724
- try {
2725
- const sourceText = serializeDocument(sourceDocument), targetText = serializeDocument(targetDocument);
2726
- devLog.debug("Documents serialized for OpenAI", {
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
- const unexpectedErrors = validation.errors.filter(
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
- devLog.info("Structure validation passed", logContext);
2785
- const status = isReview ? "reviewing_with_openai" : "translating_with_openai";
2786
- return setJobStatus(dispatch, sourceDocument._id, targetLanguage, status, {
2787
- tokensUsed: response.tokensUsed.total,
2788
- estimatedCost: response.cost,
2789
- reasoningSummary: response.reasoningSummary
2790
- }), result;
2791
- } catch (error) {
2792
- 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}`);
2793
2606
  }
2794
- }, buildTranslationPrompt = ({
2795
- sourceText,
2796
- sourceLanguage,
2797
- targetLanguage,
2798
- translationPrompts
2799
- }) => {
2800
- const { globalPrompt, localeInstructions, protectedPhrases } = translationPrompts, localePrompt = localeInstructions[targetLanguage]?.prompt, glossaryTerms = localeInstructions[targetLanguage]?.glossaryTerms;
2801
- return `# TRANSLATION TASK
2802
-
2803
- Translate this ${sourceLanguage} document into ${targetLanguage}.
2804
-
2805
- ## Guidelines
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
- ) ?? !1;
2943
- if (devLog.debug("Checking if translation is already fresh", {
2944
- ...logContext,
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
- }), devLog.timeEnd("translateDocument"), devLog.groupEnd();
2653
+ });
2951
2654
  return;
2952
2655
  }
2953
- const translationTarget = determineTranslationTarget({
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
- 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");
2978
- return devLog.debug("Translation document created", {
2979
- ...logContext,
2980
- newDocumentId: newDocument._id
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"), 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", {
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)), devLog.info("Starting document set translation", logContext), dispatch({
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: newReleaseId } = await client.releases.create({
2735
+ const { releaseId: releaseId2 } = await client.releases.create({
3042
2736
  metadata: {
3043
2737
  title: releaseTitle,
3044
2738
  description: releaseDescription,
3045
- releaseType: "asap"
2739
+ releaseType: "undecided"
3046
2740
  }
3047
2741
  });
3048
- releaseId = newReleaseId, devLog.info("Content Release created", {
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
- // This enables us to detect documents that might be stale due to unpublished changes
3079
- perspective: "raw"
3080
- }).fetch("*[_id == $id][0]", {
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
- if (devLog.error(
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
- ), releaseId)
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__ */ jsxs(Stack, { space: 2, children: [
3582
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
3583
- job.reasoningProgress && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 3, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3584
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "\u{1F914} GPT-5 Reasoning" }),
3585
- /* @__PURE__ */ jsx(Text, { size: 1, style: { fontStyle: "italic" }, children: job.reasoningProgress })
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: ",