@braze/sanity-plugin-ai-translation

中文文档
0.4.0发布于 2026/11/05MITTypeScriptES Module近 30 天下载 3166

A Sanity Studio plugin that provides AI translation for internationalized documents.

原始使用文档
文档目录31 节
  1. Table of contents
  2. What this plugin does
  3. Prerequisites
  4. Install
  5. Configure (Studio)
  6. 1) Configure document internationalization
  7. 2) Add the AI translation plugin
  8. 3) Add translation history metadata
  9. 4) Deploy schema for Agent Actions
  10. Optional: OpenAI mode
  11. Models
  12. Server entry
  13. User guide (for editors)
  14. Translate a document
  15. Choose a Content Release
  16. Translation options
  17. Pre-flight checks
  18. Translate referenced documents
  19. What gets created
  20. Content configuration (settings + glossary)
  21. translationSettings
  22. translationGlossaryTerm
  23. Advanced configuration
  24. Exclude fields from translation
  25. Field transforms
  26. Concurrency and depth
  27. Debugging & troubleshooting
  28. Enable debug logging
  29. Common issues
  30. Development (this repo)
  31. License

@braze/sanity-plugin-ai-translation

A Sanity Studio plugin that adds an AI-powered Translate document action for internationalized content.

  • Default provider: Sanity Agent Actions (client.agent.action.translate())
  • Optional provider: OpenAI (through a Sanity function) for large documents / long style guides
  • Output: Translations are created as versioned documents inside Content Releases (staged publishing)
  • Extras: translate referenced documents, glossary terms, protected phrases, debug logging, field exclusion, and field transforms (Portable Text + captions)

For editors: see the standalone user guide at docs/user-guide.md.


Table of contents


What this plugin does

When you open a document in Sanity Studio (for a configured schema type), the plugin adds a Translate action. You can:

  1. Select one or more target languages
  2. Choose:
    • whether to translate from the draft or the published version
    • which Content Release the translations go into (new or existing)
    • whether to translate referenced documents as well (with depth control)
  3. Run translation using:
    • Sanity Agent Actions (default), or
    • OpenAI (optional)

Existing translations are always re-translated in full; there is no partial or diff-only mode.

Translations are written as version documents in a Content Release, so you can review and publish them on your schedule.


Prerequisites

Your Studio must already support internationalized documents.

Required:

  • 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)
  • @sanity/document-internationalization configured
  • sanity-plugin-internationalized-array installed and configured (used for glossary term translations)
  • @sanity/client 7.1.0+ (required for Agent Actions)

Also required for Agent Actions:

  • Your schema must be deployed to Sanity so Agent Actions can access it (see Deploy schema)
  • Important API version requirement: the client must use apiVersion: 'vX' (not date-based versions). Date-based versions can cause Agent Actions to fail.

Install

Use your Studio package manager (examples use pnpm):

pnpm add @braze/sanity-plugin-ai-translation

You’ll also need the prerequisites if you haven’t installed them yet:

pnpm add @sanity/document-internationalization sanity-plugin-internationalized-array
pnpm add @sanity/client@^7.1.0

Configure (Studio)

1) Configure document internationalization

This plugin is designed to work with @sanity/document-internationalization. Configure it first.

You’ll typically define your supported languages as:

const SUPPORTED_LANGUAGES = [
  { id: "en-us", title: "English" },
  { id: "es", title: "Spanish" },
  { id: "fr", title: "French" },
];

Then enable internationalization for the schema types you want to translate:

documentInternationalization({
  supportedLanguages: SUPPORTED_LANGUAGES,
  schemaTypes: ["post", "page"],
  languageField: "language", // your language field name (example)
});

2) Add the AI translation plugin

Add aiTranslationPlugin() after documentInternationalization():

import { defineConfig } from "sanity";
import { documentInternationalization } from "@sanity/document-internationalization";
import { aiTranslationPlugin } from "@braze/sanity-plugin-ai-translation";

const SUPPORTED_LANGUAGES = [
  { id: "en", title: "English" },
  { id: "es", title: "Spanish" },
  { id: "fr", title: "French" },
];

export default defineConfig({
  // ...your config...
  plugins: [
    documentInternationalization({
      supportedLanguages: SUPPORTED_LANGUAGES,
      schemaTypes: ["post", "page"],
      languageField: "language",
    }),

    aiTranslationPlugin({
      supportedLanguages: SUPPORTED_LANGUAGES,
      translatableSchemaTypes: ["post", "page"],
      languageFieldName: "language",
      schemaId: "_.schemas.default",
      maxConcurrentTranslationJobs: 5,
      maxReferenceDepth: 3,
    }),
  ],
});

Notes

  • schemaId is usually '_.schemas.default' for most projects.
  • languageFieldName must match the field your documents use (often language).
  • glossaryDataset (optional) reads glossary terms from another dataset in the same project. See translationGlossaryTerm.

3) Add translation history metadata

This plugin can store translation history via a metadata schema you attach to documentInternationalization.

Add translationHistorySchema:

import { translationHistorySchema } from "@braze/sanity-plugin-ai-translation";

documentInternationalization({
  supportedLanguages: SUPPORTED_LANGUAGES,
  schemaTypes: ["post", "page"],
  languageField: "language",
  metadataFields: [translationHistorySchema],
});

4) Deploy schema for Agent Actions

Agent Actions need access to your deployed schema.

From your Studio project directory:

npx sanity deploy

If Agent Actions fail with schema-related errors, re-check:

  • schema deployment
  • schemaId
  • client apiVersion is set to 'vX' (not date-based)

Optional: OpenAI mode

OpenAI mode is optional. If enabled, users can choose OpenAI as the translation provider inside the translation dialog.

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 and returns the translated text.

Example configuration:

aiTranslationPlugin({
  supportedLanguages: SUPPORTED_LANGUAGES,
  translatableSchemaTypes: ["post", "page"],
  languageFieldName: "language",
  schemaId: "_.schemas.default",

  openai: {
    enabled: true,
    stackId: "ST-xxxxxxxx", // blueprint stack that contains the function (required)
    functionName: "ai-translation",
    model: "gpt-6.1-sol", // one of OPENAI_MODELS; set it explicitly in production
    reasoningEffort: "medium", // 'low' | 'medium' | 'high'
    defaultMode: "openai-only", // 'sanity-only' | 'openai-only'
  },
});

Requirements

  • 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.
  • 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.
  • 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.

Security

  • Do not put an OpenAI API key in the Studio config or bundle. It belongs only in the function environment.
  • 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.

Why use OpenAI mode?

  • Larger effective context window for long documents and long style guides
  • Deterministic serialization/deserialization to preserve structure
  • Strict structure validation to prevent data corruption

Costs

  • OpenAI calls incur API costs based on token usage.
  • The stable parts of every request (global prompt, locale prompt, glossary, protected phrases, instructions) are sent before the document, so OpenAI's cached input pricing applies to them on every translation into the same language.

Models

openai.model must be one of OPENAI_MODELS (exported from the server entry). The list is also the only spend guard, because any Studio user can invoke the translation function: no pro tiers, o-series or 3.5 models are allowed.

ModelIntended use
gpt-6.1-solDefault. Everyday marketing and documentation pages.
gpt-6.1-terraBudget option for bulk runs or low-stakes pages.
gpt-6-lunaDrafts and testing.
gpt-6-astraOpt-in premium for legal or flagship pages.
gpt-5.1, gpt-5, gpt-5-miniPrevious generation, still supported for Studios that already use them.

DEFAULT_OPENAI_MODEL (gpt-6.1-sol) is used when model is omitted. Set model explicitly in production so a plugin upgrade cannot silently change which model, and which price, a Studio uses.

Server entry

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.

import {
  DEFAULT_OPENAI_MODEL,
  isOpenAIModel,
  OPENAI_MODELS,
  requestOpenAICompletion,
} from "@braze/sanity-plugin-ai-translation/server";

// payload: { model, messages, reasoningEffort?, temperature? } assembled in Studio
if (!isOpenAIModel(payload.model)) {
  throw new Error(`model must be one of ${OPENAI_MODELS.join(", ")}`);
}
const { content, tokensUsed } = await requestOpenAICompletion(payload, {
  apiKey: process.env.OPENAI_API_KEY!,
});

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.

  • 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.
  • Only role and content are sent for each message. Any other fields in the payload are dropped.
  • Logs never include prompt or document text, even with plugin logging turned on. They include only the model, message roles, and content lengths.

Serialization, prompt assembly, structure validation, and the Content Release write stay in Studio. The entry does not allowlist models on its own. Do that in the caller before invoking it, with isOpenAIModel (or OPENAI_MODELS): it is the same list the openai.model plugin option accepts, so Studio and the function never disagree about which models are allowed. GPT-5 and GPT-6 models are sent to the Responses API; other model families fall back to Chat Completions.

Pruning translation metadata

The plugin records each translation in translation.metadata as soon as it creates the version in a Content Release. When that release is deleted or the version is discarded, the entry still points at the removed document. pruneTranslationMetadata removes translations and translationHistory entries whose target no longer exists. A target that still exists as a published document, a draft, or a version in any release is kept. So is a target whose release is archived: archiving deletes a release's versions and unarchiving restores them, so their entries must survive.

The plugin already prunes the metadata of every document it is about to translate. To prune as soon as documents are deleted, deploy a delete-event Sanity function. functions/ in this repo is a working example:

// sanity.blueprint.ts
defineDocumentFunction({
  name: "prune-translation-metadata",
  event: {
    on: ["delete"],
    filter: "_type in ['course', 'lesson']", // your translatableSchemaTypes
    includeAllVersions: true,
    projection: "{_id}",
  },
});
// functions/prune-translation-metadata/index.ts
import { pruneTranslationMetadata } from "@braze/sanity-plugin-ai-translation/server";
import { createClient } from "@sanity/client";
import { documentEventHandler } from "@sanity/functions";

export const handler = documentEventHandler(async ({ context, event }) => {
  const client = createClient({ ...context.clientOptions, apiVersion: "vX", useCdn: false });
  await pruneTranslationMetadata({ client, documentIds: [event.data._id] });
});

Pass dryRun: true to get the entries that would be removed without writing anything.

To clean up an existing dataset, run functions/scripts/fix-translation-metadata.mjs. It prunes dangling entries, then merges duplicate metadata documents for the same source into the oldest one. If two duplicates point the same language at different live documents, the script reports that group and doesn't change it. It's a dry run unless you pass --apply. Build the plugin first (pnpm build), then run it while no translations are in progress:

SANITY_PROJECT_ID=... SANITY_DATASET=... SANITY_WRITE_TOKEN=... pnpm --filter ./functions fix-metadata
SANITY_PROJECT_ID=... SANITY_DATASET=... SANITY_WRITE_TOKEN=... pnpm --filter ./functions fix-metadata -- --apply

SANITY_WRITE_TOKEN needs the Editor role or higher on the dataset.


User guide (for editors)

Translate a document

  1. Open a document in Studio whose type is included in translatableSchemaTypes.
  2. Open the document actions menu and click Translate.
  3. Select one or more target languages.
  4. Configure translation options (see below).
  5. Click Translate to run the pre-flight checks, then Translate again to start.

While translation runs, you’ll see job statuses for each target language (and for any referenced documents, if enabled). Each job is labelled with the document's schema preview title, the same label Studio lists use. View opens the finished translation in a new pane with the release pinned, so the header shows the real title and references inside the release resolve. Pinning the release switches the whole Studio to it, not only the new pane: until you switch back to drafts, edits to any document go into that release.

Choose a Content Release

Translations are always created inside a Content Release.

In the dialog you can typically:

  • create a new release, or
  • add the translations to an existing active release

Use releases when you want:

  • staged review
  • coordinated publishing of multiple translations at once

Translation options

The dialog offers:

  • Translate from drafts / Translate from published
    • Which version of the source document (and of its references) is translated.
  • Content Release
    • Create a new release, or pick an existing active release to add the translations to.
  • Translate references
    • Also translate referenced documents and update references to point at the localized versions.
  • Max reference depth
    • How far to traverse reference graphs (depth 0 means no traversal).
  • Provider (only when OpenAI mode is enabled)
    • Sanity Agent Actions or OpenAI.

Existing translations are always re-translated in full. There is no overwrite toggle and no diff-only mode: the release holds the complete new translation, and you review it before publishing.

Pre-flight checks

Clicking Translate first runs pre-flight checks. Translation only starts once they pass:

  • Validation: every document to be translated must pass its schema validation. Documents with errors are listed with a link to open and fix them, then Rescan.
  • Unpublished changes on references: referenced documents with drafts are listed so you can confirm the draft or published source is the one you want.
  • Stale drafts on existing translations: if a translation that is about to be replaced has an unpublished draft, the dialog warns that the draft will be out of date. Publish or discard each draft from the links shown, or click Continue Anyway to proceed regardless.
  • Release conflicts: when adding to an existing release that already contains a version of the same document and language, the dialog shows the conflicting translations and asks you to choose:
    • Cancel to go back and pick another release,
    • Replace All to delete the existing versions in the release and create new ones, or
    • Skip Conflicts to translate only the non-conflicting document/language pairs.

Translate referenced documents

If you enable reference translation:

  • The plugin traverses references (up to the configured depth).
  • It translates referenced documents that are eligible (type is included in translatableSchemaTypes).
  • It updates references in the translated document to point to localized versions.

This is useful for content like:

  • posts referencing authors
  • pages referencing shared sections
  • documents referencing related content

What gets created

For each selected target language, the plugin creates a versioned document inside the selected Content Release.

When references are translated, the references in the translated version are rewritten to the translated targets the way Studio itself links unpublished documents: _weak: true with _strengthenOnPublish: { type }. They become strong references when the release is published, so the release publishes without validation errors, and Studio can render the field outside the release.

You can then:

  • review and edit translated versions
  • publish the release when ready

Content configuration (settings + glossary)

The plugin introduces two document types for configuration:

translationSettings

Create a document with _id == "translation-settings" to configure:

  • Global prompt (applies to all translations)
  • Locale prompts (per-target-language instructions)
  • Protected phrases (never translate these terms; e.g. brand names)

Example shape:

{
  _type: 'translationSettings',
  _id: 'translation-settings',
  globalPrompt: 'Maintain a professional tone. Preserve formatting and links.',
  localePrompts: [
    {_key: 'es', value: 'Use formal Spanish (usted).'},
    {_key: 'fr', value: 'Use formal French.'},
  ],
  protectedPhrases: ['Braze', 'iOS', 'Android', 'SDK']
}

Notes:

  • localePrompts uses _key for the locale id (must match your supportedLanguages[].id).
  • Protected phrases may be filtered to only those that appear in the source document (to keep prompts smaller).

translationGlossaryTerm

Create glossary term documents to enforce consistent translations of key terms.

This schema uses internationalizedArrayString so you can provide one value per locale.

Conceptual example:

{
  _type: 'translationGlossaryTerm',
  translations: [
    {_key: 'en', value: 'dashboard'},
    {_key: 'es', value: 'panel de control'},
    {_key: 'fr', value: 'tableau de bord'},
  ]
}

Glossary terms are used to build locale-specific translation instructions.

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:

aiTranslationPlugin({
  // ...
  glossaryDataset: "production",
});
  • Terms are read with the editor's existing Studio session, so editors need read access to that dataset.
  • Only glossary terms come from glossaryDataset. translationSettings (global prompt, locale prompts, protected phrases) is still read from the document's dataset.
  • Nothing is written to either dataset's glossary.

Advanced configuration

Exclude fields from translation

Use excludeFields to prevent certain fields from being translated (slugs, enums, flags, etc.).

Supports:

  • field name matches anywhere (e.g. 'status')
  • exact paths (e.g. 'seo.title')
  • array paths / wildcards (e.g. 'body[*].imageType')

Example:

aiTranslationPlugin({
  // ...
  excludeFields: ["slug", "status", "seo.canonicalUrl", "body[*].variant"],
});

Important behavior:

  • The plugin will not allow the languageFieldName to be excluded. If you include it, it will be removed to prevent translation errors.
  • Sanity system fields (_id, _type, _key, _rev, _createdAt, _updatedAt, and any other _-prefixed field) never need excluding. Neither provider translates them: Agent Actions reads the document from the dataset by id, and the OpenAI serializer skips _-prefixed keys. Patterns that target them (such as _id or body[*]._key) are ignored with a warning, because stripping them would remove the id and keys the pipeline relies on.

Field transforms

Some fields (like Portable Text or caption formats) benefit from transformation before translation.

Transforms run in this sequence:

  1. Filter excluded fields
  2. Apply transforms (to produce a translatable representation)
  3. Translate (Sanity Agent or OpenAI)
  4. Restore transforms (back into original structure)
  5. Restore excluded fields (except language)
  6. Create version document in the release

Built-in transforms include:

  • richTextTransform for Portable Text
  • vttCaptionTransform for VTT captions
  • wistiaCaptionTransform for Wistia captions (if used by your schema)

Example setup:

import {
  aiTranslationPlugin,
  richTextTransform,
  vttCaptionTransform,
  wistiaCaptionTransform,
} from "@braze/sanity-plugin-ai-translation";

aiTranslationPlugin({
  // ...
  fieldTransforms: [
    {
      ...vttCaptionTransform,
      matcher: { schemaType: "video", fieldName: "captions" },
    },
    {
      ...wistiaCaptionTransform,
      matcher: { schemaType: "videoDoc", fieldName: "wistiaCaptions" },
    },
    richTextTransform,
  ],
});

Order matters: first match wins.

Concurrency and depth

  • maxConcurrentTranslationJobs controls how many translations run at the same time (default behavior is conservative).
  • maxReferenceDepth controls how deeply references are traversed (set to 0 to disable traversal).

Debugging & troubleshooting

Enable debug logging

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):

# Standalone Sanity Studio
SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING=true

# Studio embedded in Next.js
NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING=true

To turn it off, remove the variable or set it to any other value.

Common issues

Agent Actions fail immediately

  • Confirm @sanity/client is 7.1.0+
  • Confirm your client API version is apiVersion: 'vX' (not date-based)
  • Confirm schema is deployed: npx sanity deploy
  • Confirm schemaId matches your deployed schema (commonly '_.schemas.default')

Translation completes but content didn’t change

  • Ensure the document type is included in translatableSchemaTypes
  • Check whether excluded fields or transforms are preventing updates
  • If using OpenAI mode, structure validation can reject translations that modify non-text fields

Reference translation is slow

  • Reduce maxReferenceDepth
  • Reduce maxConcurrentTranslationJobs
  • Ensure you’re not translating large reference graphs unnecessarily

Development (this repo)

This repository is a pnpm monorepo with:

  • package/ — the plugin package
  • demo-nextjs/ — demo Next.js app with embedded Studio

Common commands:

pnpm dev
pnpm build

cd package
pnpm test
pnpm typecheck
pnpm lint

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.

To check the 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.


License

MIT

版本与发布记录

@braze/sanity-plugin-ai-translation 共发布 70 个版本,首次发布于 2025/10/08;过去 12 个月发布了 50 个版本。

版本发布日期距今
0.4.0latest2026/11/053 天前
0.3.22026/10/2712 天前
0.3.12026/10/2415 天前
0.3.02026/10/2217 天前
0.2.872026/03/247 个月前
0.2.862026/03/247 个月前

关于 @braze/sanity-plugin-ai-translation 的常见问题

@braze/sanity-plugin-ai-translation 是什么?

A Sanity Studio plugin that provides AI translation for internationalized documents.

如何安装 @braze/sanity-plugin-ai-translation?

在项目目录运行 npm install @braze/sanity-plugin-ai-translation,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^19.2)、sanity(^6.12.0)、react-dom(^19.2)、@sanity/ui(^4)、typescript(^5)、styled-components(^6.1)、sanity-plugin-internationalized-array(^3.1.5 || ^4 || ^5)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@braze/sanity-plugin-ai-translation 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@braze/sanity-plugin-ai-translation 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@braze/sanity-plugin-ai-translation 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.12。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@braze/sanity-plugin-ai-translation 可以商用吗?

@braze/sanity-plugin-ai-translation 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@braze/sanity-plugin-ai-translation 的下载量有多少?

根据 npm 官方下载统计,@braze/sanity-plugin-ai-translation 在 2026/10/09 – 2026/11/07 这 30 天内被下载 3,166 次,日均约 106 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@braze/sanity-plugin-ai-translation 还在维护吗?

最新版本 0.4.0 发布于 2026/11/05(3 天前);至今共发布 70 个版本,其中过去 12 个月发布了 50 个;首次发布于 2025/10/08。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。