
Waarom je Contentful-inhoud nog niet echt meertalig is
By Robert
Waarom je Contentful-inhoud eigenlijk nog niet meertalig is
Contentful gaat goed om met locale instellingen. Je kunt er zoveel definiëren als je nodig hebt, een fallback-keten instellen, wisselen tussen deze in de editor, en een frontend bouwen die de juiste taal per route serveert. De infrastructuur voor meertalige inhoud is echt goed.
Wat Contentful niet doet, is je inhoud vertalen.
Dit klinkt vanzelfsprekend als het direct wordt gezegd, maar het is verrassend makkelijk om "we hebben locale velden ingesteld" te verwarren met "we zijn meertalig." De twee zijn niet hetzelfde. Locale velden zijn een container. Meertalig betekent dat de container inhoud bevat.
Als je Contentful-ruimte Franse en Duitse locales heeft geconfigureerd, maar de velden in die locales leeg zijn, of gevuld met de Engelse tekst als tijdelijke aanduiding, of gevuld met een ruwe eerste versie die niemand heeft nagekeken sinds de site live ging — dan ben je niet meertalig. Je hebt de basis voor meertaligheid.
Deze post gaat over het dichten van die kloof.
Wat Contentful je eigenlijk geeft
Het locale systeem van Contentful is goed ontworpen. Elke content entry kan veldwaarden per locale hebben. Je stelt een standaard locale in, configureert fallbacks, en je delivery API levert de juiste locale wanneer daarom wordt gevraagd. Het contentmodelleren is flexibel genoeg om complexe meertalige vereisten over verschillende contenttypes heen aan te kunnen.
Maar het systeem is volledig neutraal over hoe de vertaalde content daar terechtkomt. Contentful weet niet of je Franse locale velden professionele vertalingen bevatten, machinaal vertaalde tekst, per ongeluk geplakte Engelse tekst, of helemaal niets. Het slaat gewoon op en levert wat je erin stopt.
Het vertaalprobleem is volledig aan jou om op te lossen.
Hoe teams het meestal aanpakken (en waar het misgaat)
De meeste teams die Contentful-vertalingen beheren, vallen in een van een paar patronen.
Het handmatige exportpatroon. Een ontwikkelaar exporteert content uit Contentful, stuurt het naar een vertaalbureau of freelancer, wacht tot het terugkomt, formatteert het opnieuw en importeert het. Dit werkt voor een eenmalige lancering, maar wordt onhoudbaar naarmate de content verandert. Elke update van de brontaal betekent dat je de hele cyclus opnieuw moet doorlopen. In de praktijk loopt de vertaalde content snel achter op de bron en heeft niemand tijd om het bij te werken.
Het in-editor vertaalpatroon. Een redacteur opent elke invoer, schakelt over naar de doeltaal en vertaalt veld voor veld, handmatig of door content in een vertaaltool te plakken. Dit is nauwkeurig maar traag. Het schaalt ook niet — als je honderden invoeren hebt verspreid over een dozijn contenttypes, is de hoeveelheid handmatig werk aanzienlijk.
Het "het zal wel gaan" patroon. De vertaalde content bestaat, maar is niet herzien sinds het voor het eerst werd geproduceerd. De brontaal is sindsdien meerdere keren bijgewerkt. De Franse versie van je prijspagina verwijst nog steeds naar een plan dat je acht maanden geleden hebt stopgezet. De Duitse homepage heeft nog steeds de oude slogan. Niemand heeft het gemeld omdat niemand het controleert.
Alle drie de patronen delen hetzelfde onderliggende probleem: vertaling wordt behandeld als een eenmalige taak in plaats van een doorlopend onderdeel van de contentworkflow.
Wat meertaligheid eigenlijk vereist
Echte meertalige inhoud in Contentful heeft drie dingen nodig die samenwerken.
Nauwkeurige initiële vertaling. Elk veld in elke doellocatie heeft vertaalde inhoud nodig die nauwkeurig is, passend gelokaliseerd, en daadwerkelijk leest alsof het voor die markt is geschreven in plaats van door een eenvoudige vertaaltool te zijn gehaald en ongereviseerd te zijn gelaten.
Een proces om vertalingen actueel te houden. Wanneer de broninhoud verandert, moet de vertaalde inhoud ook veranderen. Dit is het deel dat de meeste teams onderschatten. Een contentteam dat meerdere updates per week publiceert in een Contentful-ruimte met vier locaties en vijftig contenttypes, kijkt tegen een aanzienlijke doorlopende vertaalwerkbelasting aan als dit handmatig wordt afgehandeld.
Een manier om te weten wanneer vertalingen verouderd zijn. Het locatiesysteem van Contentful toont geen veroudering. Als je de Engelse tekst van een item bijwerkt en vergeet de Franse tekst bij te werken, blijft de Franse versie stilletjes de oude inhoud tonen. Je hebt ofwel een proces of tooling nodig om dit te detecteren.
Waar PolyLingo past
De API van PolyLingo vertaalt gestructureerde inhoud terwijl de structuur behouden blijft. Voor Contentful is het relevante formaat JSON — de veldwaarden van een Contentful-item, geëxtraheerd en naar de API gestuurd, komen vertaald terug met dezelfde structuur intact.
De basisstroom in 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')
// Haal de entry op die je wilt vertalen
const entry = await environment.getEntry('your-entry-id')
// PolyLingo gebruikt BCP-47 codes zonder regio (fr, de).
// Map deze naar je Contentful locale-ID's als ze verschillen (bijv. fr-FR, de-DE).
const localeMap = { fr: 'fr', de: 'de' } // pas aan om te matchen met de locale-ID's van je space
// Extraheer alleen Engelse stringvelden — sla referenties, links, booleans en nummers over
const sourceFields = Object.fromEntries(
Object.entries(entry.fields)
.filter(([, value]) => typeof value['en-US'] === 'string')
.map(([key, value]) => [key, value['en-US']])
)
// Vertaal naar Frans en Duits in één verzoek
const result = await poly.translate({
content: JSON.stringify(sourceFields),
format: 'json',
targets: Object.keys(localeMap),
})
// Schrijf vertaalde waarden terug naar de entry met Contentful locale-ID's
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('Entry vertaald en gepubliceerd.')
Een enkele API-aanroep retourneert zowel Frans als Duits. De entry wordt bijgewerkt en gepubliceerd in hetzelfde script.
Een opmerking over locale-ID's: PolyLingo gebruikt BCP-47 codes zonder regio-suffix (fr, de). Contentful spaces gebruiken vaak regio-gekwalificeerde ID's zoals fr-FR of de-DE. Het localeMap object hierboven is waar je ze op elkaar afstemt — werk het bij om te matchen met welke locale-ID's jouw Contentful space gebruikt.
Hetzelfde patroon werkt in 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')
# Extraheer Engelse veldwaarden
source_fields = {
key: value.get('en-US')
for key, value in entry.fields().items()
if value.get('en-US')
}
# Vertaal naar Frans en Duits
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']
# Schrijf terug naar entry
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('Entry vertaald en gepubliceerd.')
Vertalen op grote schaal
De bovenstaande voorbeelden behandelen één enkele invoer. Voor een Contentful-ruimte met veel invoeren over meerdere contenttypen, verwerkt de batch-endpoint tot 100 items per verzoek:
// Haal meerdere invoeren op en vertaal ze in één batch-aanroep
const entries = await environment.getEntries({
content_type: 'blogPost',
limit: 50,
})
// Map PolyLingo locale codes naar Contentful locale IDs
const localeMap = { fr: 'fr', de: 'de', es: 'es' } // pas aan om bij je ruimte te passen
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()
}
Vijftig invoeren, drie talen, één batch-verzoek.
Vertalingen actueel houden
De bovenstaande scripts kunnen op aanvraag worden uitgevoerd of geïntegreerd in je publicatieworkflow. Een veelvoorkomend patroon is om vertalingen uit te voeren als onderdeel van een CI-taak die wordt geactiveerd wanneer de broninhoud verandert, of volgens een schema dat alle items bijwerkt die in de afgelopen 24 uur zijn aangepast.
Contentful ondersteunt ook webhooks — je kunt een webhook configureren die wordt geactiveerd wanneer een item wordt gepubliceerd in de standaardtaal, wat automatisch een vertaaltaak kan starten. Het koppelen van PolyLingo aan die webhook betekent dat elke keer dat een redacteur bijgewerkte Engelse inhoud publiceert, de vertaalde talen worden bijgewerkt zonder handmatige stappen. Dit staat op de roadmap voor een speciale PolyLingo-integratie; in de tussentijd biedt de API alles wat je nodig hebt om die workflow zelf te bouwen.
Een opmerking over veldtypen
De bovenstaande aanpak werkt goed voor korte tekst, lange tekst en rijke tekst die als strings worden opgeslagen. Sommige Contentful-veldtypen moeten apart worden behandeld:
Rijke-tekstvelden die in Contentful's documentformaat zijn opgeslagen (niet als gewone strings) moeten worden geserialiseerd naar platte tekst of HTML voordat ze naar PolyLingo worden gestuurd, en daarna weer worden gedeserialiseerd. Het pakket @contentful/rich-text-html-renderer verzorgt de serialisatiestap, en format: "html" aan de PolyLingo-kant behoudt de opmaak correct.
Referentievelden (links naar andere items of assets) mogen niet vertaald worden — het zijn ID's, geen tekst. Sluit ze uit van de velden die je naar de API stuurt.
Nummer-, boolean- en datumvelden zijn van nature niet vertaalbaar. Filter deze eruit voordat je je bronobject bouwt.
Aan de slag
De gratis laag van PolyLingo omvat 50.000 tokens per maand. Voor een Contentful-ruimte met een matige hoeveelheid inhoud, dekt dit een eerste vertaalronde over meerdere items in meerdere locaties met ruimte over voor doorlopende updates.
Volledige API-documentatie is te vinden op usepolylingo.com/docs. SDK-pakketten zijn beschikbaar voor Node.js, Python, Ruby, PHP, Java en Go.
npm install polylingo