该插件提供对从Sanity工作室内部翻译的Phrase内容的访问。
仅支持文档级翻译。不支持字段级翻译。
特点:
-
实时预览
翻译保持同步,以便语言学家和翻译人员可以实时查看预览更改。
-
智能重新翻译
该插件比较自上次翻译以来内容的变化,并仅将这些变化发送到Phrase。
-
自动引用翻译
在发出翻译时,编辑可以选择翻译当前引用的文档,插件将自动按目标语言链接它们。
-
灵活的架构
无论结构如何,插件都会适应,并确保最终翻译的内容符合Sanity架构。
-
Phrase工作流
Phrase中的翻译工作流保持不变;不需要重新培训或重新配置操作。
安装在命令行中进行。
假设网站生成器和Sanity Studio已经配置好。如果没有,请使用Sanity提供的starter templates之一。
安装
导航到包含Sanity Studio实例的项目,并安装插件:
npm install sanity-plugin-phrase # 或 pnpm, yarn, bun
环境变量
在配置插件之前,必须设置以下环境变量。在项目根目录中创建一个 `.env` 文件(或 `.env.local` 用于 Next.js)。
下面的示例适用于 NextJS。对于其他框架,请参考其特定文档,并注意可能需要删除 `NEXT_PUBLIC_` 前缀以用于公共变量。
重要
服务器端变量 (SANITY_WRITE_TOKEN, PHRASE_USER_NAME, PHRASE_PASSWORD) 不应暴露给客户端。在 Next.js 中,只有以 NEXT_PUBLIC_ 为前缀的变量会暴露给浏览器。
# 您网站的基本 URL(用于预览链接) NEXT_PUBLIC_BASE_URL="http://localhost:3000" # 插件后端处理程序将位于的 URL NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase" # Phrase 数据中心区域('eu' 或 'us') NEXT_PUBLIC_PHRASE_REGION="eu" # Sanity 项目配置 NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id" NEXT_PUBLIC_SANITY_DATASET="production" # 带有写权限的 Sanity API 令牌(仅限服务器端) SANITY_WRITE_TOKEN="" # Phrase 凭据(仅限服务器端) # 注释:Phrase API 仅期望用户名部分,而不是完整的电子邮件地址 PHRASE_USER_NAME="phraseUsername" PHRASE_PASSWORD="secretPassword"
插件配置
插件添加到 sanity.config.ts,并带有所需的配置选项:
// sanity.config.ts
从'sanity'导入{ defineConfig }
从{导入
phrasePlugin,
definePhraseOptions,
documentInternationalizationAdapter,
}从'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
// 必需:文档国际化的i18n适配器
i18nAdapter: documentInternationalizationAdapter(),
// 必需:可以翻译的文档类型
translatableTypes: ['page', 'post', 'article'],
// 必需:源语言(主要语言)
// 这必须与您Phrase项目模板中定义的语言匹配
sourceLang: 'en',
// 必需:用户可以翻译的目标语言
// 使用与您的Sanity文档相同的代码
// 此列表必须与您Phrase项目模板中定义的语言匹配
supportedTargetLangs: ['es', 'fr', 'de', 'pt'],
// 必需:您的后端API端点URL
apiEndpoint: process.env.NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT!,
// 必需:Phrase数据中心区域('eu'或'us')
phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION作为'eu' | 'us',
// 必需:可供编辑者使用的Phrase项目模板
phraseTemplates: [
{
模板Uid:'YOUR_TEMPLATE_UID_HERE',
标签:'默认翻译模板',
},
],
// 必需:为语言学家生成预览URL
getDocumentPreview: (doc, sanityClient) => {
const publishedId = doc._id.replace('drafts.', '')
return `${process.env.NEXT_PUBLIC_BASE_URL}/api/draft?id=${publishedId}`
},
// 可选设置
// 翻译引用文档的最大深度(默认:3)
maxReferencesDepth:3,
// 允许翻译草稿文档(默认: false)
translateDrafts: false,
// 特殊内容类型的自定义数据转换器
数据转换器: [],
// 调试的日志配置
记录器: {
minimumLogLevel: 'info', // 'debug' | 'info' | 'warning' | 'error' | 'fatal'
},
// 根据用户角色隐藏短语仪表盘
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ... 您现有的配置
插件: [
phrasePlugin(PHRASE_CONFIG),
// ... 其他插件
],
})// sanity.config.(js|ts)
从{导入
phrasePlugin,
documentInternationalizationAdapter,
}从'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
/**
* 用于此插件的 i18n 适配器。
* 它将负责获取和修改每种目标语言的文档。
*
* 有关适配器的更多信息,请参见下文。
*/
i18nAdapter: documentInternationalizationAdapter(),
/**
* 插件可以翻译的可翻译的 Sanity 架构类型
*/
translatableTypes: ['页面', '帖子', '课程', '课', '定义'],
/**
* 用户可以翻译到的所有语言的语言代码。
* 应与存储在您的 Sanity 文档中并由您的前端使用的语言代码相同。该插件将自动将其翻译为 Phrase 的格式。
*/
supportedTargetLangs: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],
/**
* 将被翻译的源语言的语言代码。
* 应与存储在您的 Sanity 文档中并由您的前端使用的语言代码相同。该插件将自动将其翻译为 Phrase 的格式。
*/
sourceLang: 'en',
/**
* 按照您 Phrase 账户的设置定义
* 可以是 `eu` 或 `us`
*/
phraseRegion: 'us|eu',
/**
* 您配置的插件后端 API 的 URL。
*
* **注释:** 请按照下面概述的步骤设置端点
*/
apiEndpoint: 'https://my-site.com/api/phrase',
/**
* 用于将语言学家从 Phrase 仪表盘重定向到其翻译的前端预览。
*/
getDocumentPreview: async (doc, sanityClient) => {
const publishedId = doc._id.replace('drafts.', '')
return `${process.env.NEXT_PUBLIC_FRONT_END_URL}/api/draft?publishedId=${publishedId}`
},
/**
* 短语项目模板,您的编辑可以在请求翻译时使用。
*
* **注释:** 请按照下面概述的步骤设置模板
*/
phraseTemplates: [
{
模板Uid:'1jYg0Pc1d8kAHUyM0tgdmt',
标签: '[Sanity.io] 默认模板',
},
],
/**
* @可选
* 如果您想根据用户权限显示或隐藏短语仪表盘。
*
* 接收当前用户和文档的上下文,并必须返回一个布尔值。
*/
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ...
插件: [
// ...
phrasePlugin(PHRASE_CONFIG),
],
})
模式注入
为了告诉插件可以翻译哪些文档类型,请将文档类型数组传递给injectPhraseIntoSchema函数,在sanity.config.ts文件中:
// sanity.config.ts
import { injectPhraseIntoSchema } from 'sanity-plugin-phrase'
// 可翻译模式类型的列表。通常从索引文件导出
// 无论您将 Sanity 模式放在哪里
const 可译模式 = ['页面', '帖子', '课程', '课', '定义']
export default defineConfig({
模式: {
类型: injectPhraseIntoSchema(可译模式, 短语配置),
模板: (prev) =>
prev.filter((template) => !TRANSLATABLE_SCHEMAS.includes(template.id)),
},
插件: [
// ...
phrasePlugin({
// 在这里输入您的配置选项
}),
],
})
排除文档列表中的PTD
PTD(短语翻译文档)是临时文档,不应出现在Sanity Studio中的正常文档列表中。NOT_PTD常量提供了一个GROQ筛选器用于此目的:
// sanity.config.ts
导入 { NOT_PTD } 来自 'sanity-plugin-phrase/utils'
export default defineConfig({
// ... 其他配置
插件: [
structureTool({
结构:(S) =>
S.list()
.title('Content')
.items([
S.listItem()
.title('Posts')
.schemaType('post')
.child(
S.documentList()
.title('Posts')
.filter(`_type == "post" && ${NOT_PTD}`),
),
// ... 其他项目
),
}),
],
})
从PTD中隐藏翻译菜单
使用文档国际化插件时,翻译菜单应从PTD中隐藏。isPtdId工具识别PTD文档:
导入 { isPtdId } 来自 'sanity-plugin-phrase/utils'
import { DocumentInternationalizationMenu } from '@sanity/document-internationalization'
// 使用与PHRASE_CONFIG中的translatableTypes相同的数组
const TRANSLATABLE_TYPES = ['页面', '帖子', '文章']
export default defineConfig({
文档:{
unstable_languageFilter: (prev, ctx) => {
const { schemaType, documentId } = ctx
// 仅为真实文档显示翻译菜单,而不是PTD
返回 TRANSLATABLE_TYPES.includes(schemaType) &&
documentId &&
!isPtdId(documentId)
? [...prev, DocumentInternationalizationMenu]
: prev
},
},
})
该插件在短语用户界面中没有配置,但必须配置短语以向后端API端点发送webhook通知。这使得翻译进程可以实时更新。
创建一个webhook
使用以下设置创建一个webhook:
-
URL
插件的API端点的URL,如在选项中配置的。
-
活动:
-
招贤纳士
-
工作已删除
-
Job assigned
-
工作截止日期已更改
-
工作译文已更新
-
-
项目
-
项目已删除
-
项目截止日期已更改
-
-
其他
-
预翻译完成
-
-
这确保插件能够接收到对短语项目的任何更改通知,并能够保持Sanity数据的同步。
设置项目模板(s)
使用工作流和团队需求所需的属性配置短语项目模板(s)。在订购新翻译时,可以提供一个或多个模板供选择。短语项目模板必须具有特定的JSON导入设置,以使插件正常工作。这些设置控制哪些字段发送给翻译者,哪些字段作为元数据保留。
JSON文件导入
使用正则表达式排除特定的键:
(^|.*\/) (_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|YOUR_IGNORED_KEYS_HERE| (_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|YOUR_IGNORED_KEYS_HERE) /.*)
该表达式故意包含重复的键,以确保它们被短语的正则表达式解析器忽略。确保它们被正确复制。
-
排除特定于本地化的数据,例如在使用
@sanity/document-internationalization时给定文档的语言。 -
包括任何不需要翻译的项目特定键,例如用于内容的slug,跨所有语言使用相同的路径。用一个以管道分隔的要忽略的键的列表替换
YOUR_IGNORED_KEYS_HERE。 -
上下文注释:
/_sanityContext
源语言
目前,此插件的操作假设只有一种源语言。项目模板必须与插件的sourceLanguage中配置的源相同。
目标语言
确保在Phrase中选择的语言与插件的配置中的语言保持同步。
这是插件用于与Sanity Studio通信的端点。它用于对Phrase的API进行身份验证,接收来自Sanity Studio的webhooks和用户请求。
在Sanity项目中创建一个自定义API端点以处理这些请求。最简单的方法之一是通过前端框架(如NextJS、Remix、SvelteKit或Nuxt)使用无服务器函数。
访问配置处理程序,使用请求-响应模式,通过import {createRequestHandler} from backend,或直接使用内部处理程序,通过import {createInternalHandler} from 。确保正确处理CORS请求,工作室和端点具有不同的来源。
NextJS的应用程序目录当前不受支持,因为它错误地将后端处理程序解析为React客户端组件。
此示例演示如何在配置的apiEndpoint路径/api/phrase上创建路由处理程序,使用Next.js页面路由器:
// app/api/phrase/route.ts
// Next.js API路由支持:https://nextjs.org/docs/api-routes/introduction
导入类型 { NextApiRequest, NextApiResponse } 来自 'next'
导入 { PHRASE_CONFIG } 来自 'phraseConfig'
导入 { createInternalHandler } 来自 'sanity-plugin-phrase/backend'
导入 { writeToken } 来自 '~/lib/sanity.api'
导入 { client } 来自 '~/lib/sanity.client'
导出常量 maxDuration = 60
导出常量 dynamic = 'force-dynamic'
const phraseHandler = createInternalHandler({
phraseCredentials: {
userName: process.env.PHRASE_USER_NAME || '',
password: process.env.PHRASE_PASSWORD || '',
},
sanityClient: client.withConfig({ token: writeToken }),
pluginOptions:PHRASE_CONFIG,
})
导出默认异步函数 handler(
req:NextApiRequest,
res:NextApiResponse,
) {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', '*')
if (req.method?.toUpperCase() === 'OPTIONS') {
res.status(200).json({})
return
}
if (
!req.method ||
(req.method.toUpperCase() !== 'POST' && req.method.toUpperCase() !== 'GET')
) {
res.status(405).json({ error:'不允许的方法' })
return
}
const phraseRes = await phraseHandler(
req.method.toUpperCase() === 'POST' ? req.body : req.query,
)
const resBody = await phraseRes.json().catch(() => {})
Array.from(phraseRes.headers.entries()).forEach((value: [string, any]) => {
res.setHeader(value[0], value[1])
})
res.status(phraseRes.status).json(resBody)
}// src/pages/api/phrase.ts
// Next.js API 路由: https://nextjs.org/docs/pages/building-your-application/routing/api-routes
导入类型 { NextApiRequest, NextApiResponse } 来自 'next'
导入 { PHRASE_CONFIG } 来自 'phraseConfig'
导入 { createInternalHandler } 来自 'sanity-plugin-phrase/backend'
导入 { writeToken } 来自 '~/lib/sanity.api'
导入 { client } 来自 '~/lib/sanity.client'
const phraseHandler = createInternalHandler({
phraseCredentials: {
userName: process.env.PHRASE_USER_NAME || '',
password: process.env.PHRASE_PASSWORD || '',
},
sanityClient: client.withConfig({ token: writeToken }),
pluginOptions:PHRASE_CONFIG,
})
导出默认异步函数 handler(
req:NextApiRequest,
res:NextApiResponse,
) {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', '*')
if (req.method?.toUpperCase() === 'OPTIONS') {
res.status(200).json({})
return
}
if (
!req.method ||
(req.method.toUpperCase() !== 'POST' && req.method.toUpperCase() !== 'GET')
) {
res.status(405).json({ error:'不允许的方法' })
return
}
const phraseRes = await phraseHandler(
req.method.toUpperCase() === 'POST' ? req.body : req.query,
)
const resBody = await phraseRes.json().catch(() => {})
Array.from(phraseRes.headers.entries()).forEach((value) => {
res.setHeader(value[0], value[1])
})
res.status(phraseRes.status).json(resBody)
}
i18n 适配器
Sanity 对国际化没有规定的方法,实施它有很多种方式。此插件使用适配器模式,允许根据内容的结构和应如何翻译进行配置。
目前,唯一可用的适配器是 ,这是 Sanity 官方 document-internationalization plugin(版本 ^2.0.0)使用的适配器。如果需要特定适配器,请提交问题,或参考此 repository's package/src/adapters/document-internationalization.ts 以获取如何实现自定义适配器的示例。
自定义数据转换器
如果在将数据发送到 Phrase 之前需要转换数据,请使用 选项。如果需要更改数据的结构,或排除某些字段不被翻译,这将很有用。
每个数据转换器在将数据发送到 Phrase 之前需要对数据进行编码;并在接收数据时解码,以便在保存到 Sanity 之前进行转换。可以堆叠多个转换器并按顺序运行。
该插件没有提供在隔离状态下测试转换器的方法,因此开发可能会很复杂。将 Sanity 数据集中的真实目标文档保存为 .JSON,并将其用作每个编码/解码函数的测试数据。
将 JSON 编码的 VTT 文件修改为 HTML 的示例,以便 Phrase 更好地分段字幕内容:
import { DataTransformer } from 'sanity-plugin-phrase'
const vttJsonTransformer:DataTransformer = {
encode: {
array(arr) {
// 检查数组是否包含 VTT 字幕节点
if (
arr.every(
(item) =>
typeof item === 'object' &&
!!item &&
'_type' in item &&
typeof item._type === 'string' &&
item._type.startsWith('vtt.'),
)
) {
return encodeSubtitles(arr as StoredSubtitleNode[])
}
return undefined // 返回 undefined 以跳过转换
},
},
decode: {
object(obj) {
if (!!obj && '_type' in obj && obj._type === 'encodedSubtitles') {
return decodeSubtitles(obj as EncodedSubtitles)
}
return undefined
},
},
}
export const PHRASE_CONFIG = definePhraseOptions({
// ...
dataTransformers: [vttJsonTransformer],
})export const PHRASE_CONFIG = definePhraseOptions({
// ...
dataTransformers: [vttJsonTransformer],
})
const vttJsonTransformer:DataTransformer = {
encode: {
array(arr) {
if (
arr.every(
(item) =>
typeof item === 'object' &&
!!item &&
'_type' in item &&
typeof item._type === 'string' &&
item._type.startsWith('vtt.'),
)
) {
return encodeSubtitles(arr as StoredSubtitleNode[])
}
return undefined
},
},
decode: {
object(obj) {
if (!!obj && '_type' in obj && obj._type === 'encodedSubtitles') {
return decodeSubtitles(obj as EncodedSubtitles)
}
return undefined
},
},
}
// 跳过实现
// 请参阅 /demo-nextjs/src/utils/vttJsonTransformer.ts 以获取完整源代码
declare function decodeSubtitles(
编码:EncodedSubtitles,
):StoredSubtitleNode[]
声明函数 encodeSubtitles(nodes:StoredSubtitleNode[]):EncodedSubtitles
限制编辑器访问
可以通过实现 i 选项来限制哪些编辑器可以访问 Phrase 仪表盘。此函数等同于传递给 Sanity 中字段的 hidden 属性的函数。它接收一个包含当前用户和文档的上下文,并必须返回一个布尔值。
限制访问 Phrase 仪表盘给具有 admin 角色的用户的示例:
const PHRASE_CONFIG = definePhraseOptions({
// ...
isPhraseDashboardHidden: (context) => {
const isAdmin = (context.currentUser.roles || []).some(
(r) => r.name === 'admin',
)
// 如果不是管理员则隐藏
return !isAdmin
},
})