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

Instalação do plug-in Sanity

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),
  ],
})

Configuração do plug-in Sanity

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
    },
  },
})

Configuração do Phrase

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

  • 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

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

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_HERE por 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.

API endpoint do Sanity

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 sanity-plugin-phrase/backend ou usar o manipulador interno diretamente via importar {createInternalHandler} de sanity-plugin-phrase/backend. 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 é documentInternationalizationAdapter, 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 dataTransformers. 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 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 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
  },
})
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.