Voltar ao blog
A Contentful-style content entry card showing a filled English locale tab and an empty French locale tab with a warning icon.

Por que o seu conteúdo do Contentful ainda não é realmente multilíngue

By Robert

Por que seu conteúdo no Contentful ainda não é realmente multilíngue

O Contentful lida bem com locais. Você pode definir quantos precisar, configurar uma cadeia de fallback, alternar entre eles no editor e construir um frontend que sirva o idioma correto por rota. A infraestrutura para conteúdo multilíngue é realmente boa.

O que o Contentful não faz é traduzir seu conteúdo.

Isso parece óbvio quando dito diretamente, mas é surpreendentemente fácil confundir "temos campos de local configurados" com "somos multilíngues." As duas coisas não são a mesma coisa. Campos de local são um contêiner. Multilíngue significa que o contêiner tem conteúdo dentro.

Se seu espaço no Contentful tem locais em francês e alemão configurados, mas os campos nesses locais estão vazios, ou preenchidos com o texto em inglês como um espaço reservado, ou populados com uma primeira versão aproximada que ninguém revisou desde o lançamento do site — você não é multilíngue. Você tem a estrutura para multilíngue.

Este post é sobre fechar essa lacuna.


O que o Contentful realmente oferece

O sistema de localidade do Contentful é bem projetado. Cada entrada de conteúdo pode ter valores de campo por localidade. Você define uma localidade padrão, configura substituições, e sua API de entrega retorna a localidade correta quando solicitada. A modelagem de conteúdo é flexível o suficiente para lidar com requisitos multilíngues complexos em diferentes tipos de conteúdo.

Mas o sistema é totalmente neutro quanto a como o conteúdo traduzido é inserido. O Contentful não sabe se os campos da sua localidade francesa contêm traduções profissionais, cópia traduzida por máquina, texto em inglês colado por acidente ou nada. Ele apenas armazena e entrega o que você coloca.

O problema da tradução é inteiramente seu para resolver.


Como as equipes geralmente lidam com isso (e onde falha)

A maioria das equipes que lidam com tradução no Contentful segue um de alguns padrões.

O padrão de exportação manual. Um desenvolvedor exporta o conteúdo do Contentful, envia para uma agência de tradução ou freelancer, espera o retorno, reformata e importa. Isso funciona para um lançamento único, mas se torna insustentável à medida que o conteúdo muda. Cada atualização no local de origem significa passar por todo o ciclo novamente. Na prática, o conteúdo traduzido rapidamente fica defasado em relação à fonte e ninguém tem tempo para atualizá-lo.

O padrão de tradução no editor. Um editor abre cada entrada, muda para o local de destino e traduz campo por campo, manualmente ou colando o conteúdo em uma ferramenta de tradução. Isso é preciso, mas lento. Também não escala — se você tem centenas de entradas em uma dúzia de tipos de conteúdo, o volume de trabalho manual é significativo.

O padrão "serve assim". O conteúdo traduzido existe, mas não foi revisado desde que foi produzido pela primeira vez. O local de origem foi atualizado várias vezes desde então. A versão em francês da sua página de preços ainda referencia um plano que você aposentou há oito meses. A página inicial em alemão ainda tem o slogan antigo. Ninguém sinalizou isso porque ninguém verifica.

Todos os três padrões compartilham o mesmo problema raiz: a tradução é tratada como uma tarefa única em vez de uma parte contínua do fluxo de trabalho de conteúdo.


O que o multilíngue realmente exige

Conteúdo multilíngue genuíno no Contentful precisa de três coisas funcionando em conjunto.

Tradução inicial precisa. Cada campo em cada localidade alvo precisa de conteúdo traduzido que seja preciso, adequadamente localizado e que realmente pareça ter sido escrito para aquele mercado, em vez de ter passado por uma ferramenta básica de tradução e ficar sem revisão.

Um processo para manter as traduções atualizadas. Quando o conteúdo fonte muda, o conteúdo traduzido também precisa mudar. Esta é a parte que a maioria das equipes subestima. Uma equipe de conteúdo que publica várias atualizações por semana em um espaço Contentful com quatro localidades e cinquenta tipos de conteúdo está diante de uma carga significativa de trabalho de tradução contínua se isso for feito manualmente.

Uma forma de saber quando as traduções estão desatualizadas. O sistema de localidades do Contentful não indica obsolescência. Se você atualizar a cópia em inglês de uma entrada e esquecer de atualizar a cópia em francês, a versão francesa continuará silenciosamente servindo o conteúdo antigo. Você precisa de um processo ou ferramenta para detectar isso.


Onde o PolyLingo se encaixa

A API do PolyLingo traduz conteúdo estruturado preservando sua estrutura. Para o Contentful, o formato relevante é JSON — os valores dos campos de uma entrada do Contentful, extraídos e enviados para a API, retornam traduzidos com a mesma estrutura intacta.

O fluxo básico em Node.js:

import PolyLingo from 'polylingo'
import { createClient } from 'contentful-management'

const poly = new PolyLingo({ apiKey: process.env.POLYLINGO_API_KEY })
const contentful = createClient({
  accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
})

const space = await contentful.getSpace(process.env.CONTENTFUL_SPACE_ID)
const environment = await space.getEnvironment('master')

// Busque a entrada que você quer traduzir
const entry = await environment.getEntry('your-entry-id')

// PolyLingo usa códigos BCP-47 sem região (fr, de).
// Mapeie-os para os IDs de localidade do Contentful se forem diferentes (ex: fr-FR, de-DE).
const localeMap = { fr: 'fr', de: 'de' } // ajuste para corresponder aos IDs de localidade do seu espaço

// Extraia apenas campos de string em inglês — ignore referências, links, booleanos e números
const sourceFields = Object.fromEntries(
  Object.entries(entry.fields)
    .filter(([, value]) => typeof value['en-US'] === 'string')
    .map(([key, value]) => [key, value['en-US']])
)

// Traduza para francês e alemão em uma única requisição
const result = await poly.translate({
  content: JSON.stringify(sourceFields),
  format: 'json',
  targets: Object.keys(localeMap),
})

// Escreva os valores traduzidos de volta na entrada usando os IDs de localidade do Contentful
for (const [polyLocale, translated] of Object.entries(result.translations)) {
  const contentfulLocale = localeMap[polyLocale]
  const parsed = JSON.parse(translated)
  for (const [field, value] of Object.entries(parsed)) {
    if (!entry.fields[field]) entry.fields[field] = {}
    entry.fields[field][contentfulLocale] = value
  }
}

await entry.update()
await entry.publish()

console.log('Entrada traduzida e publicada.')

Uma chamada de API retorna tanto francês quanto alemão. A entrada é atualizada e publicada no mesmo script.

Uma nota sobre IDs de localidade: PolyLingo usa códigos BCP-47 sem sufixo de região (fr, de). Espaços do Contentful frequentemente usam IDs qualificados por região como fr-FR ou de-DE. O objeto localeMap acima é onde você os alinha — atualize-o para corresponder aos IDs de localidade usados no seu espaço Contentful.

O mesmo padrão funciona em Python:

import os, json, requests
from contentful_management import Client

poly_key = os.environ['POLYLINGO_API_KEY']
contentful = Client(os.environ['CONTENTFUL_MANAGEMENT_TOKEN'])

space = contentful.spaces().find(os.environ['CONTENTFUL_SPACE_ID'])
environment = space.environments().find('master')
entry = environment.entries().find('your-entry-id')

# Extraia valores dos campos em inglês
source_fields = {
    key: value.get('en-US')
    for key, value in entry.fields().items()
    if value.get('en-US')
}

# Traduza para francês e alemão
r = requests.post(
    'https://api.usepolylingo.com/v1/translate',
    headers={'Authorization': f"Bearer {poly_key}"},
    json={
        'content': json.dumps(source_fields),
        'format': 'json',
        'targets': ['fr', 'de'],
    },
    timeout=120,
)
r.raise_for_status()
translations = r.json()['translations']

# Escreva de volta na entrada
for locale, translated in translations.items():
    parsed = json.loads(translated)
    for field, value in parsed.items():
        if field in entry.fields():
            entry.fields()[field][locale] = value

entry.save()
entry.publish()
print('Entrada traduzida e publicada.')

Tradução em escala

Os exemplos acima lidam com uma única entrada. Para um espaço Contentful com muitas entradas em vários tipos de conteúdo, o endpoint batch lida com até 100 itens por solicitação:

// Buscar múltiplas entradas e traduzi-las em uma única chamada batch
const entries = await environment.getEntries({
  content_type: 'blogPost',
  limit: 50,
})
 
// Mapear códigos de localidade PolyLingo para IDs de localidade Contentful
const localeMap = { fr: 'fr', de: 'de', es: 'es' } // ajuste para corresponder ao seu espaço
 
const items = entries.items.map(entry => ({
  id: entry.sys.id,
  content: JSON.stringify(
    Object.fromEntries(
      Object.entries(entry.fields)
        .filter(([, value]) => typeof value['en-US'] === 'string')
        .map(([k, v]) => [k, v['en-US']])
    )
  ),
  format: 'json',
}))
 
const batch = await poly.batch({
  items,
  targets: Object.keys(localeMap),
})
 
for (const result of batch.results) {
  const entry = entries.items.find(e => e.sys.id === result.id)
  for (const [polyLocale, translated] of Object.entries(result.translations)) {
    const contentfulLocale = localeMap[polyLocale]
    const parsed = JSON.parse(translated)
    for (const [field, value] of Object.entries(parsed)) {
      if (!entry.fields[field]) entry.fields[field] = {}
      entry.fields[field][contentfulLocale] = value
    }
  }
  await entry.update()
  await entry.publish()
}

Cinquenta entradas, três idiomas, uma solicitação batch.


Mantendo as traduções atualizadas

Os scripts acima podem ser executados sob demanda ou integrados ao seu fluxo de trabalho de publicação. Um padrão comum é executar a tradução como parte de um trabalho de CI acionado quando o conteúdo fonte muda, ou em uma programação que captura quaisquer entradas atualizadas nas últimas 24 horas.

O Contentful também suporta webhooks — você pode configurar um webhook que dispara quando uma entrada é publicada no idioma padrão, o que pode acionar um trabalho de tradução automaticamente. Integrar o PolyLingo nesse webhook significa que toda vez que um editor publica conteúdo em inglês atualizado, os locais traduzidos são atualizados sem nenhuma etapa manual. Isso está no roteiro para uma integração dedicada do PolyLingo; enquanto isso, a API oferece tudo o que você precisa para construir esse fluxo você mesmo.


Uma nota sobre tipos de campo

A abordagem acima funciona bem para texto curto, texto longo e texto rico armazenado como strings. Alguns tipos de campo do Contentful precisam de tratamento separado:

Campos de texto rico armazenados no formato de documento do Contentful (não strings simples) precisam ser serializados para texto simples ou HTML antes de enviar para o PolyLingo, e depois desserializados. O pacote @contentful/rich-text-html-renderer lida com a etapa de serialização, e format: "html" no lado do PolyLingo preserva a marcação corretamente.

Campos de referência (links para outras entradas ou ativos) não devem ser traduzidos — eles são IDs, não texto. Exclua-os dos campos que você envia para a API.

Campos de número, booleano e data não são traduzíveis por natureza. Filtre-os antes de construir seu objeto fonte.


Começando

O nível gratuito do PolyLingo inclui 50.000 tokens por mês. Para um espaço Contentful com uma quantidade moderada de conteúdo, isso cobre uma passagem inicial de tradução em várias entradas em múltiplos locais, com espaço de sobra para atualizações contínuas.

A documentação completa da API está em usepolylingo.com/docs. Pacotes SDK estão disponíveis para Node.js, Python, Ruby, PHP, Java e Go.

npm install polylingo

Obtenha sua chave API