O plug-in fornece acesso ao conteúdo traduzido do Phrase a partir do Sanity studio.
Apenas traduções em nível de documento são suportadas. Traduções em nível de campo não são suportadas.
Recursos:
-
Prévia em tempo real
As traduções são mantidas sincronizadas para permitir que linguistas e tradutores vejam as alterações da prévia em tempo real.
-
Retraduções inteligentes
O plug-in verifica quais conteúdos foram alterados desde a última tradução e envia apenas essas alterações para o Phrase.
-
Tradução automática de referências
Ao solicitar traduções, os editores podem optar por também traduzir documentos referenciados pelo atual e o plug-in os vinculará automaticamente por idioma de tradução.
-
Esquemas flexíveis
Independentemente da estrutura, o plug-in adapta-se a ela e garante que o conteúdo final traduzido esteja de acordo com os esquemas do Sanity.
-
Fluxos de trabalho do Phrase
Os fluxos de trabalho de tradução no Phrase permanecem os mesmos; não é necessário retreinar ou reconfigurar operações.
A instalação é realizada na linha de comando.
Pressupõe-se que um gerador de sites e o Sanity Studio já estejam configurados. Caso contrário, use um dos modelos Starter fornecidos pelo Sanity.
Instalação
Navegue até o projeto que contém a instância do Sanity Studio e instale o plug-in:
npm install sanity-plugin-phrase # ou pnpm, yarn, bun
Variáveis de ambiente
Antes de configurar o plug-in, as seguintes variáveis de ambiente devem ser configuradas. Criar um arquivo `.env` (ou `.env.local` para Next.js) na raiz do projeto.
Exemplos abaixo são fornecidos para NextJS. Para outros frameworks, consulte a documentação específica deles e observe que o prefixo `NEXT_PUBLIC_` pode precisar ser removido para variáveis públicas.
Importante
Variáveis do lado do servidor (SANITY_WRITE_TOKEN, PHRASE_USER_NAME, PHRASE_PASSWORD) nunca devem ser expostas ao cliente. No Next.js, apenas variáveis com o prefixo NEXT_PUBLIC_ são expostas ao navegador.
# URL base do seu site (usada para links de prévia)
NEXT_PUBLIC_BASE_URL="http://localhost:3000"
# URL onde o manipulador de backend do plug-in estará localizado
NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase"
# Região do datacenter do Phrase ('eu' ou 'us')
NEXT_PUBLIC_PHRASE_REGION="eu"
# Configuração do projeto Sanity
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"
# API token do Sanity com permissões de escrita (apenas servidor)
SANITY_WRITE_TOKEN=""
# Phrase credenciais (apenas servidor)
# Nota: A API do Phrase espera apenas a parte do nome de usuário, NÃO o endereço de e-mail completo
PHRASE_USER_NAME="phraseUsername"
PHRASE_PASSWORD="secretPassword"
Configuração do plug-in
O plug-in é adicionado ao sanity.config.ts com as opções de configuração necessárias:
// sanity.config.ts
import { defineConfig } from 'sanity'
import {
phrasePlugin,
definePhraseOptions,
documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
// Obrigatório: adaptador i18n para internacionalização de documento
i18nAdapter: documentInternationalizationAdapter(),
// Obrigatório: tipos de documento que podem ser traduzidos
translatableTypes: ['page', 'post', 'article'],
// Obrigatório: idioma do texto original (idioma principal)
// Isso deve corresponder ao idioma definido no seu modelo de projeto Phrase
sourceLang: 'en',
// Obrigatório: idiomas de tradução para os quais os usuários podem traduzir
// Usar os mesmos códigos dos seus documentos Sanity
// Esta lista deve corresponder aos idiomas definidos no seu modelo de projeto Phrase
supportedTargetLangs: ['es', 'fr', 'de', 'pt'],
// Obrigatório: URL do endpoint da API do seu backend
apiEndpoint: process.env.NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT!,
// Obrigatório: Região do datacenter do Phrase ('eu' ou 'us')
phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION as 'eu' | 'us',
// Obrigatório: Modelos de projeto do Phrase disponíveis para editores
phraseTemplates: [
{
templateUid: 'YOUR_TEMPLATE_UID_HERE',
etiqueta: 'Default Translation Template',
},
],
// Obrigatório: Gerar URLs de prévia para linguistas
getDocumentPreview: (doc, sanityClient) => {
const publishedId = doc._id.replace('drafts.', '')
return `${process.env.NEXT_PUBLIC_BASE_URL}/api/draft?id=${publishedId}`
},
// Configurações opcionais
// Profundidade máxima para traduzir documentos referenciados (padrão: 3)
maxReferencesDepth: 3,
// Permitir tradução de documentos de rascunho (padrão: false)
translateDrafts: false,
// Transformadores de dados personalizado para tipos de conteúdo especiais
dataTransformers: [],
// Configuração de log para depuração
logger: {
minimumLogLevel: 'info', // 'debug' | 'info' | 'warning' | 'error' | 'fatal'
},
// Ocultar painel de controle do Phrase com base nas funções do usuário
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ... sua configuração existente
plugins: [
phrasePlugin(PHRASE_CONFIG),
// ... outros plug-ins
],
})// sanity.config.(js|ts)
import {
phrasePlugin,
documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
/**
* O adaptador i18n para usar neste plug-in.
* Ele será responsável por buscar e modificar documentos para cada tradução.
*
* Veja abaixo mais informações sobre adaptadores.
*/
i18nAdapter: documentInternationalizationAdapter(),
/**
* Tipos de esquema do Sanity que o plug-in pode traduzir
*/
translatableTypes: ['page', 'post', 'course', 'lesson', 'definition'],
/**
* código do idioma de todos os idiomas para os quais os usuários podem realizar a tradução.
* Deve ser o mesmo armazenado em seus documentos do Sanity e usado pelo seu front-end. O plug-in irá traduzi-lo automaticamente para o formato do Phrase.
*/
supportedTargetLangs: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],
/**
* código do idioma do texto original que será traduzido.
* Deve ser o mesmo armazenado em seus documentos do Sanity e usado pelo seu front-end. O plug-in irá traduzi-lo automaticamente para o formato do Phrase.
*/
sourceLang: 'en',
/**
* Conforme definido nas configurações da sua conta do Phrase
* Ou `eu` ou `us`
*/
phraseRegion: 'us|eu',
/**
* A URL para a API do back-end do seu plug-in configurado.
*
* **Nota:** siga as etapas para configurar o endpoint, descritas abaixo
*/
apiEndpoint: 'https://my-site.com/api/phrase',
/**
* Usado para redirecionar linguistas do painel de controle do Phrase para a prévia front-end de suas traduções.
*/
getDocumentPreview: async (doc, sanityClient) => {
const publishedId = doc._id.replace('drafts.', '')
return `${process.env.NEXT_PUBLIC_FRONT_END_URL}/api/draft?publishedId=${publishedId}`
},
/**
* Modelos de projeto Phrase que seus editores podem usar ao solicitar traduções.
*
* **Nota:** siga as etapas para configurar modelos, descritas abaixo
*/
phraseTemplates: [
{
templateUid: '1jYg0Pc1d8kAHUyM0tgdmt',
label: '[Sanity.io] Modelo padrão',
},
],
/**
* @Opcional
* Caso você queira mostrar ou ocultar o painel de controle do Phrase de acordo com os privilégios do usuário.
*
* Recebe um contexto com o usuário e o documento atuais e deve retornar um booleano.
*/
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ...
plugins: [
// ...
phrasePlugin(PHRASE_CONFIG),
],
})
Injeção de esquema
Para informar ao plug-in quais tipos de documento podem ser traduzidos, passe uma matriz de tipos de documento para a função injectPhraseIntoSchema no arquivo sanity.config.ts:
// sanity.config.ts
import { injectPhraseIntoSchema } from 'sanity-plugin-phrase'
// Lista de tipos de esquema traduzível. Geralmente exportado de um arquivo de índice
// onde quer que seu esquema Sanity esteja localizado
const TRANSLATABLE_SCHEMAS = ['page', 'post', 'course', 'lesson', 'definition']
export default defineConfig({
schema: {
types: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
templates: (prev) =>
prev.filter((template) => !TRANSLATABLE_SCHEMAS.includes(template.id)),
},
plugins: [
// ...
phrasePlugin({
// Suas opções de configuração aqui
}),
],
})
Excluindo PTDs de listas de documento
PTDs (Phrase Translation Documents) são documentos temporários que não devem aparecer em listas de documento normais dentro do Sanity Studio. A constante NOT_PTD fornece um filtro GROQ para este propósito:
// sanity.config.ts
import { NOT_PTD } from 'sanity-plugin-phrase/utils'
export default defineConfig({
// ... other config
plugins: [
structureTool({
structure: (S) =>
S.list()
.title('Content')
.items([
S.listItem()
.title('Posts')
.schemaType('post')
.child(
S.documentList()
.title('Posts')
.filter(`_type == "post" && ${NOT_PTD}`),
),
// ... outros itens
]),
}),
],
})
Ocultando o menu de tradução de PTDs
Ao usar o plug-in document-internationalization, o menu de tradução deve ser ocultado de PTDs. O utilitário isPtdId identifica documentos PTD:
import { isPtdId } from 'sanity-plugin-phrase/utils'
import { DocumentInternationalizationMenu } from '@sanity/document-internationalization'
// Usar a mesma matriz que translatableTypes da sua PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['page', 'post', 'article']
export default defineConfig({
documento: {
unstable_languageFilter: (prev, ctx) => {
const { schemaType, documentId } = ctx
// Exibir menu de tradução apenas para documentos reais, não PTDs
return TRANSLATABLE_TYPES.includes(schemaType) &&
documentId &&
!isPtdId(documentId)
? [...prev, DocumentInternationalizationMenu]
: prev
},
},
})
O plug-in não possui configuração na interface do Phrase, mas o Phrase deve ser configurado para enviar notificações de webhook para o endpoint da API de backend. Isso permite atualizações em tempo real conforme o progresso das traduções.
Criar um webhook
Criar um webhook com estas configurações:
-
URL
A URL para o endpoint da API do plug-in, conforme configurado na opção .
-
Eventos:
-
Trabalhos
-
Trabalho excluído
-
Job assigned
-
Data de entrega do trabalho alterada
-
Tradução do trabalho atualizada
-
-
Projetos
-
Projeto excluído
-
Data de entrega do projeto alterada
-
-
Outros
-
Pré-tradução concluída
-
-
Isso garante que o plug-in seja notificado de quaisquer alterações nos projetos Phrase e possa manter os dados do Sanity sincronizados.
Configuração de modelo(s) de projeto
Importar arquivo JSON
Usar regex para excluir chaves específicas:
(^|.*\/) (_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) /.*)
Esta expressão inclui chaves duplicadas propositalmente para garantir que sejam ignoradas pelo analisador RegEx do Phrase. Certifique-se de que elas estejam duplicadas corretamente.
-
Excluir dados específicos de localização, como o idioma de um determinado documento se estiver usando
@sanity/document-internationalization. -
Incluir quaisquer chaves específicas do projeto que não requerem tradução, como um slug para conteúdo usando o mesmo caminho em todos os idiomas. Substituir
YOUR_IGNORED_KEYS_HEREpor uma lista separada por pipe de chaves para ignorar. -
Context note:
/_sanityContext
Idioma do texto original
Atualmente, este plug-in opera com a premissa de ter um único idioma do texto original. O(s) modelo(s) de projeto deve(m) ter o mesmo texto original que aquele configurado no sourceLanguage do plug-in.
Idiomas de tradução
Certifique-se de que os idiomas escolhidos no Phrase estejam sincronizados com o que está na configuração do plug-in.
Este é o endpoint que o plug-in usa para se comunicar com o Sanity Studio. Ele é usado para autenticar na API do Phrase, receber webhooks e solicitações de usuário do Sanity studio.
Criar um endpoint de API Personalizado no projeto Sanity para lidar com essas solicitações. Uma das maneiras mais fáceis de fazer isso é usar funções serverless por meio de frameworks front-end como NextJS, Remix, SvelteKit ou Nuxt.
Acessar configurar o manipulador com um padrão de Solicitação-Resposta via importar {createRequestHandler} de ou usar o manipulador interno diretamente via importar {createInternalHandler} de . Garanta que as solicitações CORS sejam tratadas corretamente, o estúdio e o endpoint têm origens diferentes.
O diretório app do SeguinteJS não é suportado atualmente, pois ele analisa incorretamente o manipulador de backend como um componente cliente React.
Este exemplo demonstra a criação de um manipulador de rota no caminho APIEndpoint configurado em /api/Phrase usando o roteador de páginas do Seguinte.js:
// app/api/phrase/route.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
importar type { SeguinteApiRequest, SeguinteApiResponse } from 'Seguinte'
importar { Phrase_CONFIG } from 'PhraseConfig'
importar { createInternalHandler } from 'sanity-plug-in-Phrase/backend'
import { writeToken } from '~/lib/sanity.api'
import { cliente } from '~/lib/sanity.cliente'
exportar const maxDuration = 60
exportar const dynamic = 'force-dynamic'
const phraseHandler = createInternalHandler({
PhraseCredentials: {
userName: process.env.Phrase_usuário_NAME || '',
password: process.env.Phrase_PASSWORD || '',
},
sanityClient: client.withConfig({ token: writeToken }),
plug-inOptions: Phrase_CONFIG,
})
exportar default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
res.setHeader('Acessar-Control-Allow-Origin', '*')
res.setHeader('Acessar-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Acessar-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.estado(405).json({ error: 'Método não permitido' })
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 route: https://nextjs.org/docs/pages/building-your-application/routing/api-routes
importar type { SeguinteApiRequest, SeguinteApiResponse } from 'Seguinte'
importar { Phrase_CONFIG } from 'PhraseConfig'
importar { createInternalHandler } from 'sanity-plug-in-Phrase/backend'
import { writeToken } from '~/lib/sanity.api'
import { cliente } from '~/lib/sanity.cliente'
const phraseHandler = createInternalHandler({
PhraseCredentials: {
userName: process.env.Phrase_usuário_NAME || '',
password: process.env.Phrase_PASSWORD || '',
},
sanityClient: client.withConfig({ token: writeToken }),
plug-inOptions: Phrase_CONFIG,
})
exportar default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
res.setHeader('Acessar-Control-Allow-Origin', '*')
res.setHeader('Acessar-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Acessar-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.estado(405).json({ error: 'Método não permitido' })
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)
}
adaptadores de i18n
O Sanity não tem uma abordagem prescritiva para a internacionalização, e existem muitas maneiras de implementá-la. Este plug-in usa um padrão de adaptador para permitir a configuração com base em como o conteúdo é estruturado e como ele deve ser traduzido.
Atualmente, o único adaptador disponível é , que é o usado pelo plug-in oficial de internacionalização de documento do Sanity document-internationalization plugin (versão ^2.0.0). Abra um problema se um adaptador específico for necessário ou consulte este repositório package/src/adapters/document-internationalization.ts para obter um exemplo de como implementar um personalizado.
Transformadores de dados personalizado
Se for necessário transformar dados antes de enviá-los para o Phrase, usar a opção . Isso é útil se estiver alterando a estrutura dos dados, ou se estiver excluindo certos campos de serem traduzidos.
Cada transformador de dados precisa codificar os dados antes de enviá-los para o Phrase; e decodificá-los ao recebê-los de volta para transformá-los antes de gravar no Sanity. Múltiplos transformadores podem ser empilhados e executados sequencialmente.
O plug-in não oferece nenhuma maneira de testar transformadores isoladamente, portanto o desenvolvimento pode ser complexo. Gravar documentos de tradução reais do conjunto de dados do Sanity para .JSON e usá-los como dados de teste para cada função de codificação/decodificação.
Exemplo de modificação de arquivos VTT codificados em JSON para HTML para que o Phrase possa segmentar melhor o conteúdo das legendas:
import { DataTransformer } from 'sanity-plugin-phrase'
const vttJsonTransformer: DataTransformer = {
encode: {
array(arr) {
// Verificar se o array contém nós de legenda 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 // Retornar undefined para pular a transformação
},
},
decode: {
object(obj) {
if (!!obj && '_type' in obj && obj._type === 'encodedSubtitles') {
return decodeSubtitles(obj as EncodedSubtitles)
}
return undefined
},
},
}
exportar const PHRASE_CONFIG = definePhraseOptions({
// ...
dataTransformers: [vttJsonTransformer],
})exportar 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
},
},
}
// Pulando implementação
// Consulte /demo-nextjs/src/utils/vttJsonTransformer.ts para o texto original completo
declare function decodeSubtitles(
encoded: EncodedSubtitles,
): StoredSubtitleNode[]
declare function encodeSubtitles(nodes: StoredSubtitleNode[]): EncodedSubtitles
Limitando o acesso do editor
Quais editores podem acessar o painel de controle do Phrase podem ser limitados implementando a opção i. Esta função é equivalente àquela passada para a propriedade hidden de um campo no Sanity. Ela recebe um contexto com o usuário e documento atuais e deve retornar um booleano.
Exemplo de limitação de acesso ao painel de controle do Phrase para usuários com a função admin:
const PHRASE_CONFIG = definePhraseOptions({
// ...
isPhraseDashboardHidden: (context) => {
const isAdmin = (context.currentUser.roles || []).some(
(r) => r.name === 'admin',
)
// Ocultar se não for um admin
return !isAdmin
},
})