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

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  A Sanity Studio plugin that adds an AI-powered **Translate** document action for internationalized content.
4
4
 
5
5
  - **Default provider:** Sanity Agent Actions (`client.agent.action.translate()`)
6
- - **Optional provider:** OpenAI GPT-5 (direct API) for large documents / long style guides
6
+ - **Optional provider:** OpenAI GPT-5 (through a Sanity function) for large documents / long style guides
7
7
  - **Output:** Translations are created as **versioned documents inside Content Releases** (staged publishing)
8
8
  - **Extras:** translate referenced documents, glossary terms, protected phrases, debug logging, field exclusion, and field transforms (Portable Text + captions)
9
9
 
@@ -64,7 +64,7 @@ Your Studio must already support internationalized documents.
64
64
 
65
65
  Required:
66
66
 
67
- - **Sanity Studio v4** (plugin is built for Sanity Studio v4)
67
+ - **Sanity Studio v6.12 or newer** with `@sanity/ui` v4, React 19.2+, and Node.js 22.12+ (the plugin invokes Sanity Functions through Studio's client, which needs `@sanity/client` 8.3+; Studio 6.12 is the first release that bundles it)
68
68
  - `@sanity/document-internationalization` configured
69
69
  - `sanity-plugin-internationalized-array` installed and configured (used for glossary term translations)
70
70
  - `@sanity/client` **7.1.0+** (required for Agent Actions)
@@ -159,6 +159,7 @@ export default defineConfig({
159
159
 
160
160
  - `schemaId` is usually `'_.schemas.default'` for most projects.
161
161
  - `languageFieldName` must match the field your documents use (often `language`).
162
+ - `glossaryDataset` (optional) reads glossary terms from another dataset in the same project. See [`translationGlossaryTerm`](#translationglossaryterm).
162
163
 
163
164
  ### 3) Add translation history metadata
164
165
 
@@ -199,6 +200,8 @@ If Agent Actions fail with schema-related errors, re-check:
199
200
 
200
201
  OpenAI mode is optional. If enabled, users can choose **OpenAI** as the translation provider inside the translation dialog.
201
202
 
203
+ Studio never calls OpenAI or holds an OpenAI API key. It sends the assembled prompt to a deployed Sanity function with `client.functions.invoke(..., { sync: true })`, using the editor's Studio session. The function makes the OpenAI call with the [server entry](#server-entry) and returns the translated text.
204
+
202
205
  Example configuration:
203
206
 
204
207
  ```typescript
@@ -210,19 +213,25 @@ aiTranslationPlugin({
210
213
 
211
214
  openai: {
212
215
  enabled: true,
213
- apiKey: process.env.OPENAI_API_KEY!,
216
+ stackId: "ST-xxxxxxxx", // blueprint stack that contains the function (required)
217
+ functionName: "ai-translation",
214
218
  model: "gpt-5",
215
- reasoningEffort: "medium", // 'minimal' | 'low' | 'medium' | 'high'
216
- verbosity: "medium", // 'low' | 'medium' | 'high' (if supported by model)
219
+ reasoningEffort: "medium", // 'low' | 'medium' | 'high'
217
220
  defaultMode: "openai-only", // 'sanity-only' | 'openai-only'
218
221
  },
219
222
  });
220
223
  ```
221
224
 
225
+ **Requirements**
226
+
227
+ - Studio's Sanity client must be `@sanity/client` 8.1 or newer, which adds `client.functions.invoke`. If it is older, the dialog shows OpenAI mode as unavailable.
228
+ - A pubsub function with the configured name must be deployed in the stack. It reads `OPENAI_API_KEY` from its own environment, allowlists models, and returns the result of `requestOpenAICompletion`.
229
+ - The function timeout must be long enough for a reasoning call. Use at least 120 seconds; Sanity allows up to 900. The plugin waits up to 900 seconds for the invocation.
230
+
222
231
  **Security**
223
232
 
224
- - Do not hardcode API keys.
225
- - Use environment variables (e.g. `.env` / `.env.local`) and your deployment platform’s secret manager.
233
+ - Do not put an OpenAI API key in the Studio config or bundle. It belongs only in the function environment.
234
+ - The payload sent to the function contains only the messages, model, and reasoning effort. It is client-supplied, so the function must validate it and must not treat any field in it as an identity check.
226
235
 
227
236
  **Why use OpenAI mode?**
228
237
 
@@ -234,6 +243,27 @@ aiTranslationPlugin({
234
243
 
235
244
  - OpenAI calls incur API costs based on token usage.
236
245
 
246
+ ### Server entry
247
+
248
+ The plugin exposes a server-only entry at `@braze/sanity-plugin-ai-translation/server` for running the OpenAI call outside the browser (for example in a Sanity function). It does not load React or Studio, it is not part of the Studio bundle, and it does not read the API key from the environment, so the caller passes it in.
249
+
250
+ ```typescript
251
+ import { requestOpenAICompletion } from "@braze/sanity-plugin-ai-translation/server";
252
+
253
+ // payload: { model, messages, reasoningEffort?, temperature? } assembled in Studio
254
+ const { content, tokensUsed } = await requestOpenAICompletion(payload, {
255
+ apiKey: process.env.OPENAI_API_KEY!,
256
+ });
257
+ ```
258
+
259
+ The first argument is the client-supplied request and is validated at runtime. The second holds server-side configuration such as the API key, and is never read from the payload.
260
+
261
+ - Requests with more than 20 messages, or more than 1,000,000 characters of message content in total, are rejected before anything is sent to OpenAI.
262
+ - Only `role` and `content` are sent for each message. Any other fields in the payload are dropped.
263
+ - Logs never include prompt or document text, even with plugin logging turned on. They include only the model, message roles, and content lengths.
264
+
265
+ Serialization, prompt assembly, structure validation, and the Content Release write stay in Studio. The entry does not allowlist models. Do that in the caller before invoking it.
266
+
237
267
  ---
238
268
 
239
269
  ## User guide (for editors)
@@ -353,6 +383,19 @@ Conceptual example:
353
383
 
354
384
  Glossary terms are used to build locale-specific translation instructions.
355
385
 
386
+ By default, glossary terms are read from the dataset of the document being translated. To share one glossary across datasets, set `glossaryDataset` to another dataset in the same project:
387
+
388
+ ```typescript
389
+ aiTranslationPlugin({
390
+ // ...
391
+ glossaryDataset: "production",
392
+ });
393
+ ```
394
+
395
+ - Terms are read with the editor's existing Studio session, so editors need read access to that dataset.
396
+ - Only glossary terms come from `glossaryDataset`. `translationSettings` (global prompt, locale prompts, protected phrases) is still read from the document's dataset.
397
+ - Nothing is written to either dataset's glossary.
398
+
356
399
  ---
357
400
 
358
401
  ## Advanced configuration
@@ -438,18 +481,18 @@ aiTranslationPlugin({
438
481
 
439
482
  ### Enable debug logging
440
483
 
441
- This plugin uses a centralized debug logger. To enable debug output in your browser:
484
+ This plugin uses a centralized debug logger. Output is off by default. To turn it on, set this environment variable in the host Studio, then restart the dev server or rebuild (the value is inlined at build time):
442
485
 
443
- ```js
444
- localStorage.setItem("sanity-ai-translation:debug", "true");
445
- ```
446
-
447
- To disable:
486
+ ```sh
487
+ # Standalone Sanity Studio
488
+ SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING=true
448
489
 
449
- ```js
450
- localStorage.removeItem("sanity-ai-translation:debug");
490
+ # Studio embedded in Next.js
491
+ NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING=true
451
492
  ```
452
493
 
494
+ To turn it off, remove the variable or set it to any other value.
495
+
453
496
  ### Common issues
454
497
 
455
498
  **Agent Actions fail immediately**
@@ -492,6 +535,10 @@ pnpm typecheck
492
535
  pnpm lint
493
536
  ```
494
537
 
538
+ CI (`.github/workflows/ci.yml`) runs `biome ci`, the plugin typecheck, the test suite, and the plugin build on every pull request and on pushes to `main`.
539
+
540
+ To check the [server entry](#server-entry) against the real OpenAI API, run `pnpm check:openai-server` from the repo root or from `package/`. It reads `OPENAI_API_KEY` from the environment or from a `.env` file. See `package/scripts/README.md` for details.
541
+
495
542
  ---
496
543
 
497
544
  ## License
@@ -0,0 +1,142 @@
1
+ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true" || process.env.SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true", devLog = {
2
+ info: (message, context) => {
3
+ isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
4
+ },
5
+ warn: (message, context) => {
6
+ isLoggingEnabled() && console.warn("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
7
+ },
8
+ error: (message, error, context) => {
9
+ isLoggingEnabled() && console.error("[AI Translation]", message, error, context ? JSON.stringify(context, null, 2) : "");
10
+ },
11
+ debug: (message, data, context) => {
12
+ isLoggingEnabled() && console.debug("[AI Translation]", message, data, context ? JSON.stringify(context, null, 2) : "");
13
+ },
14
+ group: (label, context) => {
15
+ isLoggingEnabled() && console.group(`[AI Translation] ${label}`, context ? JSON.stringify(context, null, 2) : "");
16
+ },
17
+ groupEnd: () => {
18
+ isLoggingEnabled() && console.groupEnd();
19
+ },
20
+ time: (label) => {
21
+ isLoggingEnabled() && console.time(`[AI Translation] ${label}`);
22
+ },
23
+ timeEnd: (label) => {
24
+ isLoggingEnabled() && console.timeEnd(`[AI Translation] ${label}`);
25
+ }
26
+ };
27
+ function isNetworkError(error) {
28
+ if (!error) return !1;
29
+ if (error instanceof TypeError) {
30
+ const message = error.message.toLowerCase();
31
+ return message.includes("fetch") || message.includes("network") || message.includes("connection") || message.includes("failed to fetch") || message.includes("networkerror");
32
+ }
33
+ if (error && typeof error == "object" && "code" in error) {
34
+ const code = error.code;
35
+ return ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "ECONNREFUSED", "EHOSTUNREACH"].includes(code);
36
+ }
37
+ if (error && typeof error == "object" && "response" in error) {
38
+ const response = error.response;
39
+ if (response?.status)
40
+ return response.status >= 500 || response.status === 429;
41
+ }
42
+ if (error instanceof Error) {
43
+ const message = error.message.toLowerCase();
44
+ if (/\b(50[0-9]|429)\b/.test(message) || message.includes("rate limit"))
45
+ return !0;
46
+ }
47
+ return !1;
48
+ }
49
+ function isSanityMutationError(error) {
50
+ if (!error) return !1;
51
+ if (error instanceof Error) {
52
+ const message = error.message.toLowerCase();
53
+ return [
54
+ "validation",
55
+ "schema",
56
+ "invalid document",
57
+ "invalid field",
58
+ "type mismatch",
59
+ "reference",
60
+ "patch",
61
+ "mutation",
62
+ "document structure",
63
+ "required field",
64
+ "field type",
65
+ "sanity"
66
+ ].some((pattern) => message.includes(pattern));
67
+ }
68
+ if (error && typeof error == "object") {
69
+ const errorObj = error;
70
+ if (("statusCode" in errorObj || "response" in errorObj || "details" in errorObj) && "statusCode" in errorObj) {
71
+ const statusCode = errorObj.statusCode;
72
+ if (statusCode >= 400 && statusCode < 500 && statusCode !== 429)
73
+ return !0;
74
+ }
75
+ }
76
+ return !1;
77
+ }
78
+ const DEFAULT_RETRY_OPTIONS = {
79
+ maxAttempts: 2,
80
+ initialDelayMs: 0,
81
+ maxDelayMs: 5e3,
82
+ backoffMultiplier: 2,
83
+ retryableErrors: isNetworkError,
84
+ logRetries: !0
85
+ };
86
+ function calculateDelay(attempt, options) {
87
+ const delay = options.initialDelayMs * options.backoffMultiplier ** attempt;
88
+ return Math.min(delay, options.maxDelayMs);
89
+ }
90
+ function sleep(ms) {
91
+ return new Promise((resolve) => setTimeout(resolve, ms));
92
+ }
93
+ async function retryWithBackoff(operation, options = {}) {
94
+ const opts = {
95
+ ...DEFAULT_RETRY_OPTIONS,
96
+ ...options,
97
+ retryableErrors: options.retryableErrors || DEFAULT_RETRY_OPTIONS.retryableErrors
98
+ };
99
+ let lastError;
100
+ const maxAttempts = opts.maxAttempts + 1;
101
+ for (let attempt = 1; attempt <= maxAttempts; attempt++)
102
+ try {
103
+ const result = await operation();
104
+ return attempt > 1 && opts.logRetries && devLog.info(`Operation succeeded after ${attempt} attempts`, {
105
+ ...opts.context,
106
+ attempt,
107
+ totalAttempts: attempt
108
+ }), result;
109
+ } catch (error) {
110
+ if (lastError = error, !opts.retryableErrors(error))
111
+ throw opts.logRetries && devLog.warn("Error is not retryable, aborting retries", {
112
+ ...opts.context,
113
+ attempt,
114
+ error: error instanceof Error ? error.message : String(error)
115
+ }), error;
116
+ if (attempt >= maxAttempts)
117
+ throw opts.logRetries && devLog.error(
118
+ `Operation failed after ${maxAttempts} attempts`,
119
+ error instanceof Error ? error : new Error(String(error)),
120
+ {
121
+ ...opts.context,
122
+ totalAttempts: maxAttempts
123
+ }
124
+ ), error;
125
+ const delay = calculateDelay(attempt - 1, opts);
126
+ opts.logRetries && devLog.warn(`Operation failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {
127
+ ...opts.context,
128
+ attempt,
129
+ totalAttempts: maxAttempts,
130
+ delayMs: delay,
131
+ error: error instanceof Error ? error.message : String(error)
132
+ }), delay > 0 && await sleep(delay);
133
+ }
134
+ throw lastError;
135
+ }
136
+ export {
137
+ devLog,
138
+ isNetworkError,
139
+ isSanityMutationError,
140
+ retryWithBackoff
141
+ };
142
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sources":["../../src/utils/common/devLog.ts","../../src/utils/common/retry.ts"],"sourcesContent":["/**\n * Development logging utility for AI Translation plugin troubleshooting\n * Only logs when SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING environment variable is \"true\"\n */\n\nconst isLoggingEnabled = (): boolean => {\n return (\n process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === 'true' ||\n process.env.SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === 'true'\n )\n}\n\nexport interface DevLogContext {\n component?: string\n action?: string\n documentId?: string\n language?: string\n jobId?: string\n // biome-ignore lint/suspicious/noExplicitAny: not worth the effort\n [key: string]: any\n}\n\nexport const devLog = {\n info: (message: string, context?: DevLogContext) => {\n if (!isLoggingEnabled()) return\n console.info('[AI Translation]', message, context ? JSON.stringify(context, null, 2) : '')\n },\n\n warn: (message: string, context?: DevLogContext) => {\n if (!isLoggingEnabled()) return\n console.warn('[AI Translation]', message, context ? JSON.stringify(context, null, 2) : '')\n },\n\n error: (message: string, error?: Error | unknown, context?: DevLogContext) => {\n if (!isLoggingEnabled()) return\n console.error('[AI Translation]', message, error, context ? JSON.stringify(context, null, 2) : '')\n },\n\n debug: (message: string, data?: unknown, context?: DevLogContext) => {\n if (!isLoggingEnabled()) return\n console.debug('[AI Translation]', message, data, context ? JSON.stringify(context, null, 2) : '')\n },\n\n group: (label: string, context?: DevLogContext) => {\n if (!isLoggingEnabled()) return\n console.group(`[AI Translation] ${label}`, context ? JSON.stringify(context, null, 2) : '')\n },\n\n groupEnd: () => {\n if (!isLoggingEnabled()) return\n console.groupEnd()\n },\n\n time: (label: string) => {\n if (!isLoggingEnabled()) return\n console.time(`[AI Translation] ${label}`)\n },\n\n timeEnd: (label: string) => {\n if (!isLoggingEnabled()) return\n console.timeEnd(`[AI Translation] ${label}`)\n },\n}\n","import { devLog } from './devLog'\n\n/**\n * Error classification utilities for determining retry strategies\n */\n\n/**\n * Checks if an error is a network error that should be retried immediately\n */\nexport function isNetworkError(error: unknown): boolean {\n if (!error) return false\n\n // Check for fetch TypeError (network failures)\n if (error instanceof TypeError) {\n const message = error.message.toLowerCase()\n return (\n message.includes('fetch') ||\n message.includes('network') ||\n message.includes('connection') ||\n message.includes('failed to fetch') ||\n message.includes('networkerror')\n )\n }\n\n // Check for Node.js network error codes\n if (error && typeof error === 'object' && 'code' in error) {\n const code = (error as { code: string }).code\n return ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED', 'EHOSTUNREACH'].includes(code)\n }\n\n // Check for HTTP error responses\n if (error && typeof error === 'object' && 'response' in error) {\n const response = (error as { response: { status?: number } }).response\n if (response?.status) {\n // Retry on 5xx server errors and 429 rate limiting\n return response.status >= 500 || response.status === 429\n }\n }\n\n // Check for HTTP status in error message\n if (error instanceof Error) {\n const message = error.message.toLowerCase()\n // Check for 5xx or 429 in error message\n if (/\\b(50[0-9]|429)\\b/.test(message)) {\n return true\n }\n // Check for rate limit error messages\n if (message.includes('rate limit')) {\n return true\n }\n }\n\n return false\n}\n\n/**\n * Checks if an error is a Sanity API mutation error\n * These errors typically indicate invalid document structure or schema violations\n */\nexport function isSanityMutationError(error: unknown): boolean {\n if (!error) return false\n\n if (error instanceof Error) {\n const message = error.message.toLowerCase()\n\n // Common Sanity error patterns\n const sanityErrorPatterns = [\n 'validation',\n 'schema',\n 'invalid document',\n 'invalid field',\n 'type mismatch',\n 'reference',\n 'patch',\n 'mutation',\n 'document structure',\n 'required field',\n 'field type',\n 'sanity',\n ]\n\n return sanityErrorPatterns.some((pattern) => message.includes(pattern))\n }\n\n // Check for Sanity-specific error structure\n if (error && typeof error === 'object') {\n const errorObj = error as Record<string, unknown>\n // Sanity errors often have these properties\n if ('statusCode' in errorObj || 'response' in errorObj || 'details' in errorObj) {\n // Check if it's a client error (4xx) that's not a network issue\n if ('statusCode' in errorObj) {\n const statusCode = errorObj.statusCode as number\n if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) {\n return true\n }\n }\n }\n }\n\n return false\n}\n\n/**\n * Checks if an error is an OpenAI API error\n */\nexport function isOpenAIError(error: unknown): boolean {\n if (!error) return false\n\n if (error instanceof Error) {\n const message = error.message.toLowerCase()\n return message.includes('openai') || message.includes('gpt-5 api error') || message.includes('api error')\n }\n\n return false\n}\n\n/**\n * Configuration options for retry logic\n */\nexport interface RetryOptions {\n /** Maximum number of retry attempts (default: 2) */\n maxAttempts?: number\n /** Initial delay in milliseconds before first retry (default: 0 for immediate) */\n initialDelayMs?: number\n /** Maximum delay in milliseconds (default: 5000) */\n maxDelayMs?: number\n /** Backoff multiplier (default: 2) */\n backoffMultiplier?: number\n /** Custom function to determine if error is retryable (default: isNetworkError) */\n retryableErrors?: (error: unknown) => boolean\n /** Context for logging */\n context?: Record<string, unknown>\n /** Whether to log retry attempts */\n logRetries?: boolean\n}\n\n/**\n * Default retry options\n */\nconst DEFAULT_RETRY_OPTIONS: Required<Omit<RetryOptions, 'context' | 'retryableErrors'>> & {\n retryableErrors: (error: unknown) => boolean\n} = {\n maxAttempts: 2,\n initialDelayMs: 0,\n maxDelayMs: 5000,\n backoffMultiplier: 2,\n retryableErrors: isNetworkError,\n logRetries: true,\n}\n\n/**\n * Calculates delay for exponential backoff\n */\nfunction calculateDelay(\n attempt: number,\n options: Required<Omit<RetryOptions, 'context' | 'retryableErrors'>> & {\n retryableErrors: (error: unknown) => boolean\n },\n): number {\n const delay = options.initialDelayMs * options.backoffMultiplier ** attempt\n return Math.min(delay, options.maxDelayMs)\n}\n\n/**\n * Waits for the specified number of milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Retries an operation with exponential backoff\n *\n * @param operation - The async operation to retry\n * @param options - Retry configuration options\n * @returns The result of the operation\n * @throws The last error if all retries are exhausted\n *\n * @example\n * ```typescript\n * const result = await retryWithBackoff(\n * () => fetch('https://api.example.com/data'),\n * {\n * maxAttempts: 3,\n * initialDelayMs: 100,\n * context: { documentId: 'doc123' }\n * }\n * )\n * ```\n */\nexport async function retryWithBackoff<T>(operation: () => Promise<T>, options: RetryOptions = {}): Promise<T> {\n const opts = {\n ...DEFAULT_RETRY_OPTIONS,\n ...options,\n retryableErrors: options.retryableErrors || DEFAULT_RETRY_OPTIONS.retryableErrors,\n }\n\n let lastError: unknown\n const maxAttempts = opts.maxAttempts + 1 // +1 for initial attempt\n\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const result = await operation()\n if (attempt > 1 && opts.logRetries) {\n devLog.info(`Operation succeeded after ${attempt} attempts`, {\n ...opts.context,\n attempt,\n totalAttempts: attempt,\n })\n }\n return result\n } catch (error) {\n lastError = error\n\n // Check if error is retryable\n if (!opts.retryableErrors(error)) {\n if (opts.logRetries) {\n devLog.warn('Error is not retryable, aborting retries', {\n ...opts.context,\n attempt,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n throw error\n }\n\n // If this was the last attempt, throw the error\n if (attempt >= maxAttempts) {\n if (opts.logRetries) {\n devLog.error(\n `Operation failed after ${maxAttempts} attempts`,\n error instanceof Error ? error : new Error(String(error)),\n {\n ...opts.context,\n totalAttempts: maxAttempts,\n },\n )\n }\n throw error\n }\n\n // Calculate delay and wait before retrying\n const delay = calculateDelay(attempt - 1, opts)\n if (opts.logRetries) {\n devLog.warn(`Operation failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {\n ...opts.context,\n attempt,\n totalAttempts: maxAttempts,\n delayMs: delay,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n\n if (delay > 0) {\n await sleep(delay)\n }\n }\n }\n\n // This should never be reached, but TypeScript needs it\n throw lastError\n}\n"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,5 @@
1
- import { ArrayDefinition } from "sanity";
2
- import { FieldDefinitionBase } from "sanity";
3
1
  import { Plugin as Plugin_2 } from "sanity";
4
2
  import type { SanityDocument } from "sanity";
5
- import { WidenInitialValue } from "sanity";
6
- import { WidenValidation } from "sanity";
7
3
 
8
4
  /**
9
5
  * AI translation plugin for Sanity Studio
@@ -304,8 +300,19 @@ export declare interface PluginConfig {
304
300
  schemaId: string;
305
301
  maxConcurrentTranslationJobs?: number;
306
302
  maxReferenceDepth?: number;
303
+ /**
304
+ * OpenAI mode. Studio never calls OpenAI directly: it invokes a deployed Sanity function
305
+ * (which holds the API key) with the editor's session via `client.functions.invoke`.
306
+ * Requires `@sanity/client` 8.1 or newer as Studio's client.
307
+ */
307
308
  openai?: {
308
- apiKey: string;
309
+ /** Blueprint stack that contains the translation function, e.g. `ST-xxxxxxxxxx`. Required for OpenAI mode. */
310
+ stackId: string;
311
+ /**
312
+ * Name of the translation function, as declared in the blueprint.
313
+ * @defaultValue 'ai-translation'
314
+ */
315
+ functionName?: string;
309
316
  model?:
310
317
  | "gpt-5.1"
311
318
  | "gpt-5"
@@ -351,6 +358,17 @@ export declare interface PluginConfig {
351
358
  * ```
352
359
  */
353
360
  unsetFields?: UnsetFieldEntry[];
361
+ /**
362
+ * Dataset (in the same project) to read `translationGlossaryTerm` documents from.
363
+ * Defaults to the dataset of the document being translated.
364
+ * Translation settings and locale prompts are always read from the document's dataset.
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * glossaryDataset: 'production'
369
+ * ```
370
+ */
371
+ glossaryDataset?: string;
354
372
  /**
355
373
  * Optional telemetry configuration for monitoring translation operations.
356
374
  * Provides callbacks for lightweight events and detailed error bundles.
@@ -673,12 +691,51 @@ declare type TranslationErrorType =
673
691
  * @public
674
692
  */
675
693
  export declare const translationHistorySchema: {
676
- type: "array";
677
694
  name: "translationHistory";
678
- } & Omit<ArrayDefinition, "preview"> &
679
- FieldDefinitionBase &
680
- WidenValidation &
681
- WidenInitialValue;
695
+ type: "array";
696
+ of: {
697
+ name: string;
698
+ type: "object";
699
+ fields: (
700
+ | {
701
+ name: "language";
702
+ title: string;
703
+ description: string;
704
+ type: "string";
705
+ }
706
+ | {
707
+ name: "sourceId";
708
+ title: string;
709
+ description: string;
710
+ type: "string";
711
+ }
712
+ | {
713
+ name: "sourceRev";
714
+ title: string;
715
+ description: string;
716
+ type: "string";
717
+ }
718
+ | {
719
+ name: "sourceLanguage";
720
+ title: string;
721
+ description: string;
722
+ type: "string";
723
+ }
724
+ | {
725
+ name: "sourceSnapshot";
726
+ title: string;
727
+ description: string;
728
+ type: "text";
729
+ }
730
+ | {
731
+ name: "targetId";
732
+ title: string;
733
+ description: string;
734
+ type: "string";
735
+ }
736
+ )[];
737
+ }[];
738
+ };
682
739
 
683
740
  /**
684
741
  * Event emitted when an individual translation job completes successfully