
Por qué tu contenido de Contentful aún no es realmente multilingüe
By Robert
Por qué tu contenido de Contentful aún no es realmente multilingüe
Contentful maneja bien las configuraciones regionales. Puedes definir tantas como necesites, establecer una cadena de respaldo, cambiar entre ellas en el editor y construir un frontend que sirva el idioma correcto por ruta. La infraestructura para contenido multilingüe es realmente buena.
Lo que Contentful no hace es traducir tu contenido.
Esto suena obvio cuando se dice directamente, pero es sorprendentemente fácil confundir "tenemos campos de configuración regional configurados" con "somos multilingües." Las dos cosas no son lo mismo. Los campos de configuración regional son un contenedor. Multilingüe significa que el contenedor tiene contenido.
Si tu espacio de Contentful tiene configuradas las locales francés y alemán pero los campos en esas locales están vacíos, o llenos con el texto en inglés como marcador de posición, o poblados con un primer borrador que nadie ha revisado desde que se lanzó el sitio — no eres multilingüe. Tienes la estructura para ser multilingüe.
Esta publicación trata sobre cerrar esa brecha.
Lo que Contentful realmente te ofrece
El sistema de locales de Contentful está bien diseñado. Cada entrada de contenido puede tener valores de campo por locale. Configuras un locale predeterminado, configuras fallback, y tu API de entrega devuelve el locale correcto cuando se solicita. El modelado de contenido es lo suficientemente flexible para manejar requisitos multilingües complejos a través de diferentes tipos de contenido.
Pero el sistema es completamente neutral respecto a cómo se introduce el contenido traducido. Contentful no sabe si tus campos de locale en francés contienen traducciones profesionales, texto traducido por máquina, texto en inglés pegado por accidente o nada en absoluto. Simplemente almacena y entrega lo que pongas.
El problema de la traducción es completamente tuyo para resolver.
Cómo suelen manejarlo los equipos (y dónde falla)
La mayoría de los equipos que gestionan traducciones en Contentful caen en uno de unos pocos patrones.
El patrón de exportación manual. Un desarrollador exporta contenido de Contentful, lo envía a una agencia de traducción o a un freelancer, espera a que regrese, lo reformatea y lo importa. Esto funciona para un lanzamiento único, pero se vuelve insostenible a medida que el contenido cambia. Cada actualización en el idioma fuente significa pasar por todo el ciclo nuevamente. En la práctica, el contenido traducido rápidamente queda desactualizado respecto a la fuente y nadie tiene tiempo para actualizarlo.
El patrón de traducción en el editor. Un editor abre cada entrada, cambia al idioma objetivo y traduce campo por campo, ya sea manualmente o pegando contenido en una herramienta de traducción. Esto es preciso pero lento. Tampoco escala: si tienes cientos de entradas en una docena de tipos de contenido, el volumen de trabajo manual es significativo.
El patrón de "vale así". El contenido traducido existe pero no ha sido revisado desde que se produjo por primera vez. El idioma fuente ha sido actualizado varias veces desde entonces. La versión francesa de tu página de precios aún hace referencia a un plan que retiraste hace ocho meses. La página principal en alemán aún tiene el antiguo eslogan. Nadie lo ha señalado porque nadie lo revisa.
Los tres patrones comparten el mismo problema raíz: la traducción se trata como una tarea única en lugar de una parte continua del flujo de trabajo de contenido.
Lo que realmente requiere el multilingüismo
El contenido multilingüe genuino en Contentful necesita tres cosas que funcionen juntas.
Traducción inicial precisa. Cada campo en cada idioma objetivo necesita contenido traducido que sea preciso, adecuadamente localizado y que realmente se lea como si hubiera sido escrito para ese mercado en lugar de pasar por una herramienta básica de traducción y quedar sin revisar.
Un proceso para mantener las traducciones actualizadas. Cuando el contenido fuente cambia, el contenido traducido también debe cambiar. Esta es la parte que la mayoría de los equipos subestiman. Un equipo de contenido que publica varias actualizaciones a la semana en un espacio de Contentful con cuatro idiomas y cincuenta tipos de contenido enfrenta una carga significativa de trabajo de traducción continua si se maneja manualmente.
Una forma de saber cuándo las traducciones están desactualizadas. El sistema de idiomas de Contentful no muestra la obsolescencia. Si actualizas el texto en inglés de una entrada y olvidas actualizar la versión en francés, la versión francesa seguirá sirviendo silenciosamente el contenido antiguo. Necesitas un proceso o herramientas para detectar esto.
Dónde encaja PolyLingo
La API de PolyLingo traduce contenido estructurado mientras preserva su estructura. Para Contentful, el formato relevante es JSON: los valores de campo de una entrada de Contentful, extraídos y enviados a la API, regresan traducidos con la misma estructura intacta.
El flujo básico en 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')
// Obtén la entrada que quieres traducir
const entry = await environment.getEntry('your-entry-id')
// PolyLingo usa códigos BCP-47 sin región (fr, de).
// Mapea estos a tus IDs de locales de Contentful si difieren (p. ej. fr-FR, de-DE).
const localeMap = { fr: 'fr', de: 'de' } // ajusta para que coincida con los IDs de locales de tu espacio
// Extrae solo campos de cadena en inglés — omite referencias, enlaces, booleanos y números
const sourceFields = Object.fromEntries(
Object.entries(entry.fields)
.filter(([, value]) => typeof value['en-US'] === 'string')
.map(([key, value]) => [key, value['en-US']])
)
// Traduce a francés y alemán en una sola solicitud
const result = await poly.translate({
content: JSON.stringify(sourceFields),
format: 'json',
targets: Object.keys(localeMap),
})
// Escribe los valores traducidos de vuelta en la entrada usando los IDs de locales de 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 traducida y publicada.')
Una llamada a la API devuelve tanto francés como alemán. La entrada se actualiza y publica en el mismo script.
Una nota sobre los IDs de locales: PolyLingo usa códigos BCP-47 sin sufijo de región (fr, de). Los espacios de Contentful a menudo usan IDs calificados por región como fr-FR o de-DE. El objeto localeMap arriba es donde los alineas — actualízalo para que coincida con los IDs de locales que usa tu espacio Contentful.
El mismo patrón funciona en 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')
# Extrae valores de campos en inglés
source_fields = {
key: value.get('en-US')
for key, value in entry.fields().items()
if value.get('en-US')
}
# Traduce a francés y alemán
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']
# Escribe de vuelta en la 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 traducida y publicada.')
Traducción a gran escala
Los ejemplos anteriores manejan una sola entrada. Para un espacio de Contentful con muchas entradas en múltiples tipos de contenido, el endpoint por lotes maneja hasta 100 elementos por solicitud:
// Obtener múltiples entradas y traducirlas en una llamada por lotes
const entries = await environment.getEntries({
content_type: 'blogPost',
limit: 50,
})
// Mapear los códigos de localización de PolyLingo a los IDs de localización de Contentful
const localeMap = { fr: 'fr', de: 'de', es: 'es' } // ajusta para que coincida con tu espacio
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()
}
Cincuenta entradas, tres idiomas, una solicitud por lotes.
Mantener las traducciones actualizadas
Los scripts anteriores se pueden ejecutar bajo demanda o integrarse en su flujo de trabajo de publicación. Un patrón común es ejecutar la traducción como parte de un trabajo de CI que se activa cuando cambia el contenido fuente, o en un horario que captura cualquier entrada actualizada en las últimas 24 horas.
Contentful también admite webhooks: puede configurar un webhook que se active cuando se publique una entrada en la configuración regional predeterminada, lo que puede activar automáticamente un trabajo de traducción. Integrar PolyLingo en ese webhook significa que cada vez que un editor publica contenido en inglés actualizado, las configuraciones regionales traducidas se actualizan sin ningún paso manual. Esto está en la hoja de ruta para una integración dedicada de PolyLingo; mientras tanto, la API le brinda todo lo que necesita para construir ese flujo usted mismo.
Una nota sobre los tipos de campo
El enfoque anterior funciona bien para texto corto, texto largo y texto enriquecido almacenado como cadenas. Algunos tipos de campo de Contentful necesitan un manejo separado:
Los campos de texto enriquecido almacenados en el formato de documento de Contentful (no cadenas simples) deben serializarse a texto plano o HTML antes de enviarlos a PolyLingo, y luego deserializarse de nuevo. El paquete @contentful/rich-text-html-renderer maneja el paso de serialización, y format: "html" en el lado de PolyLingo preserva correctamente el marcado.
Los campos de referencia (enlaces a otras entradas o activos) no deben traducirse — son IDs, no texto. Exclúyalos de los campos que envíe a la API.
Los campos de número, booleano y fecha no son traducibles por naturaleza. Filtre estos antes de construir su objeto fuente.
Comenzando
El nivel gratuito de PolyLingo incluye 50,000 tokens por mes. Para un espacio de Contentful con una cantidad moderada de contenido, eso cubre una pasada inicial de traducción a través de varias entradas en múltiples locales con espacio de sobra para actualizaciones continuas.
La documentación completa de la API está en usepolylingo.com/docs. Los paquetes SDK están disponibles para Node.js, Python, Ruby, PHP, Java y Go.
npm install polylingo