
Pourquoi votre contenu Contentful n'est pas encore réellement multilingue
By Robert
Pourquoi votre contenu Contentful n'est pas encore réellement multilingue
Contentful gère bien les locales. Vous pouvez en définir autant que vous le souhaitez, configurer une chaîne de secours, basculer entre elles dans l'éditeur, et construire un frontend qui sert la bonne langue selon la route. L'infrastructure pour le contenu multilingue est vraiment bonne.
Ce que Contentful ne fait pas, c'est traduire votre contenu.
Cela semble évident lorsqu'on le dit directement, mais il est surprenamment facile de confondre « nous avons configuré des champs de locale » avec « nous sommes multilingues ». Les deux ne sont pas la même chose. Les champs de locale sont un conteneur. Multilingue signifie que le conteneur contient du contenu.
Si votre espace Contentful a des locales français et allemand configurées mais que les champs dans ces locales sont vides, ou remplis avec le texte anglais comme espace réservé, ou peuplés d'une première version approximative que personne n'a revue depuis le lancement du site — vous n'êtes pas multilingue. Vous avez l'ossature pour le multilingue.
Ce post parle de combler cet écart.
Ce que Contentful vous offre réellement
Le système de localisation de Contentful est bien conçu. Chaque entrée de contenu peut avoir des valeurs de champ par locale. Vous définissez une locale par défaut, configurez des solutions de repli, et votre API de livraison renvoie la bonne locale lorsqu'elle est demandée. La modélisation du contenu est suffisamment flexible pour gérer des exigences multilingues complexes à travers différents types de contenu.
Mais le système est totalement neutre quant à la manière dont le contenu traduit est intégré. Contentful ne sait pas si vos champs de locale française contiennent des traductions professionnelles, du texte traduit par machine, du texte anglais collé par erreur, ou rien du tout. Il stocke et livre simplement ce que vous y mettez.
Le problème de la traduction est entièrement à votre charge.
Comment les équipes gèrent généralement cela (et où ça coince)
La plupart des équipes qui gèrent la traduction Contentful suivent l’un de quelques schémas.
Le modèle d’export manuel. Un développeur exporte le contenu depuis Contentful, l’envoie à une agence de traduction ou à un freelance, attend son retour, le reformate et l’importe. Cela fonctionne pour un lancement unique mais devient intenable à mesure que le contenu change. Chaque mise à jour de la langue source signifie refaire tout le cycle. En pratique, le contenu traduit prend rapidement du retard sur la source et personne n’a le temps de le rattraper.
Le modèle de traduction dans l’éditeur. Un éditeur ouvre chaque entrée, passe à la langue cible, et traduit champ par champ soit manuellement, soit en collant le contenu dans un outil de traduction. C’est précis mais lent. Cela ne s’adapte pas non plus — si vous avez des centaines d’entrées réparties sur une douzaine de types de contenu, le volume de travail manuel est important.
Le modèle « ça ira bien ». Le contenu traduit existe mais n’a pas été revu depuis sa première production. La langue source a été mise à jour plusieurs fois depuis. La version française de votre page de tarification fait toujours référence à un plan que vous avez retiré il y a huit mois. La page d’accueil allemande a toujours l’ancien slogan. Personne ne l’a signalé car personne ne vérifie.
Les trois modèles partagent le même problème fondamental : la traduction est traitée comme une tâche ponctuelle plutôt que comme une partie continue du flux de travail de contenu.
Ce que le multilingue exige réellement
Un contenu multilingue authentique dans Contentful nécessite trois éléments qui fonctionnent ensemble.
Une traduction initiale précise. Chaque champ dans chaque locale cible doit contenir un contenu traduit qui est précis, correctement localisé, et qui se lit réellement comme s'il avait été écrit pour ce marché plutôt que d'avoir été passé par un outil de traduction basique et laissé sans révision.
Un processus pour maintenir les traductions à jour. Lorsque le contenu source change, le contenu traduit doit également changer. C'est la partie que la plupart des équipes sous-estiment. Une équipe de contenu qui publie plusieurs mises à jour par semaine dans un espace Contentful avec quatre locales et cinquante types de contenu fait face à une charge de travail de traduction continue significative si cela est géré manuellement.
Un moyen de savoir quand les traductions sont obsolètes. Le système de locale de Contentful ne signale pas la vétusté. Si vous mettez à jour la copie anglaise d'une entrée et oubliez de mettre à jour la copie française, la version française continuera silencieusement à servir l'ancien contenu. Vous avez besoin soit d'un processus, soit d'un outil pour détecter cela.
Où PolyLingo s'intègre
L'API de PolyLingo traduit du contenu structuré tout en préservant sa structure. Pour Contentful, le format pertinent est JSON — les valeurs des champs d'une entrée Contentful, extraites et envoyées à l'API, reviennent traduites avec la même structure intacte.
Le flux de base 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')
// Récupérez l'entrée que vous souhaitez traduire
const entry = await environment.getEntry('your-entry-id')
// PolyLingo utilise des codes BCP-47 sans région (fr, de).
// Mappez-les à vos ID de locale Contentful s'ils diffèrent (ex. fr-FR, de-DE).
const localeMap = { fr: 'fr', de: 'de' } // ajustez pour correspondre aux ID de locale de votre espace
// Extraire uniquement les champs de chaîne en anglais — ignorer les références, liens, booléens et nombres
const sourceFields = Object.fromEntries(
Object.entries(entry.fields)
.filter(([, value]) => typeof value['en-US'] === 'string')
.map(([key, value]) => [key, value['en-US']])
)
// Traduire en français et allemand en une seule requête
const result = await poly.translate({
content: JSON.stringify(sourceFields),
format: 'json',
targets: Object.keys(localeMap),
})
// Écrire les valeurs traduites dans l'entrée en utilisant les ID de locale 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('Entrée traduite et publiée.')
Un appel API retourne à la fois le français et l'allemand. L'entrée est mise à jour et publiée dans le même script.
Une note sur les ID de locale : PolyLingo utilise des codes BCP-47 sans suffixe de région (fr, de). Les espaces Contentful utilisent souvent des ID qualifiés par région comme fr-FR ou de-DE. L'objet localeMap ci-dessus est l'endroit où vous les alignez — mettez-le à jour pour correspondre aux ID de locale utilisés par votre espace Contentful.
Le même modèle fonctionne 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')
# Extraire les valeurs des champs en anglais
source_fields = {
key: value.get('en-US')
for key, value in entry.fields().items()
if value.get('en-US')
}
# Traduire en français et allemand
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']
# Écrire dans l'entrée
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('Entrée traduite et publiée.')
Traduction à grande échelle
Les exemples ci-dessus traitent une seule entrée. Pour un espace Contentful avec de nombreuses entrées réparties sur plusieurs types de contenu, le point de terminaison batch gère jusqu'à 100 éléments par requête :
// Récupérer plusieurs entrées et les traduire en un seul appel batch
const entries = await environment.getEntries({
content_type: 'blogPost',
limit: 50,
})
// Mapper les codes de langue PolyLingo aux identifiants de langue Contentful
const localeMap = { fr: 'fr', de: 'de', es: 'es' } // ajustez pour correspondre à votre espace
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()
}
Cinquante entrées, trois langues, une requête batch.
Maintenir les traductions à jour
Les scripts ci-dessus peuvent être exécutés à la demande ou intégrés dans votre flux de publication. Un schéma courant consiste à exécuter la traduction dans le cadre d'un travail CI déclenché lorsque le contenu source change, ou selon un calendrier qui prend en compte toutes les entrées mises à jour au cours des dernières 24 heures.
Contentful prend également en charge les webhooks — vous pouvez configurer un webhook qui se déclenche lorsqu'une entrée est publiée dans la langue par défaut, ce qui peut automatiquement lancer un travail de traduction. Connecter PolyLingo à ce webhook signifie qu'à chaque fois qu'un éditeur publie du contenu anglais mis à jour, les locales traduites sont mises à jour sans aucune étape manuelle. Cela fait partie de la feuille de route pour une intégration dédiée PolyLingo ; en attendant, l'API vous fournit tout ce dont vous avez besoin pour construire ce flux vous-même.
Une note sur les types de champs
L'approche ci-dessus fonctionne bien pour le texte court, le texte long et le texte enrichi stocké sous forme de chaînes. Quelques types de champs Contentful nécessitent un traitement séparé :
Les champs de texte enrichi stockés au format document de Contentful (et non sous forme de chaînes simples) doivent être sérialisés en texte brut ou HTML avant d'être envoyés à PolyLingo, puis désérialisés. Le paquet @contentful/rich-text-html-renderer gère l'étape de sérialisation, et format: "html" côté PolyLingo préserve correctement le balisage.
Les champs de référence (liens vers d'autres entrées ou ressources) ne doivent pas être traduits — ce sont des identifiants, pas du texte. Excluez-les des champs que vous envoyez à l'API.
Les champs de type nombre, booléen et date ne sont pas traduisibles par nature. Filtrez-les avant de construire votre objet source.
Commencer
Le niveau gratuit de PolyLingo comprend 50 000 jetons par mois. Pour un espace Contentful avec une quantité modérée de contenu, cela couvre un premier passage de traduction sur plusieurs entrées dans plusieurs locales avec de la marge pour les mises à jour continues.
La documentation complète de l'API est disponible sur usepolylingo.com/docs. Des packages SDK sont disponibles pour Node.js, Python, Ruby, PHP, Java et Go.
npm install polylingo