@braze/sanity-plugin-ai-translation 0.2.87 → 0.3.0

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,7 +1,8 @@
1
- import { getPublishedId, pathToString, useClient, useWorkspace, validateDocument, defineType, defineField, definePlugin } from "sanity";
1
+ import { useClient, getPublishedId, pathToString, useWorkspace, validateDocument, defineType, defineField, definePlugin } from "sanity";
2
2
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
3
  import { WarningOutlineIcon, ErrorOutlineIcon, CheckmarkIcon, SyncIcon, PanelRightIcon, SearchIcon, CircleIcon, ArrowRightIcon, ResetIcon, TranslateIcon } from "@sanity/icons";
4
- import { createContext, useReducer, useRef, useEffect, useContext, useMemo, useState, useCallback } from "react";
4
+ import { createContext, useEffect, useReducer, useRef, useContext, useMemo, useState, useCallback } from "react";
5
+ import { devLog, retryWithBackoff, isNetworkError, isSanityMutationError } from "./_chunks-es/retry.js";
5
6
  import defaults from "lodash.defaults";
6
7
  import { Card, Stack, Flex, Text, Box, Heading, Button, Spinner, Badge, Inline, Radio, Checkbox, Grid, TextInput, Select, Label, Code, Tooltip } from "@sanity/ui";
7
8
  import { IntentLink } from "sanity/router";
@@ -11,59 +12,55 @@ import { uuid } from "@sanity/uuid";
11
12
  import cloneDeep from "lodash.clonedeep";
12
13
  import get from "lodash.get";
13
14
  import set from "lodash.set";
14
- const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true" || process.env.SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true", devLog = {
15
- info: (message, context) => {
16
- isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
17
- },
18
- warn: (message, context) => {
19
- isLoggingEnabled() && console.warn("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
20
- },
21
- error: (message, error, context) => {
22
- isLoggingEnabled() && console.error("[AI Translation]", message, error, context ? JSON.stringify(context, null, 2) : "");
23
- },
24
- debug: (message, data, context) => {
25
- isLoggingEnabled() && console.debug("[AI Translation]", message, data, context ? JSON.stringify(context, null, 2) : "");
26
- },
27
- group: (label, context) => {
28
- isLoggingEnabled() && console.group(`[AI Translation] ${label}`, context ? JSON.stringify(context, null, 2) : "");
29
- },
30
- groupEnd: () => {
31
- isLoggingEnabled() && console.groupEnd();
32
- },
33
- time: (label) => {
34
- isLoggingEnabled() && console.time(`[AI Translation] ${label}`);
35
- },
36
- timeEnd: (label) => {
37
- isLoggingEnabled() && console.timeEnd(`[AI Translation] ${label}`);
38
- }
39
- }, validateOpenAIApiKey = async (apiKey, model = "gpt-5") => {
15
+ const DEFAULT_TRANSLATION_FUNCTION_NAME = "ai-translation", DEFAULT_MODEL = "gpt-5", TRANSLATION_FUNCTION_TIMEOUT_MS = 9e5, isOpenAIConfigured = (openaiConfig) => !!(openaiConfig?.enabled && typeof openaiConfig.stackId == "string" && openaiConfig.stackId.trim() !== ""), supportsFunctionInvoke = (client) => typeof client.functions?.invoke == "function", toNumber = (value) => typeof value == "number" && Number.isFinite(value) ? value : 0, parseResult = (result, functionName) => {
16
+ const value = result ?? {};
17
+ if (typeof value.content != "string")
18
+ throw new Error(`OpenAI translation function "${functionName}" returned no translated content`);
19
+ const tokensUsed = value.tokensUsed ?? {};
20
+ return {
21
+ content: value.content,
22
+ tokensUsed: {
23
+ prompt: toNumber(tokensUsed.prompt),
24
+ completion: toNumber(tokensUsed.completion),
25
+ reasoning: toNumber(tokensUsed.reasoning),
26
+ total: toNumber(tokensUsed.total),
27
+ cached: toNumber(tokensUsed.cached)
28
+ }
29
+ };
30
+ }, invokeTranslationFunction = async ({
31
+ client,
32
+ openaiConfig,
33
+ messages
34
+ }) => {
35
+ if (!isOpenAIConfigured(openaiConfig))
36
+ throw new Error("OpenAI mode requires openai.enabled and openai.stackId");
37
+ if (!supportsFunctionInvoke(client))
38
+ throw new Error("OpenAI mode requires @sanity/client 8.1 or newer in Studio to invoke the translation function");
39
+ const functionName = openaiConfig.functionName || DEFAULT_TRANSLATION_FUNCTION_NAME, payload = {
40
+ model: openaiConfig.model || DEFAULT_MODEL,
41
+ messages,
42
+ reasoningEffort: openaiConfig.reasoningEffort
43
+ };
44
+ devLog.debug("Invoking OpenAI translation function", {
45
+ functionName,
46
+ stackId: openaiConfig.stackId,
47
+ model: payload.model,
48
+ reasoningEffort: payload.reasoningEffort,
49
+ messages: messages.map((m) => ({ role: m.role, contentLength: m.content.length }))
50
+ });
51
+ let result;
40
52
  try {
41
- const response = await fetch("https://api.openai.com/v1/models", {
42
- method: "GET",
43
- headers: {
44
- Authorization: `Bearer ${apiKey}`
45
- }
46
- });
47
- if (!response.ok)
48
- return {
49
- valid: !1,
50
- error: (await response.json()).error?.message || "Invalid API key"
51
- };
52
- const availableModels = (await response.json()).data.map((m) => m.id);
53
- return availableModels.includes(model) ? {
54
- valid: !0,
55
- modelAvailable: availableModels
56
- } : {
57
- valid: !1,
58
- error: `Model ${model} not available. Try: ${availableModels.slice(0, 5).join(", ")}`
59
- };
53
+ result = await client.functions.invoke(
54
+ functionName,
55
+ { event: { data: payload }, stackId: openaiConfig.stackId, timeout: TRANSLATION_FUNCTION_TIMEOUT_MS },
56
+ { sync: !0 }
57
+ );
60
58
  } catch (error) {
61
- return {
62
- valid: !1,
63
- error: error instanceof Error ? error.message : "Network error"
64
- };
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ throw new Error(`OpenAI translation function "${functionName}" failed: ${message}`);
65
61
  }
66
- }, undraftId = (id) => id.replace(/^drafts\./, ""), activeTranslations = /* @__PURE__ */ new Map(), registerActiveTranslation = (documentId, releaseId) => {
62
+ return parseResult(result, functionName);
63
+ }, undraftId = (id) => id.replace(/^drafts\./, ""), draftId = (id) => `drafts.${undraftId(id)}`, activeTranslations = /* @__PURE__ */ new Map(), registerActiveTranslation = (documentId, releaseId) => {
67
64
  const entry = {
68
65
  state: {
69
66
  documentId,
@@ -102,7 +99,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
102
99
  try {
103
100
  subscriber(entry.state);
104
101
  } catch (error) {
105
- console.error("Error in active translation subscriber:", error);
102
+ devLog.error("Error in active translation subscriber", error);
106
103
  }
107
104
  }, forceCleanupActiveTranslation = (documentId) => {
108
105
  activeTranslations.delete(documentId);
@@ -253,7 +250,14 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
253
250
  supportedLanguages: config.supportedLanguages?.map((l) => l.id),
254
251
  component: "TranslationContextProvider"
255
252
  });
256
- const defaultReleaseTitle = generateDefaultReleaseTitle(document), activeTranslation = getActiveTranslation(document._id), initialContext = {
253
+ const defaultReleaseTitle = generateDefaultReleaseTitle(document), client = useClient({ apiVersion: "vX" }), openaiConfigured = isOpenAIConfigured(config?.openai), openaiAvailable = openaiConfigured && supportsFunctionInvoke(client);
254
+ useEffect(() => {
255
+ openaiConfigured && !openaiAvailable && devLog.warn("OpenAI mode is configured but the Studio client cannot invoke functions", {
256
+ requires: "@sanity/client 8.1 or newer",
257
+ component: "TranslationContextProvider"
258
+ });
259
+ }, [openaiConfigured, openaiAvailable]);
260
+ const activeTranslation = getActiveTranslation(document._id), initialContext = {
257
261
  document,
258
262
  config,
259
263
  // If there's an active translation, restore its state
@@ -266,8 +270,8 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
266
270
  selectedReferenceIds: /* @__PURE__ */ new Set(),
267
271
  // Restore jobs from active translation if available
268
272
  translationJobs: activeTranslation?.translationJobs ?? /* @__PURE__ */ new Map(),
269
- translationMode: config?.openai?.defaultMode || "sanity-only",
270
- openaiValidated: !1,
273
+ translationMode: openaiAvailable && config.openai?.defaultMode || "sanity-only",
274
+ openaiValidated: openaiAvailable,
271
275
  releaseConfig: {
272
276
  mode: "new",
273
277
  newReleaseTitle: defaultReleaseTitle
@@ -302,30 +306,6 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
302
306
  unsubscribe();
303
307
  };
304
308
  }, [document._id]), useEffect(() => {
305
- (async () => {
306
- if (config?.openai?.enabled && config?.openai?.apiKey)
307
- try {
308
- const validation = await validateOpenAIApiKey(config.openai.apiKey, config.openai.model || "gpt-5");
309
- dispatch({
310
- type: "SET_OPENAI_VALIDATED",
311
- payload: validation.valid
312
- }), validation.valid ? devLog.info("OpenAI API key validated successfully", {
313
- model: config.openai.model || "gpt-5",
314
- component: "TranslationContextProvider"
315
- }) : devLog.error("OpenAI API key validation failed", {
316
- error: validation.error,
317
- component: "TranslationContextProvider"
318
- });
319
- } catch (error) {
320
- devLog.error("OpenAI validation error", error instanceof Error ? error : new Error(String(error)), {
321
- component: "TranslationContextProvider"
322
- }), dispatch({
323
- type: "SET_OPENAI_VALIDATED",
324
- payload: !1
325
- });
326
- }
327
- })();
328
- }, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), useEffect(() => {
329
309
  const currentRefsKey = context.references.map((ref) => ref._id).sort().join(",");
330
310
  if (currentRefsKey !== lastProcessedRefsRef.current) {
331
311
  if (context.references.length > 0) {
@@ -361,7 +341,12 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
361
341
  "useTranslationContext must be used within a TranslationContextProvider with document and config props"
362
342
  );
363
343
  return context;
364
- }, checkSourceDrafts = async ({
344
+ };
345
+ async function fetchSourceDocument(client, documentId, perspective) {
346
+ const publishedId = undraftId(documentId), [draft, published] = await client.getDocuments([draftId(publishedId), publishedId]);
347
+ return perspective === "published" ? published : draft ?? published;
348
+ }
349
+ const checkSourceDrafts = async ({
365
350
  client,
366
351
  referenceIds
367
352
  }) => {
@@ -449,10 +434,10 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
449
434
  return devLog.debug("No stale drafts found", { sourceDocumentId }), [];
450
435
  const draftMap = new Map(drafts.map((d) => [d._id, d])), results = [];
451
436
  for (const entry of deduped) {
452
- const draftId = `drafts.${entry.targetId}`, draft = draftMap.get(draftId);
437
+ const draftId2 = `drafts.${entry.targetId}`, draft = draftMap.get(draftId2);
453
438
  draft && results.push({
454
439
  targetId: entry.targetId,
455
- draftId,
440
+ draftId: draftId2,
456
441
  _type: draft._type,
457
442
  title: draft.title || draft.name,
458
443
  sourceDocumentId: entry.sourceDocumentId,
@@ -498,7 +483,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
498
483
  translationHistory: []
499
484
  });
500
485
  } catch (error) {
501
- throw console.error(sourceDocument._id, "Error creating translation metadata:", error), error;
486
+ throw devLog.error("Error creating translation metadata", error, { documentId: sourceDocument._id }), error;
502
487
  }
503
488
  }, checkDocumentConflicts = async ({
504
489
  client,
@@ -587,115 +572,6 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
587
572
  }
588
573
  return allConflicts;
589
574
  };
590
- function isNetworkError(error) {
591
- if (!error) return !1;
592
- if (error instanceof TypeError) {
593
- const message = error.message.toLowerCase();
594
- return message.includes("fetch") || message.includes("network") || message.includes("connection") || message.includes("failed to fetch") || message.includes("networkerror");
595
- }
596
- if (error && typeof error == "object" && "code" in error) {
597
- const code = error.code;
598
- return ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "ECONNREFUSED", "EHOSTUNREACH"].includes(code);
599
- }
600
- if (error && typeof error == "object" && "response" in error) {
601
- const response = error.response;
602
- if (response?.status)
603
- return response.status >= 500 || response.status === 429;
604
- }
605
- if (error instanceof Error) {
606
- const message = error.message.toLowerCase();
607
- if (/\b(50[0-9]|429)\b/.test(message) || message.includes("rate limit"))
608
- return !0;
609
- }
610
- return !1;
611
- }
612
- function isSanityMutationError(error) {
613
- if (!error) return !1;
614
- if (error instanceof Error) {
615
- const message = error.message.toLowerCase();
616
- return [
617
- "validation",
618
- "schema",
619
- "invalid document",
620
- "invalid field",
621
- "type mismatch",
622
- "reference",
623
- "patch",
624
- "mutation",
625
- "document structure",
626
- "required field",
627
- "field type",
628
- "sanity"
629
- ].some((pattern) => message.includes(pattern));
630
- }
631
- if (error && typeof error == "object") {
632
- const errorObj = error;
633
- if (("statusCode" in errorObj || "response" in errorObj || "details" in errorObj) && "statusCode" in errorObj) {
634
- const statusCode = errorObj.statusCode;
635
- if (statusCode >= 400 && statusCode < 500 && statusCode !== 429)
636
- return !0;
637
- }
638
- }
639
- return !1;
640
- }
641
- const DEFAULT_RETRY_OPTIONS = {
642
- maxAttempts: 2,
643
- initialDelayMs: 0,
644
- maxDelayMs: 5e3,
645
- backoffMultiplier: 2,
646
- retryableErrors: isNetworkError,
647
- logRetries: !0
648
- };
649
- function calculateDelay(attempt, options) {
650
- const delay = options.initialDelayMs * options.backoffMultiplier ** attempt;
651
- return Math.min(delay, options.maxDelayMs);
652
- }
653
- function sleep(ms) {
654
- return new Promise((resolve) => setTimeout(resolve, ms));
655
- }
656
- async function retryWithBackoff(operation, options = {}) {
657
- const opts = {
658
- ...DEFAULT_RETRY_OPTIONS,
659
- ...options,
660
- retryableErrors: options.retryableErrors || DEFAULT_RETRY_OPTIONS.retryableErrors
661
- };
662
- let lastError;
663
- const maxAttempts = opts.maxAttempts + 1;
664
- for (let attempt = 1; attempt <= maxAttempts; attempt++)
665
- try {
666
- const result = await operation();
667
- return attempt > 1 && opts.logRetries && devLog.info(`Operation succeeded after ${attempt} attempts`, {
668
- ...opts.context,
669
- attempt,
670
- totalAttempts: attempt
671
- }), result;
672
- } catch (error) {
673
- if (lastError = error, !opts.retryableErrors(error))
674
- throw opts.logRetries && devLog.warn("Error is not retryable, aborting retries", {
675
- ...opts.context,
676
- attempt,
677
- error: error instanceof Error ? error.message : String(error)
678
- }), error;
679
- if (attempt >= maxAttempts)
680
- throw opts.logRetries && devLog.error(
681
- `Operation failed after ${maxAttempts} attempts`,
682
- error instanceof Error ? error : new Error(String(error)),
683
- {
684
- ...opts.context,
685
- totalAttempts: maxAttempts
686
- }
687
- ), error;
688
- const delay = calculateDelay(attempt - 1, opts);
689
- opts.logRetries && devLog.warn(`Operation failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {
690
- ...opts.context,
691
- attempt,
692
- totalAttempts: maxAttempts,
693
- delayMs: delay,
694
- error: error instanceof Error ? error.message : String(error)
695
- }), delay > 0 && await sleep(delay);
696
- }
697
- throw lastError;
698
- }
699
575
  class Semaphore {
700
576
  permits;
701
577
  queue = [];
@@ -1662,127 +1538,7 @@ class TransformRegistry {
1662
1538
  }), matches[0];
1663
1539
  }
1664
1540
  }
1665
- const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKey, request) => (request.model || "gpt-5").startsWith("gpt-5") ? await callGPT5ResponsesAPI(apiKey, request) : await callChatCompletionsAPI(apiKey, request), callGPT5ResponsesAPI = async (apiKey, request) => {
1666
- const model = request.model || "gpt-5", requestBody = {
1667
- model: model === "gpt-5" ? "gpt-5.1" : model,
1668
- // Map gpt-5 to gpt-5.1, use others as-is
1669
- input: request.messages
1670
- // Uses 'input' instead of 'messages'
1671
- };
1672
- request.reasoningEffort && (requestBody.reasoning = {
1673
- effort: request.reasoningEffort,
1674
- summary: "auto"
1675
- // Enable automatic reasoning summaries
1676
- }), devLog.debug("OpenAI GPT-5 Request", {
1677
- model: requestBody.model,
1678
- reasoningEffort: request.reasoningEffort,
1679
- messages: request.messages.map((m) => ({
1680
- role: m.role,
1681
- contentLength: m.content.length,
1682
- contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
1683
- }))
1684
- }), devLog.debug("OpenAI GPT-5 Full Prompt", {
1685
- messages: request.messages
1686
- });
1687
- const response = await retryWithBackoff(
1688
- async () => {
1689
- const fetchResponse = await fetch("https://api.openai.com/v1/responses", {
1690
- method: "POST",
1691
- headers: {
1692
- "Content-Type": "application/json",
1693
- Authorization: `Bearer ${apiKey}`
1694
- },
1695
- body: JSON.stringify(requestBody)
1696
- });
1697
- if (!fetchResponse.ok) {
1698
- const errorMessage = `GPT-5 API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
1699
- throw errorObj.response = { status: fetchResponse.status }, errorObj;
1700
- }
1701
- return fetchResponse;
1702
- },
1703
- {
1704
- maxAttempts: 2,
1705
- initialDelayMs: 0,
1706
- retryableErrors: isNetworkError,
1707
- context: { model: requestBody.model, endpoint: "responses" }
1708
- }
1709
- );
1710
- if (!response.ok) {
1711
- const error = await response.json();
1712
- throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
1713
- }
1714
- const result = await response.json(), tokensUsed = {
1715
- prompt: result.usage?.input_tokens || 0,
1716
- completion: result.usage?.output_tokens || 0,
1717
- reasoning: result.usage?.output_tokens_details?.reasoning_tokens || 0,
1718
- total: result.usage?.total_tokens || 0,
1719
- cached: result.usage?.input_tokens_details?.cached_tokens || 0
1720
- };
1721
- let content = "";
1722
- if (result.output && Array.isArray(result.output)) {
1723
- const messageItem = result.output.find((item) => item.type === "message");
1724
- messageItem?.content && Array.isArray(messageItem.content) && (content = messageItem.content.find((item) => item.type === "output_text")?.text || "");
1725
- }
1726
- return content || (content = result.output_text || result.text || ""), {
1727
- content,
1728
- tokensUsed
1729
- };
1730
- }, callChatCompletionsAPI = async (apiKey, request) => {
1731
- const model = request.model || "gpt-4o", requestBody = {
1732
- model,
1733
- messages: request.messages,
1734
- temperature: request.temperature ?? 0.3
1735
- };
1736
- devLog.debug("OpenAI Chat Completions Request", {
1737
- model,
1738
- temperature: request.temperature ?? 0.3,
1739
- messages: request.messages.map((m) => ({
1740
- role: m.role,
1741
- contentLength: m.content.length,
1742
- contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
1743
- }))
1744
- }), devLog.debug("OpenAI Chat Completions Full Prompt", {
1745
- messages: request.messages
1746
- });
1747
- const response = await retryWithBackoff(
1748
- async () => {
1749
- const fetchResponse = await fetch("https://api.openai.com/v1/chat/completions", {
1750
- method: "POST",
1751
- headers: {
1752
- "Content-Type": "application/json",
1753
- Authorization: `Bearer ${apiKey}`
1754
- },
1755
- body: JSON.stringify(requestBody)
1756
- });
1757
- if (!fetchResponse.ok) {
1758
- const errorMessage = `OpenAI API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
1759
- throw errorObj.response = { status: fetchResponse.status }, errorObj;
1760
- }
1761
- return fetchResponse;
1762
- },
1763
- {
1764
- maxAttempts: 2,
1765
- initialDelayMs: 0,
1766
- retryableErrors: isNetworkError,
1767
- context: { model, endpoint: "chat/completions" }
1768
- }
1769
- );
1770
- if (!response.ok) {
1771
- const error = await response.json();
1772
- throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
1773
- }
1774
- const result = await response.json(), usage = result.usage || {}, tokensUsed = {
1775
- prompt: usage.prompt_tokens || 0,
1776
- completion: usage.completion_tokens || 0,
1777
- reasoning: usage.reasoning_tokens || 0,
1778
- total: usage.total_tokens || 0,
1779
- cached: usage.prompt_tokens_details?.cached_tokens || 0
1780
- };
1781
- return {
1782
- content: result.choices[0].message.content || "",
1783
- tokensUsed
1784
- };
1785
- }, deserializeDocument = (originalDoc, reviewedText) => {
1541
+ const globalTransformRegistry = new TransformRegistry(), deserializeDocument = (originalDoc, reviewedText) => {
1786
1542
  devLog.debug("Raw GPT-5 response received", {
1787
1543
  length: reviewedText.length,
1788
1544
  preview: reviewedText.substring(0, 1e3),
@@ -2382,6 +2138,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2382
2138
  }
2383
2139
  }, getOpenAITranslation = async ({
2384
2140
  dispatch,
2141
+ client,
2385
2142
  sourceDocument,
2386
2143
  languageFieldName,
2387
2144
  translationPrompts,
@@ -2410,16 +2167,16 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2410
2167
  targetLanguage,
2411
2168
  translationPrompts,
2412
2169
  errorContext
2413
- }), response = await callGPT5(openaiConfig.apiKey, {
2170
+ }), response = await invokeTranslationFunction({
2171
+ client,
2172
+ openaiConfig,
2414
2173
  messages: [
2415
2174
  {
2416
2175
  role: "developer",
2417
2176
  content: "You are a translation quality reviewer. Review and refine translations while preserving structure."
2418
2177
  },
2419
2178
  { role: "user", content: prompt }
2420
- ],
2421
- model: openaiConfig.model,
2422
- reasoningEffort: openaiConfig.reasoningEffort
2179
+ ]
2423
2180
  });
2424
2181
  openaiResponseContent = response.content, devLog.info("OpenAI translation response received", {
2425
2182
  ...logContext,
@@ -2727,25 +2484,18 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2727
2484
  languageFieldName
2728
2485
  });
2729
2486
  const documentAfterUnset = unsetDocumentFields(sourceDocument, safeUnsetFields), { preserveFieldPatterns } = normalizeUnsetFields(safeUnsetFields);
2730
- let documentWithPreserved = documentAfterUnset;
2487
+ let preservedValues = {};
2731
2488
  if (preserveFieldPatterns.length > 0 && existingTranslationId)
2732
2489
  try {
2733
2490
  const publishedTarget = await client.getDocument(existingTranslationId);
2734
- if (publishedTarget) {
2735
- const { excludedValues: preserveValues } = filterExcludedFields(
2736
- publishedTarget,
2737
- preserveFieldPatterns
2738
- );
2739
- Object.keys(preserveValues).length > 0 && (documentWithPreserved = restoreExcludedFields(documentAfterUnset, preserveValues), devLog.info("Preserved fields from published target during re-translation", {
2740
- documentId: sourceDocument._id,
2741
- existingTranslationId,
2742
- preservedFields: Object.keys(preserveValues)
2743
- }));
2744
- } else
2745
- devLog.debug("Published target not found for preserveExisting fields", {
2746
- documentId: sourceDocument._id,
2747
- existingTranslationId
2748
- });
2491
+ publishedTarget ? (preservedValues = filterExcludedFields(publishedTarget, preserveFieldPatterns).excludedValues, Object.keys(preservedValues).length > 0 && devLog.info("Preserving fields from published target during re-translation", {
2492
+ documentId: sourceDocument._id,
2493
+ existingTranslationId,
2494
+ preservedFields: Object.keys(preservedValues)
2495
+ })) : devLog.debug("Published target not found for preserveExisting fields", {
2496
+ documentId: sourceDocument._id,
2497
+ existingTranslationId
2498
+ });
2749
2499
  } catch (error) {
2750
2500
  devLog.warn("Failed to fetch published target for preserveExisting fields", {
2751
2501
  documentId: sourceDocument._id,
@@ -2764,10 +2514,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2764
2514
  documentType: sourceDocument._type,
2765
2515
  excludeFields: safeExcludeFields,
2766
2516
  excludeFieldsLength: safeExcludeFields?.length ?? 0,
2767
- sourceHasTitle: "title" in documentWithPreserved,
2768
- sourceTitleValue: documentWithPreserved.title
2517
+ sourceHasTitle: "title" in documentAfterUnset,
2518
+ sourceTitleValue: documentAfterUnset.title
2769
2519
  });
2770
- const { filteredDocument, excludedValues } = filterExcludedFields(documentWithPreserved, safeExcludeFields);
2520
+ const { filteredDocument, excludedValues } = filterExcludedFields(documentAfterUnset, safeExcludeFields);
2771
2521
  devLog.info("After filtering excluded fields", {
2772
2522
  documentId: sourceDocument._id,
2773
2523
  filteredHasTitle: "title" in filteredDocument,
@@ -2830,7 +2580,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2830
2580
  }
2831
2581
  }
2832
2582
  case "openai-only": {
2833
- if (!openaiConfig?.enabled || !openaiConfig?.apiKey)
2583
+ if (!isOpenAIConfigured(openaiConfig))
2834
2584
  throw new Error("OpenAI is not configured but openai-only mode was selected");
2835
2585
  setJobStatus(
2836
2586
  dispatch,
@@ -2842,6 +2592,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2842
2592
  );
2843
2593
  const result = await getOpenAITranslation({
2844
2594
  dispatch,
2595
+ client,
2845
2596
  sourceDocument: documentToTranslate,
2846
2597
  targetLanguage,
2847
2598
  languageFieldName,
@@ -2861,32 +2612,39 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2861
2612
  };
2862
2613
  if (translatedDoc = (await performTranslation()).document, !translatedDoc)
2863
2614
  return null;
2864
- let transformedResult;
2865
- Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
2866
- documentId: sourceDocument._id,
2867
- documentType: sourceDocument._type
2868
- }), transformedResult = restoreTransforms({
2869
- translatedDocument: translatedDoc,
2870
- originalStructure,
2871
- registry: globalTransformRegistry,
2872
- schemaType: sourceDocument._type
2873
- })) : transformedResult = translatedDoc;
2874
2615
  const excludedValuesToRestore = { ...excludedValues };
2875
- delete excludedValuesToRestore[languageFieldName], devLog.info("Before restoring excluded fields", {
2876
- documentId: sourceDocument._id,
2877
- excludedFieldCount: Object.keys(excludedValues).length,
2878
- excludedValuesKeys: Object.keys(excludedValues),
2879
- languageFieldExcluded: languageFieldName in excludedValues,
2880
- transformedResultHasTitle: "title" in transformedResult,
2881
- transformedResultTitle: transformedResult.title
2882
- });
2883
- const finalDoc = restoreExcludedFields(transformedResult, excludedValuesToRestore);
2884
- devLog.info("After restoring excluded fields", {
2885
- documentId: sourceDocument._id,
2886
- finalDocHasTitle: "title" in finalDoc,
2887
- finalDocTitle: finalDoc.title
2888
- });
2889
- const targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
2616
+ delete excludedValuesToRestore[languageFieldName];
2617
+ const finalizeDocument = (translated) => {
2618
+ let transformedResult;
2619
+ Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
2620
+ documentId: sourceDocument._id,
2621
+ documentType: sourceDocument._type
2622
+ }), transformedResult = restoreTransforms({
2623
+ translatedDocument: translated,
2624
+ originalStructure,
2625
+ registry: globalTransformRegistry,
2626
+ schemaType: sourceDocument._type
2627
+ })) : transformedResult = translated;
2628
+ const withoutUnsetFields = unsetDocumentFields(transformedResult, safeUnsetFields);
2629
+ devLog.info("Before restoring excluded fields", {
2630
+ documentId: sourceDocument._id,
2631
+ excludedFieldCount: Object.keys(excludedValues).length,
2632
+ excludedValuesKeys: Object.keys(excludedValues),
2633
+ languageFieldExcluded: languageFieldName in excludedValues,
2634
+ transformedResultHasTitle: "title" in withoutUnsetFields,
2635
+ transformedResultTitle: withoutUnsetFields.title
2636
+ });
2637
+ const finalDoc2 = restoreExcludedFields(
2638
+ restoreExcludedFields(withoutUnsetFields, excludedValuesToRestore),
2639
+ preservedValues
2640
+ );
2641
+ return devLog.info("After restoring excluded fields", {
2642
+ documentId: sourceDocument._id,
2643
+ finalDocHasTitle: "title" in finalDoc2,
2644
+ finalDocTitle: finalDoc2.title,
2645
+ preservedFields: Object.keys(preservedValues)
2646
+ }), finalDoc2;
2647
+ }, finalDoc = finalizeDocument(translatedDoc), targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
2890
2648
  ...finalDoc,
2891
2649
  _id: targetDocumentVersionId
2892
2650
  };
@@ -2925,15 +2683,8 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2925
2683
  const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure.`;
2926
2684
  if (translatedDoc = (await performTranslation(errorContext)).document, !translatedDoc)
2927
2685
  throw new Error("Translation retry failed: no document returned");
2928
- let retryTransformedResult;
2929
- Object.keys(originalStructure).length > 0 ? retryTransformedResult = restoreTransforms({
2930
- translatedDocument: translatedDoc,
2931
- originalStructure,
2932
- registry: globalTransformRegistry,
2933
- schemaType: sourceDocument._type
2934
- }) : retryTransformedResult = translatedDoc;
2935
2686
  const retryNewDoc = {
2936
- ...restoreExcludedFields(retryTransformedResult, excludedValuesToRestore),
2687
+ ...finalizeDocument(translatedDoc),
2937
2688
  _id: targetDocumentVersionId
2938
2689
  };
2939
2690
  return await retryWithBackoff(
@@ -3224,9 +2975,11 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3224
2975
  }
3225
2976
  try {
3226
2977
  await translationSemaphore.withPermit(async () => {
3227
- const document = await client.withConfig({
3228
- perspective: sourcePerspective ?? "drafts"
3229
- }).getDocument(documentId);
2978
+ const document = await fetchSourceDocument(
2979
+ client,
2980
+ documentId,
2981
+ sourcePerspective ?? "drafts"
2982
+ );
3230
2983
  if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId) {
3231
2984
  const versionedTargetDocumentId = await translateDocument({
3232
2985
  dispatch,
@@ -3415,12 +3168,8 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3415
3168
  hasRun.current || (hasRun.current = !0, (async () => {
3416
3169
  onResult(documentId, { isValidating: !0, errors: [] });
3417
3170
  try {
3418
- const publishedId = getPublishedId(documentId), draftId = `drafts.${publishedId}`;
3419
- let doc;
3420
- if (perspective === "published" ? doc = await client.fetch("*[_id == $publishedId][0]", { publishedId }) : doc = await client.fetch(
3421
- "*[_id in [$draftId, $publishedId]] | order(_id desc) [0]",
3422
- { draftId, publishedId }
3423
- ), !doc) {
3171
+ const doc = await fetchSourceDocument(client, documentId, perspective);
3172
+ if (!doc) {
3424
3173
  devLog.warn("Document not found for validation", { documentId, perspective }), onResult(documentId, { isValidating: !1, errors: [] });
3425
3174
  return;
3426
3175
  }
@@ -3436,14 +3185,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3436
3185
  component: "DocumentValidator"
3437
3186
  }), onResult(documentId, { isValidating: !1, errors });
3438
3187
  } catch (error) {
3439
- devLog.error(
3440
- "Failed to validate document",
3441
- error instanceof Error ? error : new Error(String(error)),
3442
- { documentId, component: "DocumentValidator" }
3443
- ), onResult(documentId, { isValidating: !1, errors: [] });
3188
+ devLog.error("Failed to validate document", error instanceof Error ? error : new Error(String(error)), {
3189
+ documentId,
3190
+ component: "DocumentValidator"
3191
+ }), onResult(documentId, { isValidating: !1, errors: [] });
3444
3192
  }
3445
3193
  })());
3446
- }, [documentId, documentType, perspective, onResult, client, workspace]), null;
3194
+ }, [documentId, perspective, onResult, client, workspace]), null;
3447
3195
  }, PreflightView = () => {
3448
3196
  const {
3449
3197
  document,
@@ -3605,9 +3353,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3605
3353
  }, []), sourceRefDraftIds = useMemo(
3606
3354
  () => new Set(sourceRefsWithDrafts.map((d) => d.publishedId)),
3607
3355
  [sourceRefsWithDrafts]
3608
- ), isLoading = someStillValidating || isSourceDraftCheckLoading || isDraftCheckLoading || isCheckingConflicts, hasStaleDrafts = staleDrafts.length > 0, draftsDismissed = draftAction === "continue", canStartTranslation = !isLoading && !hasValidationErrors && !conflictContext && (!hasStaleDrafts || draftsDismissed), handleCancelConflict = useCallback(() => {
3609
- setConflictContext(null);
3610
- }, []), handleReplaceConflict = useCallback(() => {
3356
+ ), isLoading = someStillValidating || isSourceDraftCheckLoading || isDraftCheckLoading || isCheckingConflicts, hasStaleDrafts = staleDrafts.length > 0, draftsDismissed = draftAction === "continue", canStartTranslation = !isLoading && !hasValidationErrors && !conflictContext && (!hasStaleDrafts || draftsDismissed), handleReplaceConflict = useCallback(() => {
3611
3357
  conflictContext && startTranslation(!0, conflictContext.releaseId);
3612
3358
  }, [startTranslation, conflictContext]), handleSkipConflicts = useCallback(() => {
3613
3359
  conflictContext && startTranslation(!1, conflictContext.releaseId, conflictContext.conflicts);
@@ -3617,7 +3363,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3617
3363
  {
3618
3364
  conflicts: conflictContext.conflicts,
3619
3365
  totalTranslations,
3620
- onCancel: handleCancelConflict,
3366
+ onCancel: handleBack,
3621
3367
  onReplace: handleReplaceConflict,
3622
3368
  onSkipConflicts: handleSkipConflicts
3623
3369
  }
@@ -3711,14 +3457,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3711
3457
  ] }),
3712
3458
  /* @__PURE__ */ jsx(DocumentOpenerButton, { documentId: draft.targetId, documentType: draft._type })
3713
3459
  ] }, draft.draftId)) }) }) }),
3714
- /* @__PURE__ */ jsx(
3715
- Button,
3716
- {
3717
- mode: "ghost",
3718
- text: "Continue Anyway",
3719
- onClick: () => setDraftAction("continue")
3720
- }
3721
- )
3460
+ /* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Continue Anyway", onClick: () => setDraftAction("continue") })
3722
3461
  ] })
3723
3462
  ] }) }),
3724
3463
  /* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
@@ -4207,12 +3946,12 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
4207
3946
  ] }) });
4208
3947
  }, TranslationModePanel = () => {
4209
3948
  const { config, translationMode, openaiValidated, dispatch } = useTranslationContext();
4210
- if (!config?.openai?.enabled || !config?.openai?.apiKey)
3949
+ if (!isOpenAIConfigured(config?.openai))
4211
3950
  return null;
4212
3951
  if (!openaiValidated)
4213
3952
  return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
4214
- /* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Configuration" }),
4215
- /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Validating API key..." })
3953
+ /* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Unavailable" }),
3954
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "This Studio's Sanity client cannot invoke the translation function. OpenAI mode requires @sanity/client 8.1 or newer." })
4216
3955
  ] }) });
4217
3956
  const handleModeChange = (mode) => {
4218
3957
  dispatch({