Tilbage til bloggen
A Contentful-style content entry card showing a filled English locale tab and an empty French locale tab with a warning icon.

Hvorfor dit Contentful-indhold endnu ikke er faktisk flersproget

By Robert

Hvorfor dit Contentful-indhold endnu ikke er faktisk flersproget

Contentful håndterer lokaliteter godt. Du kan definere så mange, som du har brug for, sætte en fallback-kæde, skifte mellem dem i editoren og bygge en frontend, der leverer det rigtige sprog pr. rute. Infrastrukturen til flersproget indhold er virkelig god.

Hvad Contentful ikke gør, er at oversætte dit indhold.

Det lyder indlysende, når det siges direkte, men det er overraskende let at forveksle "vi har lokalefelter sat op" med "vi er flersprogede." De to er ikke det samme. Lokalefelter er en beholder. Flersproget betyder, at beholderen indeholder indhold.

Hvis dit Contentful-rum har franske og tyske lokaliteter konfigureret, men felterne i disse lokaliteter er tomme, eller fyldt med den engelske tekst som en pladsholder, eller befolket med et groft første udkast, som ingen har gennemgået siden siden blev lanceret — så er du ikke flersproget. Du har stilladset til flersproget.

Dette indlæg handler om at lukke det hul.


Hvad Contentful faktisk giver dig

Contentfuls lokalitetssystem er veludformet. Hver indholdsindgang kan have feltværdier pr. lokalitet. Du sætter en standardlokalitet, konfigurerer fallback-muligheder, og din leverings-API returnerer den rigtige lokalitet, når den anmodes om det. Indholdsmodelleringen er fleksibel nok til at håndtere komplekse flersprogede krav på tværs af forskellige indholdstyper.

Men systemet er helt neutralt med hensyn til, hvordan det oversatte indhold kommer ind der. Contentful ved ikke, om dine franske lokalitetsfelter indeholder professionelle oversættelser, maskinoversat tekst, engelsk tekst, der er indsat ved en fejl, eller slet ingenting. Det gemmer og leverer bare det, du putter ind.

Oversættelsesproblemet er helt dit ansvar at løse.


Hvordan teams normalt håndterer det (og hvor det bryder sammen)

De fleste teams, der håndterer Contentful-oversættelse, falder ind under et af få mønstre.

Det manuelle eksportmønster. En udvikler eksporterer indhold fra Contentful, sender det til et oversættelsesbureau eller freelancer, venter på at få det tilbage, omformaterer det og importerer det. Dette fungerer til en engangs-lancering, men bliver uholdbart, efterhånden som indholdet ændrer sig. Hver opdatering af kildesproget betyder, at hele cyklussen skal gennemføres igen. I praksis kommer det oversatte indhold hurtigt bagud i forhold til kilden, og ingen har tid til at indhente det.

Oversættelsesmønsteret i editoren. En redaktør åbner hver post, skifter til målsproget og oversætter felt for felt enten manuelt eller ved at indsætte indhold i et oversættelsesværktøj. Det er præcist, men langsomt. Det skalerer heller ikke — hvis du har hundredvis af poster på tværs af et dusin indholdstyper, er mængden af manuelt arbejde betydelig.

"Det går nok"-mønsteret. Det oversatte indhold findes, men er ikke blevet gennemgået siden det først blev produceret. Kildesproget er blevet opdateret flere gange siden da. Den franske version af din prisside henviser stadig til en plan, du udfasede for otte måneder siden. Den tyske startside har stadig det gamle slogan. Ingen har markeret det, fordi ingen tjekker.

Alle tre mønstre deler det samme grundlæggende problem: oversættelse behandles som en engangsopgave i stedet for en løbende del af indholdsarbejdsgangen.


Hvad flersprogethed faktisk kræver

Ægte flersproget indhold i Contentful kræver, at tre ting fungerer sammen.

Nøjagtig indledende oversættelse. Hver felt i hvert målområde skal have oversat indhold, der er nøjagtigt, passende lokaliseret og faktisk læses, som om det var skrevet til det marked i stedet for blot at være kørt gennem et grundlæggende oversættelsesværktøj og efterladt uden gennemgang.

En proces til at holde oversættelser opdaterede. Når kildeindhold ændres, skal det oversatte indhold også ændres. Dette er den del, som de fleste teams undervurderer. Et indholdsteam, der udgiver flere opdateringer om ugen på tværs af et Contentful-rum med fire lokaliteter og halvtreds indholdstyper, står over for en betydelig løbende oversættelsesarbejdsmængde, hvis det håndteres manuelt.

En måde at vide, hvornår oversættelser er forældede. Contentfuls lokalitetssystem viser ikke forældelse. Hvis du opdaterer den engelske kopi af en post og glemmer at opdatere den franske kopi, vil den franske version stille og roligt fortsætte med at levere det gamle indhold. Du har brug for enten en proces eller værktøjer til at fange dette.


Hvor PolyLingo passer ind

PolyLingos API oversætter struktureret indhold, mens det bevarer dets struktur. For Contentful er det relevante format JSON — en Contentful-entries feltværdier, udtrukket og sendt til API'en, kommer tilbage oversat med samme struktur intakt.

Den grundlæggende flow i 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')

// Hent den entry, du vil oversætte
const entry = await environment.getEntry('your-entry-id')

// PolyLingo bruger BCP-47-koder uden region (fr, de).
// Kortlæg disse til dine Contentful locale ID'er, hvis de er forskellige (f.eks. fr-FR, de-DE).
const localeMap = { fr: 'fr', de: 'de' } // juster for at matche dine spaces locale ID'er

// Udtræk kun engelske strengfelter — spring referencer, links, booleske værdier og tal over
const sourceFields = Object.fromEntries(
  Object.entries(entry.fields)
    .filter(([, value]) => typeof value['en-US'] === 'string')
    .map(([key, value]) => [key, value['en-US']])
)

// Oversæt til fransk og tysk i én anmodning
const result = await poly.translate({
  content: JSON.stringify(sourceFields),
  format: 'json',
  targets: Object.keys(localeMap),
})

// Skriv oversatte værdier tilbage til entry ved hjælp af Contentful locale ID'er
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 oversat og publiceret.')

Et API-kald returnerer både fransk og tysk. Entry opdateres og publiceres i det samme script.

En note om locale ID'er: PolyLingo bruger BCP-47-koder uden regionssuffix (fr, de). Contentful-spaces bruger ofte regionskvalificerede ID'er som fr-FR eller de-DE. Objektet localeMap ovenfor er stedet, hvor du tilpasser dem — opdater det for at matche de locale ID'er, som dit Contentful-space bruger.

Den samme fremgangsmåde virker i 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')

# Udtræk engelske feltværdier
source_fields = {
    key: value.get('en-US')
    for key, value in entry.fields().items()
    if value.get('en-US')
}

# Oversæt til fransk og tysk
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']

# Skriv tilbage til 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 oversat og publiceret.')

Oversættelse i stor skala

Eksemplerne ovenfor håndterer en enkelt post. For et Contentful-rum med mange poster på tværs af flere indholdstyper håndterer batch-endpointet op til 100 elementer pr. anmodning:

// Hent flere poster og oversæt dem i et enkelt batch-kald
const entries = await environment.getEntries({
  content_type: 'blogPost',
  limit: 50,
})
 
// Kortlæg PolyLingo lokalekoder til Contentful lokalekoder
const localeMap = { fr: 'fr', de: 'de', es: 'es' } // juster for at matche dit rum
 
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()
}

Halvtreds poster, tre sprog, en batch-anmodning.


Holde oversættelser opdaterede

Skripterne ovenfor kan køres efter behov eller integreres i din udgivelsesworkflow. Et almindeligt mønster er at køre oversættelse som en del af et CI-job, der udløses, når kildeindhold ændres, eller efter en tidsplan, der fanger alle poster opdateret inden for de sidste 24 timer.

Contentful understøtter også webhooks — du kan konfigurere en webhook, der aktiveres, når en post offentliggøres i standardlokalet, hvilket kan udløse et oversættelsesjob automatisk. At forbinde PolyLingo til den webhook betyder, at hver gang en redaktør offentliggør opdateret engelsk indhold, opdateres de oversatte lokaliteter uden nogen manuel handling. Dette er på vej i en dedikeret PolyLingo-integration; i mellemtiden giver API'et dig alt, hvad du behøver for at bygge den proces selv.


En note om felttyper

Fremgangsmåden ovenfor fungerer godt for kort tekst, lang tekst og rig tekst gemt som strenge. Nogle Contentful felttyper kræver særskilt håndtering:

Rich text-felter gemt i Contentfuls dokumentformat (ikke almindelige strenge) skal serialiseres til almindelig tekst eller HTML, før de sendes til PolyLingo, og derefter deserialiseres igen. @contentful/rich-text-html-renderer pakken håndterer serialiseringssteppet, og format: "html" på PolyLingo-siden bevarer markup korrekt.

Referencefelter (links til andre poster eller aktiver) bør ikke oversættes — de er ID'er, ikke tekst. Udeluk dem fra de felter, du sender til API'et.

Tal-, boolske- og datofelter er ikke oversættelige af natur. Filtrer dem fra, før du bygger dit kildeobjekt.


Kom godt i gang

PolyLingos gratisniveau inkluderer 50.000 tokens pr. måned. For et Contentful-rum med en moderat mængde indhold dækker det en indledende oversættelsesrunde på tværs af flere poster i flere lokaliteter med plads til løbende opdateringer.

Fuld API-dokumentation findes på usepolylingo.com/docs. SDK-pakker er tilgængelige for Node.js, Python, Ruby, PHP, Java og Go.

npm install polylingo

Få din API-nøgle