@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 +62 -15
- package/dist/_chunks-es/retry.js +142 -0
- package/dist/_chunks-es/retry.js.map +1 -0
- package/dist/index.d.ts +67 -10
- package/dist/index.js +210 -466
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +87 -0
- package/dist/server.js +144 -0
- package/dist/server.js.map +1 -0
- package/package.json +42 -31
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 (
|
|
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
|
|
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
|
-
|
|
216
|
+
stackId: "ST-xxxxxxxx", // blueprint stack that contains the function (required)
|
|
217
|
+
functionName: "ai-translation",
|
|
214
218
|
model: "gpt-5",
|
|
215
|
-
reasoningEffort: "medium", // '
|
|
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
|
|
225
|
-
-
|
|
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
|
|
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
|
-
```
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
To disable:
|
|
486
|
+
```sh
|
|
487
|
+
# Standalone Sanity Studio
|
|
488
|
+
SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING=true
|
|
448
489
|
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|