Integrações

Sanity (TMS)

O conteúdo de toda a Central de Ajuda é traduzido automaticamente de inglês pelo Phrase Language AI.

O plug-in fornece acesso ao conteúdo traduzido do Phrase dentro do estúdio Sanity.

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évias em tempo real

    As traduções são mantidas em sincronia para permitir que linguistas e tradutores vejam as alterações de prévia em tempo real.

  • Re-traduções inteligentes

    O plug-in compara o que mudou desde a última tradução e envia apenas essas alterações para o Phrase.

  • Tradução automática de referências

    Ao emitir traduções, os editores podem optar por traduzir também documentos referenciados pelo atual e o plug-in os vinculará automaticamente pelo idioma de destino.

  • Esquemas flexíveis

    Não importa a estrutura, o plug-in se adapta a ela e garante que o conteúdo traduzido final 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 re-treinar ou reconfigurar operações.

Instalação do Plug-in Sanity

A instalação é realizada na linha de comando.

Assume-se que um gerador de site e o Sanity Studio já estão configurados. Se não, use um dos modelos starter fornecidos pelo Sanity.

Instalação

Navegue até o projeto contendo 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. Crie um arquivo `.env` (ou `.env.local` para Next.js) na raiz do projeto.

Os exemplos abaixo são dados 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"

# Token da API do Sanity com permissões de escrita (apenas do lado do servidor)
SANITY_WRITE_TOKEN=""

# Credenciais do Phrase (apenas do lado do servidor)
# Nota: A API 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 a sanity.config.ts com as opções de configuração necessárias:

// sanity.config.ts
import { defineConfig } from 'sanity'
importar {
  phrasePlugin,
  definePhraseOptions,
  documentInternationalizationAdapter,
} de 'sanity-plugin-phrase'

const CONFIGURAÇÃO_PHRASE = definirOpçõesPhrase({
  // Obrigatório: adaptador i18n para internacionalização de documentos
  i18nAdapter: documentInternationalizationAdapter(),

  // Obrigatório: Tipos de documentos que podem ser traduzidos
  translatableTypes: ['page', 'post', 'article'],

  // Obrigatório: Idioma de origem (idioma principal)
  // Isso deve corresponder ao idioma definido no seu modelo de projeto Phrase
  sourceLang: 'en',

  // Obrigatório: Idiomas de destino que os usuários podem traduzir
  // Use os mesmos códigos que seus documentos Sanity
  // Esta lista deve corresponder aos idiomas definidos no seu modelo de projeto Phrase
  supportedTargetLangs: ['es', 'fr', 'de', 'pt'],

  // Obrigatório: A URL do seu endpoint da API de 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',
      label: 'Modelo de Tradução Padrão',
    },
  ],

  // 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 em rascunho (padrão: false)
  translateDrafts: false,

  // Transformadores de dados personalizados para tipos de conteúdo especiais
  dataTransformers: [],

  // Configuração de registro para depuração
  logger: {
    minimumLogLevel: 'info', // 'debug' | 'info' | 'warning' | 'error' | 'fatal'
  },

  // Ocultar Phrase painel de controle com base nos papéis 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)
importar {
  phrasePlugin,
  documentInternationalizationAdapter,
} de 'sanity-plugin-phrase'

const CONFIGURAÇÃO_PHRASE = definirOpçõesPhrase({
  /**
   * O adaptador i18n a ser usado para este plug-in.
   * Ele será responsável por buscar e modificar documentos para cada idioma-alvo.
   *
   * Veja abaixo para 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 que os usuários podem traduzir.
   * Deve ser o mesmo que o armazenado em seus documentos do Sanity e usado pelo seu front-end. O plug-in irá automaticamente traduzi-lo para o formato do Phrase.
   */
  supportedTargetLangs: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],

  /**
   * Código do idioma da língua de origem que será traduzida.
   * Deve ser o mesmo que o armazenado em seus documentos do Sanity e usado pelo seu front-end. O plug-in irá automaticamente traduzi-lo para o formato do Phrase.
   */
  sourceLang: 'en',

  /**
   * Conforme definido nas configurações da sua conta do Phrase
   * Pode ser `eu` ou `us`
   */
  phraseRegion: 'us|eu',

  /**
   * A URL para a sua API de backend do plug-in configurado.
   *
   * **Nota:** siga os passos para configurar o endpoint, descritos 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 do Phrase que seus editores podem usar ao solicitar traduções.
   *
   * **Nota:** siga os passos para configurar os modelos, descritos 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 atual e o documento e deve retornar um booleano.
   */
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})

export default defineConfig({
  // ...
  plugins: [
    // ...
    phrasePlugin(PHRASE_CONFIG),
  ],
})

Configuração do Plug-in Sanity

Injeção de esquema

Para informar ao plug-in quais tipos de documentos podem ser traduzidos, passe um array de tipos de documentos 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íveis. Geralmente exportado de um arquivo índice
// onde quer que seu esquema Sanity esteja localizado
const ESQUEMAS_TRADUZÍVEIS = ['página', 'postagem', 'curso', 'aula', 'definição']

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 das listas de documentos

PTDs (Documentos de Tradução de Frases) são documentos temporários que não devem aparecer nas listas normais de documentos dentro do Sanity Studio. A constante NÃO_PTD fornece um filtro GROQ para esse propósito:

// sanity.config.ts
importar { NÃO_PTD } de '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 dos PTDs

Ao usar o plug-in de internacionalização de documentos, o menu de tradução deve ser ocultado dos PTDs. A utilidade éIdPtd identifica documentos PTD:

importar { éIdPtd } de 'sanity-plugin-phrase/utils'
importar { MenuInternacionalizaçãoDocumento } de '@sanity/document-internationalization'

// Use o mesmo array que translatableTypes de sua PHRASE_CONFIG
const TIPOS_TRADUZÍVEIS = ['página', 'postagem', 'artigo']

export default defineConfig({
  documento: {
    unstable_languageFilter: (prev, ctx) => {
      const { schemaType, documentId } = ctx

      // Mostre o menu de tradução apenas para documentos reais, não para PTDs
      return TIPOS_TRADUZÍVEIS.includes(tipoDeEsquema) &&
        documentId &&
        !isPtdId(documentId)
        ? [...prev, DocumentInternationalizationMenu]
        : prev
    },
  },
})

Configuração da Frase

O plug-in não possui configuração na interface da Frase, mas a Frase deve ser configurada para enviar notificações de webhook para o endpoint da API de backend. Isso permite atualizações em tempo real à medida que as traduções progridem.

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

  • Eventos:

    • Trabalhos

      • Trabalho excluído

      • Trabalho atribuído

      • 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 sobre quaisquer alterações nos projetos Phrase e possa manter os dados do Sanity sincronizados.

Configurar modelo(s) de projeto(s)

Configurar modelo(s) de projeto do Phrase com as propriedades necessárias para fluxos de trabalho e requisitos da equipe. Um ou mais modelos podem ser oferecidos para escolha ao solicitar uma nova tradução. Os modelos de projeto do Phrase devem ter configurações específicas de importação JSON para que o plug-in funcione corretamente. Essas configurações controlam quais campos são enviados aos tradutores e quais são preservados como metadados.

Importação de arquivo JSON

Use regex para excluir chaves específicas:

(^|.*\/)
(_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|SUAS_CHAVES_IGNORADAS_AQUI|
(_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|SUAS_CHAVES_IGNORADAS_AQUI)
/.*)

Essa expressão inclui chaves duplicadas de propósito para garantir que sejam ignoradas pelo analisador RegEx do Phrase. Certifique-se de que estão corretamente duplicadas.

  • Excluir dados específicos de localização, como o idioma de um determinado documento se usar @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 SUAS_CHAVES_IGNORADAS_AQUI por uma lista de chaves separadas por pipe para ignorar.

  • Context note: /_sanityContext

Idioma do texto original

Atualmente, este plug-in opera sob a suposição de ter um único idioma de origem. O(s) modelo(s) do projeto devem ter a mesma origem que a configurada no sourceLanguage do plug-in.

Idiomas de destino

Certifique-se de que os idiomas escolhidos no Phrase estejam sincronizados com o que está na configuração do plug-in.

apiEndpoint do Sanity

Este é o endpoint que o plug-in usa para se comunicar com o Sanity Studio. É usado para autenticar na API do Phrase, receber webhooks e solicitações de usuários do Sanity Studio.

Crie 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 através de frameworks front-end como NextJS, Remix, SvelteKit ou Nuxt.

Acesse configure o manipulador com um padrão de Requisição-Resposta via import {createRequestHandler} de sanity-plugin-phrase/backend ou use o manipulador interno diretamente via import {createInternalHandler} de sanity-plugin-phrase/backend. Certifique-se de que as solicitações CORS sejam tratadas corretamente, pois o estúdio e o endpoint têm origens diferentes.

O diretório de aplicativos do NextJS não é atualmente suportado, pois 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 Next.js Pages Router:

// app/api/phrase/route.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
import { PHRASE_CONFIG } from 'phraseConfig'
import { createInternalHandler } from 'sanity-plugin-phrase/backend'

import { writeToken } from '~/lib/sanity.api'
importar { cliente } de '~/lib/sanity.client'

exportar const maxDuration = 60
exportar const 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,
})

exportar default async function 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: '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
import type { NextApiRequest, NextApiResponse } from 'next'
import { PHRASE_CONFIG } from 'phraseConfig'
import { createInternalHandler } from 'sanity-plugin-phrase/backend'
import { writeToken } from '~/lib/sanity.api'
importar { cliente } de '~/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,
})

exportar default async function 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: '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 i18n

A Sanity não possui uma abordagem prescritiva para internacionalização, e existem muitas maneiras de implementá-la. Este plug-in utiliza um padrão de adaptador para permitir a configuração com base em como o conteúdo está estruturado e como deve ser traduzido.

Atualmente, o único adaptador disponível é documentInternationalizationAdapter, que é o utilizado pelo plug-in oficial de document-internationalization da Sanity (versão ^2.0.0). Registre um problema se um adaptador específico for necessário ou consulte este repositório's package/src/adapters/document-internationalization.ts para um exemplo de como implementar um personalizado.

Transformadores de dados personalizados

Se for necessário transformar dados antes de enviá-los para o Phrase, use a dataTransformers opção. Isso é útil se for necessário alterar a estrutura dos dados ou se for necessário excluir 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 salvar na Sanity. Vários transformadores podem ser empilhados e executados sequencialmente.

O plug-in não oferece uma maneira de testar transformadores isoladamente, então o desenvolvimento pode ser complexo. Salve documentos reais de destino do conjunto de dados da Sanity em .JSON e use-os 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) {
      // Verifique 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 indefinido para pular a transformação
    },
  },
  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
    },
  },
}

// Ignorando implementação
// Consulte /demo-nextjs/src/utils/vttJsonTransformer.ts para o código-fonte 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 isPhraseDashboardHidden. 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 o papel de admin:

const CONFIGURAÇÃO_PHRASE = definirOpçõesPhrase({
  // ...
  isPhraseDashboardHidden: (context) => {
    const isAdmin = (context.currentUser.roles || []).some(
      (r) => r.name === 'admin',
    )
    // Ocultar se não for um admin
    return !isAdmin
  },
})
Esse artigo foi útil?

Sorry about that! In what way was it not helpful?

The article didn’t address my problem.
I couldn’t understand the article.
The feature doesn’t do what I need.
Other reason.

Note that feedback is provided anonymously so we aren't able to reply to questions.
If you'd like to ask a question, submit a request to our Support team.
Thank you for your feedback.